Agent Mentor Learn
状態管理と永続化: 長いタスクを中断から生き延びさせる · 第 2 回 / 全 6 回

レッスン2: チェックポイント: 実行中の場面をディスクに書き出す

学習目標:

  • checkpoint.jsonに入るべき6つのフィールドを挙げ、それぞれが欠けたときに再開が何にぶつかるかを言う
  • ループの1ターンの中にある2つの保存地点(モデルがツールを指名した直後、ツール結果が記録された直後)を区別し、片方しか書かないと何を招くかを説明する
  • チェックポイントファイル自体を壊しえないsaveCheckpointを書く — その場で上書きするのではなく、一時ファイルを書いてからアトミックにリネームする

前提: レッスン1を読み、メモリと実行状態を区別できること。"Agent Harness Fundamentals: Loops and Control" のmessages配列とstop_reason駆動のループ骨格に慣れていること | 前: レッスン1 << | 次: レッスン3 >>

実行中の場面はデフォルトではメモリ上にある

レッスン1ではメモリと実行状態を切り分けました。メモリはモデルに与えるもの、実行状態はハーネス自身が抱えている実行中の場面——messages配列、ターンカウンタ、結果がまだ記録されていないツール呼び出し——です。デフォルトでは、その場面はプロセスのメモリ上にしか存在しません。プロセスが死ねば一緒に消え、ディスク上の他のファイルがすべて無傷であっても、タスクはゼロからやり直すことしかできません。

その場面をディスクに書き出し、再起動したプロセスが読み戻せるものに変えること——それがチェックポイントです。実務で長時間タスクの信頼性を実際に支えているのは、たいていの場合、あらゆる失敗をモデル自身に吸収させることではありません。"the adaptability of AI agents built on Claude with deterministic safeguards like retry logic and regular checkpoints"1(Claude上に構築されたAIエージェントの適応力を、リトライロジックや定期的なチェックポイントといった決定論的な安全装置と組み合わせること)です。本レッスンはそのチェックポイント側を扱います。何を入れるのか、ループのどこで書くのか、そして書き込みそのものをどう行うのか。間違ったやり方で書かれたチェックポイントは、チェックポイントが無い場合よりも状況を悪くしうるからです。

何を保存するか: checkpoint.jsonの6つのフィールド

チェックポイントは「メモリ上のものを全部ファイルに吐き出す」ことではありません。「ループの再開に必要なものを記録する——それ以上でも以下でもなく」です。本コースの以降のレッスンはすべて、同じプロトコルの上で動きます。

  • version: プロトコルのバージョン番号です。このフォーマットはいずれ変わります(messagesの圧縮、pendingToolUseの新しい形など)。versionがあることで、再開パスは何よりも先に「このチェックポイントを自分は認識できるか?」を問えます。知らないバージョンに対しては、歯を食いしばって解析を続けるのではなく、読み込みを拒否して大きな声で失敗すべきです。
  • task: 元のユーザータスクを言葉で書いたものです。再起動後、ハーネスのコードは自分が何をしていたかを覚えていません。読めるのはディスク上のこのファイルだけです。taskがなければ、ハーネスはそのチェックポイントがどのタスクのものかすら言えず、再開の進捗をユーザーに報告することなど到底できません。
  • turns: すでに何ターン走ったかです。"Agent Harness Fundamentals: Loops and Control" の停止条件(たとえば最大ターン数の上限)を発動させるかどうかを決めるのはこの値であり、再開がゼロからではなくそこから数え続ける数字でもあります。
  • tokensUsed: 累積のトークン消費量です。"Context Engineering: Spending Finite Attention Where It Counts" の圧縮しきい値はこの数字で発火します。これをチェックポイントから外すと、再開はカウントがゼロから始まるふりをして——圧縮の判断をことごとくずらしてしまうか——messagesの全メッセージについて使用量を推定し直すはめになります。そして多くの構成では、過去の使用量の数字はもう手に入りません。
  • messages: 会話の場面の全体、モデルが見てきたuser / assistant / tool_resultのすべてのメッセージです。チェックポイントの中で最大のものであり、唯一省けないものでもあります。モデルには自前の記憶がなく、それ以前に何が起きたかについてモデルが知っていることのすべては、次のリクエストであなたが手渡すこの配列です。これを落とせば、再開されるのは「続き」ではありません。旧実行がすでに生み出した副作用をすべて引きずったまま、ゼロから始まるまったく新しいタスクです。
  • pendingToolUse: nullか、{ id, name, input }の形をしたレコードです。モデルが指名した、結果がまだ記録されていないツールを指します。このフィールドをどう使うかはレッスン3の担当で、そこで再開が突き合わせを行います。ここでは、これが中途半端な状態を印づけるためにチェックポイントが用意した専用の枠だと分かっていれば十分です。形をシンプルに保つため、本レッスンの例はすべて1ターンにつきtool_useブロックは1つと仮定します。1ターンで複数のツール呼び出しを並行して出す場合は配列にしてください。考え方は同じです。

