第 34 章:DAG から Agent Loop へ
「グラフで組めばいい」——次のノードが、前のノードを走らせて初めて分かることに依存するまでは、その通り。
5 分で要点掴む
- 事前に描ける制御フローはグラフ、観測に依存する制御フローはループ
- 選択を誤ったサイン:ノードが必ず本番に驚かされた後にしか追加されない
- 本番は最終的に混成へ収束する。決定的な外殻 + 適応的な中核 + 決定的な出口
- 常駐 Daemon は常駐 Loop を意味しない。Loop は Turn ごとに作り直される
- 連続性は永続化した Session と明示的な履歴スナップショットから来る。オブジェクトの寿命ではない
10 分パス:34.1 → 34.4-34.5 → Kocoro OSS Lab
公開ソースのスナップショット:実装例は 2026-07-27 に確認した Kocoro 公開
origin/mainのコミット4ec6772に基づきます。学ぶべき対象は不変条件であり、定数は最新ソースで再確認してください。
34.1 増え続けて止まらなかったグラフの話
あるチームが競合調査パイプラインを DAG で作りました。初版はノード 5 つ、きれいなものです。
fetch_sources → extract_claims → cross_check → synthesize → format
そして本番に出ました。
あるソースは記事ではなくペイウォールを返す——ノードを 1 つ追加。ある PDF はスキャン画像だった——OCR 分岐を追加。ベンダーがフィールド名を変えた——条件つきの正規化を追加。2 つのソースが矛盾し、統合器が自信満々に平均を取って、どちらのソースも言っていない主張を出力した——調停ノードを追加。半年後、グラフは 40 いくつのノードになり、そのおよそ 3 分の 2 は初日に誰も列挙できなかった状態への対処です。
面白いのはノード数ではありません。いつノードが追加されたかです。どれも本番が誰かを驚かせた後に足されています。これは「事前には手に入らない観測に制御フローが依存している」ことの署名です。
逆向きの誤りはもっと静かで、もっと高くつきます。別のチームは固定の夜間 ETL——5 ステップ、順序固定、成功条件固定、2 年間一度も変わっていない——を「柔軟性のために」Agent Loop で包みました。以来、一度も変わらないパイプラインを再発見するのに毎回 12 回のモデル呼び出しを払い、モデルの調子が悪い夜には、以前は保証されていたステップを飛ばします。
2 つのチームは同じことを逆方向にやりました。制御フローが事前に既知かを問う前に、実行モデルを選んだのです。
34.2 それぞれ何のための道具か
DAG が複雑さに見合うには条件が要ります。何も実行せずに全ノードを名指しできること。辺を描いたら描いたままであること。各ノードの成功が決定的に検証できること。そして——これが忘れられがちですが——形そのものが必要であること。リプレイ、監査、段階別のコスト配賦、証明可能な並列安全性は、すべて事前に宣言したトポロジーから来ます。
最後の点は大きく過小評価されています。コンプライアンス担当が実行前に何が動くか見なければならないなら、ループでは無理でグラフなら可能です。第 13 章と第 14 章が扱うのはこの仕組みで、どれも古びていません。
ループがコストに見合うのは 1 つの状況だけです。次の行動が、実際に動いてみないと得られない観測に依存するとき。 「このファイルを読む」から出る辺は描けません。どこへ向かうかは中身次第だからです。次のステップの種類は列挙できても、順序も回数も列挙できない——それをグラフに落とせば、あり得る観測ごとに 1 分岐です。先ほどの調査パイプラインが 40 ノードに育った理由がまさにこれでした。
つまりループは、列挙をポリシーに取り替えます。観測し、判断し、実行し、終了条件が真になるまで繰り返す。分岐は設計時に著者ではなく、実行時にモデルが与えます。取引の中身はそれだけ。1 反復 1 回のモデル呼び出しで適応性を買い、実行の形を事前に知る力を手放します。
34.3 本番が収束する形
現実のシステムでどちらか一方ということはほぼありません。本番との接触を生き延びる形は、決定的な外殻が適応的な中核を包む構造です。
決定的な外殻
→ 認証しソースを識別する
→ ルートを解決し Session を読み込むか作成する
→ Tool Registry を構築し権限を解決する
→ 会話履歴をスナップショットする
┌──────────────────────────────────────────┐
│ 適応的な中核 │
│ 観測 → 判断 → 実行 → 繰り返し │
└──────────────────────────────────────────┘
→ 決定的な完了判定
→ 終了状態を永続化し応答を返す
枠の外はすべて事前に既知なので、グラフか関数か一本道であるべきです。モデル呼び出しだけは避けます。枠の中はすべて、モデルが直前に見たものに依存します。
判断基準は身も蓋もありません。if 文で正しく判定できるなら、そこにモデル呼び出しを使わない。 決定的なチェックを 1 つ枠の外へ出すたび、以後すべての実行で、永久に 1 反復ぶん払わずに済みます。
34.4 常駐 Daemon は常駐 Loop ではない
ここで多くのメンタルモデルが静かに壊れます。そして Part 10 の残り全体に効きます。
数週間動く Daemon が、Loop オブジェクトを数週間生かすわけではありません。公開 Kocoro Runtime では RunAgent がリクエスト単位の入口で、Run を呼ぶ前にリクエストごとに新しい Loop を作ります。
// internal/daemon/runner.go —— リクエストごとに 1 回構築
loop := agent.NewAgentLoop(deps.GW, reg, runCfg.ModelTier, deps.ShannonDir, ...)
...
result, usage, runErr = loop.Run(ctx, prompt, resolvedContent, history)
Loop は Turn 単位のオブジェクトで、常駐しているのは Daemon です。両者を混同すると、クラッシュ復旧・メモリ増加・状態隔離について誤った推論をします。この 3 つで間違える余裕はありません。
では Loop が毎 Turn 作り直されるなら、40 回目の Turn が 1 回目を覚えているのはなぜか。仕組みは 2 つ、どちらも意図的です。
Session は Loop 実行の前にディスクへ書かれます。実行後だけではありません。ソースの理由はそのまま引用する価値があります。永続的 Agent の土台そのものだからです。
Persist session to disk before
loop.Run()so there's a record even if the daemon crashes mid-execution. The final save after completion is still needed to capture the assistant's reply.
実行途中でクラッシュしても、手元に残るのは穴ではなく記録です。第 40 章の再開可能な Turn はこの性質の上に直接建っています。
履歴はスナップショットとして渡され、Loop の状態としては保持されません。しかもスナップショットは新しいユーザーメッセージを追加する前に取られます。そうしないとモデルは同じメッセージを 2 度——prompt として 1 度、history の中で 1 度——受け取ります。スナップショットは前回のループが注入した guardrail の指摘も取り除き、前 Turn の「同じことを繰り返しているようです」が今 Turn の会話へ漏れないようにします。
この 2 つ目は見落としやすく、間違えると高くつきます。ループが注入したシステムメッセージは、注入したそのループのものです。履歴へ永続化すれば、以後すべての Turn が 20 ターン前に当てはまらなくなった指摘を相続します。
つまり連続性とは、永続化された Session と明示的なスナップショットです。オブジェクトの寿命では決してありません。
34.5 スナップショットの根拠
| 観測 | 4ec6772 のソース位置 |
|---|---|
RunAgent がリクエスト単位の入口 | runner.go L1898 |
| リクエストごとに新しい Loop を構築 | runner.go L2732 |
履歴は Run に渡され、保持されない | runner.go L3176 |
| Session は実行後だけでなく実行前にも永続化 | runner.go L2576 |
| 委譲は Tool Call でありグラフの辺ではない | cloud_delegate.go L59 |
| 反復上限は設定可能なバックストップ | config.go L421 |
これらは特定時点の実装例であり、普遍的な契約ではありません。
34.6 グラフを残すべきとき
制御フローが固定なら、グラフか素直なコードを使います。夜間 ETL は毎晩自分を再発見する必要がありません。実行内容を事前に宣言する必要があるなら、グラフです。規制対象のパイプラインが要るのは実行後のトレースではなく実行前の形だからです。証明可能な並列安全性が要るならグラフです。2 つの分岐が同じリソースに決して触れないことを証明できますが、ループは実行時に判断するしかなく、第 44 章が呼び出しごとに並行安全性を分類しているのはまさにそのためです。
もう 1 つ。処理は安いがモデル呼び出しは安くないとき。決定的に 1 ミリ秒で終わるステップを、1 秒強と 1 セントの何分の一かかるモデル呼び出しに替えるなら、3 桁のレイテンシ上乗せを正当化しなければなりません。たいてい正当化できません。
過小評価されがちな運用上の理由もあります。「ノード 7 が失敗」は対処できるアラート、「Agent が 23 反復目で諦めた」は研究課題です。 失敗を段階に帰属させる必要があるなら、グラフは構造上ループに出せないものを与えてくれます。
34.7 よくある落とし穴
反復上限を制御フローにする。 公開の既定値は 40 で、25 から引き上げられました。「12 ファイルをリファクタ」「20 個の添付を一括処理」といった実タスクが 25 回を超えることが珍しくないからです。これは暴走のバックストップであって計画ではありません。上限に当たることによって確実に終わっているなら、終了条件はなく、終了条件の衣装を着たタイムアウトがあるだけです。
決定的な検証をループに入れる。 Schema 検証、権限解決、パス正規化。どれもモデルは不要なのに、すべて反復ごとに課金されます。
Loop オブジェクトが Turn をまたいで状態を運ぶと思う。 運びません。次のメッセージまで残る前提で Loop にキャッシュを持たせても、毎回空です。Session に持たせてください。
モデルに完了を宣言させる。 「タスクを完了しました」は主張であって事後条件ではありません。決定的な検査——ファイルが書かれたか、テストが緑か、行数が合うか——があるなら、その文ではなく検査で抜けます。
外殻をループ内で作り直す。 Tool Registry・権限集合・作業ディレクトリは Turn 単位で解決します。反復単位でやれば繰り返し支払い、同じ Turn の反復間にドリフトを招きます。
Kocoro OSS Lab(10 分)
runner.goを開き、RunAgentの入口からloop.Runまでに実行される行をすべて拾います。そのリストが決定的な外殻です。そのうちどれだけがモデルに触れていないかに注目してください。- 履歴のスナップショットがユーザーメッセージ追加の前に取られる理由を説明したコメントを探します。後に取ったら何が壊れるか書き出してください。
- 自分が持っているパイプラインを 1 本選び、各ステップに印を付けます。事前に既知か、観測依存か。観測依存が 1 つもないなら、ループは要りません。
ここだけは押さえる
- 既知の制御フローはグラフ、観測依存の制御フローはループ。 自分がどちらを持っているか問う前に選ぶのが根本の誤りです。
- ノードが追加された時期を見る。 本番に驚かされた後にしか現れないノードは、ループが必要だという信号です。
- 決定的な外殻と適応的な中核。
ifが正しく判定できることに、モデル呼び出しの資格はありません。 - Daemon は常駐、Loop は Turn 単位。 Part 10 の残りすべてがここに依存します。
- 連続性は永続化した状態であって、生かし続けたオブジェクトではない。 Session は実行前に書き、履歴はスナップショットで渡します。
Part 10 は一つのシステムとして読んでください。コンテキスト圧縮、ツール面、永続化、ステアリング、時間規律、ループ検出、並行性は相互に制約し合います。