Aller au contenu
English

Point de contrôle et reprise

  • Leçon 11
  • 50 min
  • Module 4 : Approbation humaine et reprise

Ce que vous saurez faire

Sauvegardez l’état complet d’une tâche après chaque étape. Un nouveau processus pourra alors la poursuivre après un plantage ou une approbation qui prend trois jours.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet après Limites d’approbation : les six fichiers du projet de end-of-m04 et deux nouveaux fichiers de tests. python -m pytest affiche 45 passed (45 tests réussis).
  • Une base de données d’exercice réinitialisée : exécutez python -m resolver reset --all une fois avant de commencer.

Cette leçon ajoute un fichier de tests. Chaque plantage y est simulé, et chaque écriture va dans la base de données d’exercice.

Une boulangerie ouvre à sept heures. La boulangère de nuit commence la pâte à quatre heures, puis son quart se termine. Elle ne garde pas tout le travail en tête en rentrant chez elle. Elle écrit sur le tableau près du four : « Pâte de seigle mélangée à 4 h 10. Sel ajouté. Première levée jusqu’à 6 h. » La boulangère du matin lit le tableau et poursuit le travail.

Deux choses rendent ce tableau utile. Il contient tout ce dont la prochaine boulangère a besoin, pas seulement la dernière étape. Et on le remplit pendant le travail, pas à la fin du quart. Si la boulangère de nuit tombe malade à 5 h, le tableau indique quand même ce qui est fait.

Le tableau a un point faible. Supposons que la dernière ligne indique « ajout du sel », sans rien après. Le sel a-t-il été ajouté? La boulangère du matin ne peut pas le savoir en lisant le tableau. Elle doit goûter la pâte. La dernière leçon de ce module porte sur cette vérification.

Avant le module 4, le projet gardait chaque exécution uniquement en mémoire. run affichait la proposition et se terminait, puis l’état disparaissait. Cela suffisait quand le flux de travail ne faisait que proposer. Maintenant, une proposition doit attendre une personne, et les gens ne sont pas toujours disponibles.

T-65273 concerne une facturation en double de 119 dollars. Il faut donc l’approbation d’un responsable d’équipe ou de Grace. Grace va l’approuver, mais elle est absente jusqu’à lundi. Vendredi, le terminal d’Amira affiche la proposition. Que lui arrive-t-il pendant la fin de semaine?

Si l’état reste seulement dans le processus, il y a deux mauvais choix. Amira peut garder le processus actif pendant trois jours. Une mise à jour du portable, un couvercle fermé ou un plantage y met fin. Ou elle peut relancer le billet de soutien lundi. Cela coûte une deuxième série d’appels au modèle, et la proposition peut être différente.

Ce deuxième point est réel, ce n’est pas une supposition. L’agent chat-small a trois exécutions enregistrées de chaque billet. Pour 11 billets, au moins une exécution a donné une proposition différente. Voici T-90103, le pot fissuré, dans ses deux premiers enregistrements :

Fenêtre de terminal
python -m resolver run T-90103 --repeat 1
python -m resolver run T-90103 --repeat 2
Actions (nothing has happened yet):
1. reship_item LK-640436 PLANTER x 1
2. create_return_label LK-640436 PLANTER reason damaged (return shipping: free)
Actions (nothing has happened yet):
1. reship_item LK-640436 PLANTER x 1

Seules les lignes d’actions sont montrées. La deuxième exécution a oublié l’étiquette de retour gratuite demandée par W5. Si Grace a approuvé la première proposition vendredi, une nouvelle exécution lundi ne correspond pas à ce qu’elle a approuvé.

Un point de contrôle est une copie sauvegardée de l’état complet d’une tâche à un moment connu. Son stockage persiste après la fin du processus. Reprendre signifie démarrer un nouveau processus, charger le dernier point de contrôle et poursuivre à partir de là. Comme le tableau de la boulangerie, le point de contrôle est écrit pendant le travail : après chaque étape, pas à la fin.

Le nom vient des longues courses et des jeux vidéo. À un point de contrôle, votre progression est enregistrée. Si un problème survient plus tard, vous repartez de là, pas de la ligne de départ.