いつ保存するか: 1ターンに2つの保存地点

これらのフィールドをループに流し込むと、タイミングは「毎ターンの最後に1回書く」ほど単純ではないことが分かります。保存地点は2つあります。

地点Aはモデルの応答が届いた後、ツールが走る前に置きます。応答のtool_useブロックをpendingToolUseに記録してから書きます。地点Bはツール結果がmessagesに追加された後に置きます。pendingToolUsenullに戻してから、もう一度書きます。

Bだけで十分でしょうか。露出しているのはAとBの間の窓です。モデルがツールを指名し、ツールが実行中であるか、完了はしたもののその結果がmessagesに入っておらず、ディスクにも書かれていない、という窓です。この窓でプロセスが死ぬと、ディスク上の最後のチェックポイントは前のターンでBが書いたものであり、このターンの呼び出しについて何も知りません。細部が少し失われたのではなく、このツール呼び出しがディスク上に痕跡を一切残していない、ということです。レッスン3は再開時に突き合わせを行います——そのツールは本当に完了したのか、再実行が必要か——そしてその突き合わせの相手こそが、Aが書いたpendingToolUseです。本レッスンは穴を掘るところまでです。レッスン6の演習では、Bだけのチェックポイントを目の前に置き、再開で何が壊れるかを診断してもらいます。

どう保存するか: その場で上書きしてはいけない

真っ先に思いつくやり方は、stateオブジェクトをJSON.stringifyして、fs.writeFileSyncで古いcheckpoint.jsonにそのまま上書きすることです。プロセスが正常終了する場合はそれで問題ありません。しかし「正常終了」こそ、チェックポイントが用意されていない場合です。チェックポイントは、プロセスがいつ落とされるか分からないこと、電源が落ちること、コンテナが追い出されることのために存在します。ファイルの書き込みはアトミックな操作ではありません。書き込みの途中でプロセスが中断されると、ディスク上に残るcheckpoint.jsonは書きかけかもしれません。古いバージョンでもなく、新しいバージョンでもなく、ただ途中で切れたJSONです。次の再開はJSON.parseで例外を投げますし、そのファイルはタスクにとって場面の唯一のコピーでした。戻れる古いバージョンは存在しません。

取るべき手は「一時ファイルを書いてから、アトミックにリネームする」です。完全な内容をcheckpoint.json.tmpに書き込みます。その途中でクラッシュしても、犠牲になるのは一時ファイルだけで、本物のcheckpoint.jsonはクラッシュ前の無傷な古いバージョンのままであり、再開は問題なく読めます。.tmpファイルが完成したら、fs.renameSyncで本物のファイル名に付け替えます。同一ファイルシステム上では、renameは一段階のアトミックな置換です。OSはディレクトリエントリを新しいファイルに丸ごと向けるか、古いファイルに向けたままにするかのどちらかであり、途中まで名前が変わった状態というものは存在しません。

プロダクトの参照点: Claude Codeにおけるチェックポイント

本レッスンが教えるプロトコルは無人の長時間タスク向けで、粒度はループ1ターンにつき2つの保存地点です。対比として、実在のプロダクト——Claude Code——が「チェックポイント」という語をどこに置いているかを見てみましょう。"checkpointing automatically captures the state of your code before each user prompt."2(チェックポイント機能は、ユーザープロンプトのたびにコードの状態を自動的に捕捉する)。そして "Every user prompt creates a new checkpoint"2(ユーザープロンプトのたびに新しいチェックポイントが作られる)であり、"Claude Code saves checkpoints with the conversation, so you can still run /rewind after you resume a session"2(Claude Codeはチェックポイントを会話とともに保存するため、セッションを再開したあとでも/rewindを実行できる)です。

