Point de contrôle et reprise
- Leçon 11
- 50 min
- Module 4 : Approbation humaine et reprise
Leçon 11 sur 18 · Agents d'IA et orchestration des flux de travailPoint de contrôle et reprise
1. Choisir le bon modèle d'orchestration
2. Représenter l'état et les outils
3. Raisonnement, planification et contexte
4. Approbation humaine et reprise
5. Plusieurs exécutants et intégrations
6. Évaluer et exploiter un agent borné
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 main
Ce que vous devez savoir
- La demande d’approbation,
approve(approuver) etreject(refuser), dans Limites d’approbation. - Les champs de
TaskState: état, preuves, messages, proposition, dans Un état de tâche explicite et Gérer le contexte de travail.
Ce qu’il vous faut sur votre ordinateur
- Le projet après Limites d’approbation : les six fichiers du projet de
end-of-m04et deux nouveaux fichiers de tests.python -m pytestaffiche45 passed(45 tests réussis). - Une base de données d’exercice réinitialisée : exécutez
python -m resolver reset --allune 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 note sur le mur de la boulangerie
Section intitulée « Une note sur le mur de la boulangerie »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.
Le problème : l’état reste dans le processus
Section intitulée « Le problème : l’état reste dans le processus »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 :
python -m resolver run T-90103 --repeat 1python -m resolver run T-90103 --repeat 2Actions (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 1Seules 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é.
L’idée : sauvegarder l’état, pas le processus
Section intitulée « L’idée : sauvegarder l’état, pas le processus »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 :
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.
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 :
python -m resolver run T-65273La 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 :
python -m resolver runsT-65273-agent-chat-small-1 waiting_approval 2026-10-09T17:00:50-04:00Lundi, 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 :
python -m resolver resume T-65273-agent-chat-small-1Resuming 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 aboveLa 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.
Le monde peut changer pendant l’attente
Section intitulée « Le monde peut changer pendant l’attente »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 WorldWorld(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 :
python -m resolver approve T-65273-agent-chat-small-1 --as grace --name Gracerequest_refund LK-507862: failed (operation b029a4020ded5cae, 1 call(s)) PAY-507862-2 is not a captured charge of LK-507862Status: handed_to_personLe 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.
Un plantage au milieu de la boucle
Section intitulée « Un plantage au milieu de la boucle »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 CheckpointStorefrom resolver.data import load_taskfrom resolver.providers import MockProviderfrom resolver.runner import new_state, run_taskfrom 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 1Tapez exit() pour quitter Python. Le point de contrôle contient l’état après l’étape 1, avec l’état running :
python -m resolver runspython -m resolver resume T-65273-crash-1T-65273-crash-1 running 2026-10-09T17:00:51-04:00Resuming 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.00081La 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.
Ce qui survit à un plantage
Section intitulée « Ce qui survit à un plantage »L’image montre où chaque élément se trouve quand le processus s’arrête.
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 :
python -m resolver run T-90104 --variant agent_structured --model chat-strongpython -m resolver approve T-90104-agent_structured-chat-strong-1 --as agent --interrupt before-writeThe process stopped: simulated crash before the write of 797fbb5bbb9f8a7e. Resume with: python -m resolver resume T-90104-agent_structured-chat-strong-1La 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 :
python -m resolver resume T-90104-agent_structured-chat-strong-1Resuming T-90104-agent_structured-chat-strong-1 (status executing)request_refund LK-793996: done (operation 797fbb5bbb9f8a7e, 1 call(s))Status: doneUn 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.
Quand créer un point de contrôle, et à quel coût
Section intitulée « Quand créer un point de contrôle, et à quel coût »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 pouroperationsetfaults_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.
Exercice guidé : tester le point de contrôle
Section intitulée « Exercice guidé : tester le point de contrôle »-
Vérifiez votre point de départ :
Fenêtre de terminal python -m pytestLa commande affiche
45 passed. -
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 CheckpointStorefrom resolver.data import load_taskfrom resolver.runner import new_statedef 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 mainmain(["run", "T-90104", "--variant", "agent", "--repeat", "1"])main(["runs"])assert "T-90104-agent-chat-small-1" in capsys.readouterr().outLe 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/. -
Exécutez les tests :
Fenêtre de terminal python -m pytestLa commande affiche
47 passed(47 tests réussis). -
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écutezpython -m resolver changes. La sortie montre un remboursement de 9,95.
À vous de jouer : planter à une autre étape
Section intitulée « À vous de jouer : planter à une autre étape »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 :
python -m resolver resume T-65273-crash-2python -m resolver trace T-65273-crash-2La 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érification des connaissances
Section intitulée « Vérification des connaissances »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é.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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
TaskStatedanswork/runs.sqliteaprè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 :
resumeetapprove(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