Que doit contenir le point de contrôle? Tout ce dont la prochaine étape a besoin, sans rien qui existe seulement dans le processus. Dans ce projet, c’est tout le TaskState :

  • l’état : où en est la tâche (running (en cours), waiting_approval (en attente d’approbation), executing (exécution des actions), done (terminée)…);
  • les preuves et les messages : les résultats des outils et le contexte de travail du modèle pour le prochain appel de la boucle;
  • la proposition et l’approbation, avec son empreinte;
  • les opérations : chaque écriture approuvée, son identifiant, son état et le nombre d’appels nécessaires;
  • les défaillances déjà utilisées, pour qu’une défaillance simulée ne survienne pas deux fois.

La boucle ouverte, les variables Python et un appel qui attend une réponse ne sont pas dans le point de contrôle. Ils disparaissent avec le processus, et la reprise n’en a pas besoin.

Le stockage des points de contrôle du projet est resolver/checkpoint.py, qui compte 52 lignes. Il garde une ligne par exécution dans work/runs.sqlite, avec l’état complet en JSON. save() remplace la ligne, et load() la reconvertit en TaskState :

resolver/checkpoint.py (part)
def save(self, state: TaskState) -> None:
state.updated_at = now()
with closing(sqlite3.connect(self.path)) as db:
db.execute("INSERT OR REPLACE INTO runs VALUES (?,?,?,?,?,?,?)",
(state.run_id, state.task_id, state.variant, state.model, state.status, state.updated_at,
state.model_dump_json()))
db.commit()
def load(self, run_id: str) -> TaskState:
with closing(sqlite3.connect(self.path)) as db:
row = db.execute("SELECT state FROM runs WHERE run_id = ?", (run_id,)).fetchone()
if row is None:
raise KeyError(f"No run {run_id}. Run `python -m resolver runs` to see the saved runs.")
return TaskState.model_validate_json(row[0])

Pydantic fait le travail difficile. model_dump_json() écrit chaque champ, et model_validate_json() vérifie chaque champ au chargement. Un point de contrôle avec un champ manquant ou incorrect produit une erreur explicite au chargement.

run sauvegarde après chaque étape de la boucle (on_step), puis à la fin. approve, reject et l’exécuteur sauvegardent après chaque changement d’état et avant chaque écriture. L’état sur disque n’a donc jamais plus d’une étape de retard.

L’image montre les états d’une exécution et ce que fait resume (reprendre) à partir de chacun.

Diagramme des états d'une exécution. running, la boucle avec un appel au modèle par étape, passe à waiting_approval lors d'une fin avec actions, à done pour une réponse seule, et à handed_to_person ou stopped lors d'un transfert à une personne ou d'une limite atteinte. waiting_approval, une proposition avec actions et aucune écriture, passe à executing après approve et à handed_to_person après reject. executing, les écritures approuvées une opération à la fois, passe à done quand toutes les opérations sont terminées, et à handed_to_person quand une écriture est refusée ou échoue. Notes : depuis running, resume relance la boucle à partir des messages sauvegardés; depuis waiting_approval, resume affiche de nouveau la demande d'approbation; depuis executing, resume relance execute() et rapproche les opérations en attente.
Les états d'une exécution. Le point de contrôle est sauvegardé à chaque étape et à chaque changement d'état. Les notes en pointillé indiquent ce que fait resume depuis chaque état non final.
Description textuelle du diagramme

running : la boucle fait un appel au modèle par étape. Avec une proposition contenant des actions, elle passe à waiting_approval. Une réponse seule se termine dans done. Un transfert à une personne ou une limite de la boucle mène à handed_to_person ou stopped. Une reprise depuis running relance la boucle à partir des messages sauvegardés.

waiting_approval : une proposition avec des actions, et aucune écriture. L’approbation mène à executing; le refus mène à handed_to_person. La reprise affiche de nouveau la demande d’approbation.

executing : les écritures approuvées, une opération à la fois. Quand toutes les opérations sont terminées, l’exécution passe à done. Une écriture refusée ou qui échoue continuellement mène à handed_to_person. La reprise relance execute() et rapproche les opérations en attente.

Exemple détaillé : une approbation qui attend lundi

Section intitulée « Exemple détaillé : une approbation qui attend lundi »

Vendredi, Amira lance T-65273 :

Fenêtre de terminal
python -m resolver run T-65273

La sortie se termine par Next: python -m resolver approve T-65273-agent-chat-small-1 --as team_lead (prochaine étape : lancer cette commande d’approbation). L’exécution est sauvegardée, alors Amira peut fermer le terminal. runs liste toutes les exécutions sauvegardées, leur état et l’heure de leur dernier point de contrôle :