それが仕えている場面は、ここでの場面とは違います。Claude Codeのチェックポイントは人間がループに入っているセッションのために作られています。ユーザーはいつでも手を止め、あるやり方を試し、どこかのメッセージより前に戻ってもう一度やり直そうと決めることができる——だから自然な単位は「ユーザーが何かを言った」になります。あなたがここで作っているものは無人の長時間タスク向けです。止めてくれる人は誰も控えておらず、単位は「ループが1周した」であり、1ターンの中でさらに保存地点AとBに分かれます。クラッシュは「モデルがツールを指名した」と「結果が記録された」の間に落ちうるからです。この2つは同じ問題を解いているのではありません。並べて置いたのは、主に1つのことをはっきりさせるためです。チェックポイントをどれだけ細かく刻むか、どれだけ頻繁に書くかは、そのチェックポイントが何に仕えているかで決まります。唯一の答えがあるわけではありません。

チェックポイントはタダではない

チェックポイントにはコストがかかります。本レッスンのプロトコルでは、ループ1ターンにつきディスクへの書き込みが2回発生します。3ターンや5ターンで終わる短いタスクにとって、それは純粋なオーバーヘッドです。プロセスは最後まで走り切り、それらのチェックポイントファイルは一度も読まれません。この仕組みを自分のハーネスに入れるかどうかは、"you should consider adding complexity only when it demonstrably improves outcomes."3(複雑さを加えるのは、それが結果を明確に改善すると示せるときだけにすべきである)という原則に照らして測る価値があります。タスクが長く、クラッシュのコストが高いほど、この取引は割に合うようになります。数秒で終わるものなら、おそらく必要ありません。

💻 演習

まとめ

  • 実行中の場面はデフォルトではメモリ上にあり、プロセスとともに死ぬ。チェックポイントの役目は、クラッシュのたびに長時間タスクをゼロから走り直させるのではなく、壊れた地点から拾い上げさせることにある1
  • checkpoint.jsonは6つのフィールドを持つ: versiontaskturnstokensUsedmessagespendingToolUsemessagesが最大の塊であり、これがなければモデルは以前に何が起きたかを知る手がかりを一切持たない。pendingToolUseはレッスン3が突き合わせる、宙ぶらりんの呼び出しの印である
  • ループの1ターンには2つの保存地点がある。モデルがツールを指名した後・ツールが走る前のA、ツール結果がmessagesに完全に記録された後のB。Bだけで保存すると、モデルが指名したツールがまだ完了していない窓に死角ができる
  • チェックポイントファイルをその場で上書きするのは安全ではない。プロセスはいつ落とされるか分からず、書き込み途中のクラッシュは場面の唯一のコピーを半分のJSONドキュメントに変える。まず.tmpファイルを書き、fs.renameSyncで置き換えること——それが、どの瞬間にディスク上にあるものも1つの完全なバージョンであることを保証する
  • Claude Codeのチェックポイントは異なる粒度で動く。ユーザープロンプトのたびに自動的に捕捉され2、人間がループに入っているセッションに仕えている。本レッスンが作るものは無人の長時間タスク向けだ。刻む地点は違うが、どちらも同じ問いに答えている: 何かがうまくいかなかったとき、どこへ戻るのか?
  • チェックポイントはタダではない。1ターンにつき2回のディスク書き込みは短いタスクでは純粋なオーバーヘッドであり、加えるべきかどうかは、多ければよいという思い込みではなく、それが結果を明確に改善するかどうかで決まる3

>> レッスン3: チェックポイントからの再開: ループを再起動する

Footnotes

  1. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2

  2. Checkpointing — Claude Code Docs — https://code.claude.com/docs/en/checkpointing 2 3 4

  3. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2

練習

01

ある人がsaveCheckpointをこう書きました。

レベル1: 不完全なチェックポイントを埋める

本レッスンが定めたプロトコルに照らして、このチェックポイントにはまだどのフィールドが欠けていますか? 欠けているフィールドそれぞれについて、この不完全なチェックポイントから再開しようとしたとき、正確にどこで破綻するかを具体的に述べてください。

完了基準 · ローカルでチェック
02

同僚が、ツールを2回続けて呼ぶタスクを実行するために、以下のrunAgentを書きました。普段は問題なく見えますが、実行の途中でプロセスが落とされた瞬間、復旧できる場面は開けないか、辻褄が合わないかのどちらかになります。クラッシュで崩れる2箇所を見つけ、それぞれが何を招くかを述べ、修正してください。修正後のコードは実際に動くものでなければなりません。

レベル2: 潜在する2つの障害を直す
完了基準 · ローカルでチェック