Fenêtre de terminal
python -m resolver runs
T-65273-agent-chat-small-1 waiting_approval 2026-10-09T17:00:50-04:00

Lundi, Grace ouvre un nouveau terminal. Elle n’a pas besoin du processus d’Amira. resume charge le point de contrôle et, pour une exécution en attente d’approbation, affiche de nouveau la demande :

Fenêtre de terminal
python -m resolver resume T-65273-agent-chat-small-1
Resuming T-65273-agent-chat-small-1 (status waiting_approval)
Approval needed for run T-65273-agent-chat-small-1 (ticket T-65273)
Proposed by: agent, chat-small. Rule: W7.
Actions (nothing has happened yet):
1. request_refund LK-507862 119.00 CAD RFD-DOUBLE payment PAY-507862-2
Who may approve: team lead or above

La sortie continue avec le motif, les preuves et le brouillon de réponse, comme vendredi. C’est la même proposition, car c’est le même état sauvegardé : aucun appel au modèle, aucun nouveau coût. Avant que Grace l’approuve, pensez à ce qui pourrait aussi changer en trois jours.

Vous ne pouvez pas attendre trois jours dans une leçon. Le temps ne change rien au point de contrôle. Après une minute ou une semaine, le processus qui approuve lit la même ligne.

Un point de contrôle sauvegarde la tâche, pas le monde. Pendant la fin de semaine, le client a téléphoné au soutien. Un collègue a remboursé manuellement la deuxième facturation. Simulez cet appel dans la base de données d’exercice. Collez ceci dans Python, dans le dossier du projet :

from resolver.systems import World
World(ticket_id="phone-call").request_refund("manual-1", "LK-507862", 119.00, "RFD-DOUBLE", "PAY-507862-2")

Python affiche le nouveau remboursement, RF-4CFEAE8E. Tapez exit() pour quitter Python. Maintenant, Grace approuve la proposition sauvegardée :

Fenêtre de terminal
python -m resolver approve T-65273-agent-chat-small-1 --as grace --name Grace
request_refund LK-507862: failed (operation b029a4020ded5cae, 1 call(s)) PAY-507862-2 is not a captured charge of LK-507862
Status: handed_to_person

Le service de paiement vérifie sa propre règle : il rembourse seulement une facturation encaissée. La facturation a déjà été remboursée, alors il refuse. L’exécution passe à une personne, sans deuxième remboursement. C’est pourquoi le projet vérifie de nouveau au moment de l’écriture. execute() appelle encore authorize_write(), et les services vérifient leurs propres invariants. Une vérification faite vendredi ne dit rien sur lundi.

Exécutez python -m resolver reset --all avant le prochain exemple.

Voici maintenant un plantage pendant que l’agent travaille encore. Le projet n’a pas de commande pour cela, alors nous le simulons dans Python. L’exécution sauvegarde son état après chaque étape, puis « plante » après l’étape 1. Collez ceci dans Python, dans le dossier du projet :

from resolver.checkpoint import CheckpointStore
from resolver.data import load_task
from resolver.providers import MockProvider
from resolver.runner import new_state, run_task
from resolver.systems import PRACTICE_DB, Faults, World
class Crash(Exception):
pass
def save_then_crash(state):
store.save(state)
if state.step == 1:
raise Crash("simulated crash after step 1")
store = CheckpointStore()
task = load_task("T-65273")
state = new_state(task, "agent", "chat-small", "T-65273-crash-1")
world = World(PRACTICE_DB, task.task_id, Faults(task.faults))
try:
run_task(task, "agent", "chat-small", MockProvider().complete, world, state=state, on_step=save_then_crash)
except Crash as e:
print(e)
simulated crash after step 1

Tapez exit() pour quitter Python. Le point de contrôle contient l’état après l’étape 1, avec l’état running :

Fenêtre de terminal
python -m resolver runs
python -m resolver resume T-65273-crash-1
T-65273-crash-1 running 2026-10-09T17:00:51-04:00
Resuming T-65273-crash-1 (status running)
Outcome: resolve (rule W7) | 3 model call(s), 2 tool call(s) | 7155 tokens in, 192 out | US$0.00081

La demande d’approbation suit ensuite. Regardez les comptes : 3 appels au modèle et 7 155 jetons (tokens) en entrée, comme une exécution sans plantage. La reprise n’a pas répété l’étape 1. Elle a chargé les messages sauvegardés et les preuves de get_order, puis demandé au modèle d’effectuer l’étape 2. python -m resolver trace T-65273-crash-1 montre les trois étapes, numérotées 1, 2 et 3.

Pourquoi est-ce important? Un appel au modèle coûte de l’argent et du temps, et il peut donner une réponse différente. Une reprise depuis le début paie de nouveau et peut produire une proposition différente, comme l’a montré T-90103. Une reprise depuis le point de contrôle paie seulement les étapes restantes.

L’image montre où chaque élément se trouve quand le processus s’arrête.

Trois zones. Le processus Python contient les variables, la boucle ouverte et l'appel qui attend une réponse; un plantage simulé les fait disparaître. Le stockage des points de contrôle work/runs.sqlite contient le TaskState, sauvegardé après chaque étape : état, preuves, messages, proposition, approbation, opérations avec identifiant, état et appels effectués. La base de données d'exercice work/larkfield.sqlite contient les remboursements, les étiquettes, les réexpéditions et le registre des opérations, qui associe l'identifiant d'opération au résultat. Une reprise, dans un nouveau processus, charge l'état depuis le stockage des points de contrôle et demande à la base de données d'exercice si une opération donnée a eu lieu.
Ce qu'un plantage fait disparaître, et les deux registres utilisés par une reprise : le point de contrôle de la tâche et le registre des opérations du service.
Description textuelle du diagramme

Le processus Python contient les variables, la boucle ouverte et tout appel qui attend une réponse. Un plantage les fait tous disparaître.

Le stockage des points de contrôle, work/runs.sqlite, contient le TaskState, sauvegardé après chaque étape. Il contient l’état, les preuves, les messages, la proposition et l’approbation, ainsi que les opérations avec leur identifiant, état et nombre d’appels.

La base de données d’exercice, work/larkfield.sqlite, contient les remboursements, les étiquettes et les réexpéditions. Elle contient aussi le registre des opérations : pour chaque identifiant d’opération, son résultat.

Une reprise démarre un nouveau processus. Elle charge l’état depuis le stockage des points de contrôle. Pour une écriture en attente, elle demande à la base de données d’exercice si cette opération a eu lieu.

Il y a deux registres, gérés par des systèmes différents. Le point de contrôle indique ce que le flux de travail voulait faire et où il en était. Le registre du service indique ce qui s’est réellement passé. Une écriture se situe entre les deux, et un plantage peut arriver au pire moment.

Essayez. Un plantage juste avant une écriture est le cas simple :

Fenêtre de terminal
python -m resolver run T-90104 --variant agent_structured --model chat-strong
python -m resolver approve T-90104-agent_structured-chat-strong-1 --as agent --interrupt before-write
The process stopped: simulated crash before the write of 797fbb5bbb9f8a7e. Resume with: python -m resolver resume T-90104-agent_structured-chat-strong-1

La sortie de run n’est pas montrée ici : c’est la demande d’approbation pour un remboursement de 9,95. changes affiche 0 change(s) in the practice database (aucun changement dans la base de données d’exercice). runs montre l’exécution dans l’état executing. Reprenez-la :

Fenêtre de terminal
python -m resolver resume T-90104-agent_structured-chat-strong-1
Resuming T-90104-agent_structured-chat-strong-1 (status executing)
request_refund LK-793996: done (operation 797fbb5bbb9f8a7e, 1 call(s))
Status: done

Un remboursement, un appel. L’approbation était dans le point de contrôle. La reprise ne l’a donc pas redemandée, mais elle ne l’a pas contournée non plus. Un deuxième resume affiche Nothing to resume: the run is done. (rien à reprendre : l’exécution est terminée).

Un plantage juste après l’écriture est le cas difficile. Le remboursement a eu lieu, mais le point de contrôle indique encore « en attente ». C’est le tableau de la boulangerie dont la dernière ligne indique « ajout du sel ». La prochaine leçon montre ce que fait alors l’exécuteur.

Erreur courante

« Relancez simplement le billet lundi »

Symptôme : vendredi, Grace lit la proposition pour T-90103 (réexpédier le pot, étiquette de retour gratuite) et dit oui. Lundi, Tomás relance le billet, voit une proposition pour la même commande et l’approuve. Le client reçoit un nouveau pot, sans étiquette de retour. python -m resolver runs montre deux exécutions de T-90103.

Cause : l’exécution a été répétée au lieu d’être reprise. Un modèle peut donner une proposition différente pour le même billet. Dans les enregistrements, la deuxième exécution de T-90103 proposait la réexpédition sans l’étiquette. La deuxième exécution a aussi coûté une deuxième série d’appels au modèle.

Diagnostic : comparez les actions des deux exécutions, ligne par ligne. Vérifiez qui a approuvé chaque exécution. L’approbation de Grace appartient à la première exécution et à son empreinte, pas au billet.

Correction : sauvegardez l’état à chaque étape et poursuivez une exécution en attente avec resume ou approve, jamais avec un nouveau run. Si un billet exige vraiment une nouvelle exécution, traitez-la comme une nouvelle proposition qui nécessite une nouvelle approbation.

Créez un point de contrôle pour toute tâche qui peut attendre une personne ou qui écrit dans un autre système. Les deux s’appliquent à Larkfield. Une tâche qui lit seulement et répond en deux secondes peut s’en passer. Si elle échoue, vous la relancez, et rien n’a changé.

Les coûts sont faibles, mais réels :

  • Une écriture par étape. Une ligne SQLite par exécution, remplacée à chaque étape. Ici, quelques millisecondes. Un système très sollicité exige une véritable base de données et un plan pour de nombreux processus d’écriture.
  • Des données stockées. Les messages contiennent le texte du billet et les détails de la commande. Un point de contrôle contient des renseignements personnels, comme la mémoire dans Concevoir la mémoire. Décidez combien de temps le garder et supprimez les anciennes exécutions.
  • Des changements à l’état. Si une version ultérieure ajoute un champ obligatoire à TaskState, les anciens points de contrôle ne se chargent plus. Donnez une valeur par défaut aux nouveaux champs, comme pour operations et faults_seen, ou migrez les anciennes lignes.
  • Des faits périmés. Un point de contrôle peut dater de plusieurs jours. Vérifiez de nouveau au moment de l’écriture, comme le service de paiement ci-dessus.
  1. Vérifiez votre point de départ :

    Fenêtre de terminal
    python -m pytest

    La commande affiche 45 passed.

  2. Créez le fichier tests/test_checkpoint.py. Copiez-le depuis l’instantané end-of-m04, ou saisissez-le :

    tests/test_checkpoint.py
    """Checkpoint and resume: the whole state survives the process."""
    from resolver.checkpoint import CheckpointStore
    from resolver.data import load_task
    from resolver.runner import new_state
    def test_save_and_load_the_whole_state(tmp_path):
    store = CheckpointStore(tmp_path / "runs.sqlite")
    s = new_state(load_task("T-90103"), "agent", "chat-small", "r-1")
    s.plan = ["look up the order", "propose"]
    s.log("note", "hello", x=1)
    store.save(s)
    back = CheckpointStore(tmp_path / "runs.sqlite").load("r-1")
    assert back.model_dump() == s.model_dump()
    assert store.latest("T-90103").run_id == "r-1"
    def test_a_crashed_cli_run_resumes(capsys):
    from resolver.__main__ import main
    main(["run", "T-90104", "--variant", "agent", "--repeat", "1"])
    main(["runs"])
    assert "T-90104-agent-chat-small-1" in capsys.readouterr().out

    Le premier test charge l’état avec un nouvel objet de stockage, comme le ferait un nouveau processus. Le deuxième lance l’interface en ligne de commande et vérifie que l’exécution figure dans la liste sauvegardée. Les tests utilisent un dossier temporaire, pas votre dossier work/.

  3. Exécutez les tests :

    Fenêtre de terminal
    python -m pytest

    La commande affiche 47 passed (47 tests réussis).

  4. Exécutez python -m resolver reset --all. Répétez ensuite le plantage avant l’écriture et la reprise de cette leçon, puis exécutez python -m resolver changes. La sortie montre un remboursement de 9,95.

Modifiez l’exemple Python pour qu’il plante après l’étape 2 de T-65273, avec un nouvel identifiant d’exécution. Avant la reprise, faites une prédiction : combien d’appels au modèle l’exécution reprise rapportera-t-elle? Que montrera trace à l’étape 2? Reprenez ensuite l’exécution et vérifiez.

Afficher un exemple de réponse

Modifiez deux lignes : if state.step == 2: dans save_then_crash, et l’identifiant d’exécution "T-65273-crash-2". Exécutez le code, puis :

Fenêtre de terminal
python -m resolver resume T-65273-crash-2
python -m resolver trace T-65273-crash-2

La ligne de résultat est la même qu’avant : 3 model call(s), 2 tool call(s) | 7155 tokens in, 192 out (3 appels au modèle, 2 appels d’outils, 7 155 jetons en entrée et 192 en sortie). Les appels sont comptés dans l’état sauvegardé, alors l’exécution reprise rapporte les trois. Un seul, celui de l’étape 3, a eu lieu après la reprise. La trace montre get_payments à l’étape 2, avant le plantage, puis finish à l’étape 3. Les preuves et les messages sauvegardés permettent au modèle de continuer, sans rien demander deux fois.

Vérifiez votre compréhension

Que doit contenir un point de contrôle de ce flux de travail pour qu'un nouveau processus puisse continuer? Que peut-il exclure?

Afficher la réponse

Il doit contenir tout l’état de la tâche. Cela comprend le statut, les éléments probants et les messages pour le prochain appel au modèle. Il comprend aussi la proposition et l’approbation avec son empreinte. Il contient également les opérations avec leurs identifiants et leurs statuts, ainsi que les défaillances simulées déjà utilisées. Il n’a pas besoin de conserver le processus : les variables Python, la boucle ouverte ou un appel qui attend une réponse. Ces éléments disparaissent avec le processus. La reprise reconstruit ce dont elle a besoin à partir de l’état sauvegardé.

Vérifiez votre compréhension

T-65273 a attendu Grace du vendredi au lundi. Pourquoi vaut-il mieux « reprendre l'exécution sauvegardée » que « traiter le billet à nouveau »? Donnez deux raisons avec des chiffres de cette leçon.

Afficher la réponse

D’abord, la proposition reste la même. Dans les enregistrements, 11 billets ont reçu une proposition différente dans au moins une des trois exécutions. La deuxième exécution de T-90103 a omis l’étiquette de retour gratuite. L’approbation porte sur la proposition que la personne responsable de l’approbation a lue. Ensuite, aucun nouveau coût. La reprise n’a fait aucun appel au modèle. Une nouvelle exécution de T-65273 fait toutefois 3 appels et utilise à nouveau 7 155 jetons en entrée.

Vérifiez votre compréhension

Pendant la fin de semaine, un collègue a remboursé manuellement le deuxième paiement de T-65273. Lundi, Grace approuve la proposition sauvegardée. Que se passe-t-il, et quelle vérification détecte le problème?

Afficher la réponse

L’exécutant appelle le service de paiement, qui refuse : PAY-507862-2 is not a captured charge of LK-507862 (PAY-507862-2 n’est pas un paiement encaissé pour LK-507862). L’exécution passe à une personne, sans deuxième remboursement. La vérification porte sur l’invariant du service lui-même, vérifié au moment de l’écriture. Le point de contrôle a sauvegardé la tâche, pas le monde. Le flux de travail doit donc vérifier de nouveau le monde avant de le modifier.

Récapitulatif

  • Un point de contrôle est une copie sauvegardée de tout l’état de la tâche à un moment connu. Son stockage persiste après la fin du processus. La reprise la charge dans un nouveau processus et continue à partir de là.
  • Le projet sauvegarde TaskState dans work/runs.sqlite après chaque étape et chaque changement de statut. runs (exécutions) liste les exécutions sauvegardées; resume (reprendre) en poursuit une.
  • Une approbation peut attendre plusieurs jours : resume et approve (approuver) lisent la même proposition sauvegardée, sans nouvel appel au modèle. Traiter le billet à nouveau peut donner une proposition différente.
  • Après un plantage dans la boucle, l’exécution reprend à l’étape suivante : T-65273 a tout de même fait 3 appels au modèle au total.
  • Le point de contrôle sauvegarde la tâche, pas le monde : vérifiez de nouveau au moment de l’écriture. Un plantage juste après une écriture est le cas difficile, abordé dans la prochaine leçon.
  • tests/test_checkpoint.py : 47 passed (47 tests réussis).

Leçon suivante : Effets de bord et nouvelles tentatives