yoi/tickets/pod-orchestration.md
2026-04-16 13:55:17 +09:00

16 KiB
Raw Blame History

Pod オーケストレーション: LLM によるマルチエージェント分業

背景

Insomnia の差別化の中核は「複数の Pod が独立プロセスとして並行動作し、LLM が自律的に分業・統括できる」点にある。シングルエージェントの LLM リグが1つのコンテキストで全タスクを処理するのに対し、Insomnia はタスクを別 Pod に委譲し、結果を集約してフィードバックするオーケストレーションを LLM 自身に行わせることで、コンテキスト長・専門性・並列性の壁を突破する。

現状の Pod は単独で動くことしかできず、Pod 間の連携は外部の人間が仲介するしかない。LLM が「この調査は別の Pod に任せて、結果が返ってきたら本筋に統合する」という判断をツールとして実行できる仕組みが無い。

ゴール

Pod の LLM が、ツール呼び出しを通じて別 Pod のライフサイクル全体生成・指示・結果読み取り・終了を制御でき、spawned Pod の進捗を非同期に受け取れる基盤を設計・実装する。あわせて、人間がこの Pod ネットワークを監視・介入できる観測経路を用意する。

コンセプト: 独立したピアとしての Pod

Human (GUI / TUI) ─── Pod A (orchestrator)
                          ├── socket ──→ Pod B (researcher)
                          ├── socket ──→ Pod C (coder)
                          │                  └── socket ──→ Pod D (reviewer)
                          └── socket ──→ Pod E (tester)

プロセス独立

  • spawn された Pod は 完全に独立したプロセス として動作する。OS レベルの親子関係subprocessは持たない
  • spawner が落ちても、spawned Pod は続行する。再接続も可能
  • spawner と spawned Pod の関係は「socket client として接続している」だけ。関係性はプロセスではなくソケット接続で表現される
  • すべての Pod は同格。「誰が spawn したか」は Pod の runtime 属性であり、プロセスの従属関係ではない
  • Pod の発見にはレジストリruntime_dir 内の socket パスを列挙、または明示的な登録)が必要

Scope の分譲

  • Pod A が Pod B を spawn するとき、A は自身の scope の一部を B に譲渡する
  • 譲渡した scope 領域は A の effective scope から deny されるA は自分が譲った部分にアクセスできなくなる)
  • これにより構造的に排他制御が保証される:同一パスに対して write 権限を持つ Pod は常に高々1つ
  • Pod B が終了すると、譲渡された scope は A に返却されるA の deny が解除される)
  • Pod B が scope の一部をさらに Pod D に分譲した場合、B の終了時に D が保持している分は D にそのまま残るD は独立して生きているため。D が終了するまで、その scope 領域は誰にも返却されない

tickets/scope-exclusion.md との関係

scope 分譲により Pod 間の write 排他制御は spawn 時に構造的に保証される。tickets/scope-exclusion.md が扱おうとしていた「複数 Pod が同一パスに同時 write する問題」は、本チケットの scope 分譲モデルで吸収される

ただし、分譲ではなく共有 read (複数 Pod が同じパスを同時に read する) は引き続き許可される。read 同士は衝突しない。

LLM に公開するツール群

Pod の LLM が使えるツールとして以下を設計する。いずれも通常の Tool trait 実装で、Worker に登録される。

SpawnPod

新しい Pod を起動し、scope の一部を譲渡する。

入力:

  • name: spawned Pod の識別名spawner のスコープ内で一意)
  • instruction: spawned Pod の instruction ファイル参照(省略時は $insomnia/default
  • task: spawned Pod への最初のメッセージspawn 後に即座に run される)
  • scope: 譲渡する scope 定義allow / deny ルール)。spawner の現在の effective scope のサブセットでなければならない。バリデーションで超過分は拒否

出力:

  • spawned Pod の識別子(pod_id)と接続状態

内部動作:

  • spawner の effective scope から、譲渡する scope 領域を deny に追加spawner 側の scope を縮小)
  • PodFactory のカスケードuser manifest + project manifestに加え、spawner からの overlay譲渡 scope + instruction + provider 等)を重ねて spawned Pod の PodManifest を構築
  • 独立プロセスとして Pod を起動、socket 確立
  • taskMethod::Run で送信

SendToPod

spawned Pod にメッセージを送る。

入力:

  • pod_id: 対象の Pod
  • message: 送信するテキスト

出力:

  • 送信確認

ReadPodOutput

spawned Pod の最新の出力を読む。

入力:

  • pod_id: 対象の Pod
  • since: 前回読んだ時点からの差分のみ取得するオプション(省略時は全出力)

出力:

  • 対象 Pod の assistant 応答テキスト(最新ターン or 差分)
  • 現在の状態(running / idle / stopped

StopPod

spawned Pod を終了させ、譲渡した scope を回収する。

入力:

  • pod_id: 対象の Pod

出力:

  • 終了確認
  • 回収された scope の要約

内部動作:

  • 対象 Pod に graceful shutdown を要求
  • 対象 Pod が保持していた scope のうち、さらに下流に分譲されていない分を spawner に返却
  • spawner の deny リストから返却分を解除

ListPods

spawner が spawn した Pod の一覧とそれぞれの状態を返す。

入力: なし

出力:

  • spawned Pod の pod_idnamestatusrunning / idle / stopped、譲渡中の scope 要約、最終応答の要約

非同期通知

spawned Pod が出力を生成したとき、spawner の LLM にそれを伝える仕組みが必要。ただし LLM は「ターン」の単位で動くため、イベントストリームをリアルタイムに割り込ませるのは不自然。

方針: ターン間フックで集約通知

  • spawner のターンが終了した後、次のターンの開始前にフックが走り、spawned Pod から届いているイベントを集約する
  • 集約された通知は次のターンの先頭に system message として注入される(例: [pod "researcher"] completed: found 3 relevant files
  • 通知が無い場合は何もしない

通知の粒度

  • ターン完了通知: spawned Pod の1ターンが終わったとき
  • エラー通知: spawned Pod でエラーが発生したとき
  • 終了通知: spawned Pod が停止したときscope 返却が発生する)

ツール出力の内容自体は ReadPodOutput で明示的に取りに行くモデル。通知は「何か変化があった」というシグナルのみで、内容全体は含まないspawner のコンテキストを圧迫しない)。

Scope の分譲と回収の詳細

分譲時

  1. spawner が SpawnPodscope を指定
  2. システムが検証: 指定された scope が spawner の 現在の effective scope のサブセットであること
  3. 検証通過後、spawner の effective scope から譲渡分を deny として差し引く
  4. spawned Pod は譲渡された scope を自身の allow として持って起動

回収時StopPod または spawned Pod の自発的終了)

  1. 終了する Pod が保持している effective scope を確認
  2. そのうち、さらに下流に分譲中の scope は除外(下流 Pod が生きている間は返却されない)
  3. 残りの scope を spawner の deny から解除し、spawner の effective scope に復帰

分譲チェーンのケース

Pod A が Pod B に /src を分譲 → Pod B が Pod D に /src/core を分譲:

  • A の scope: /src は deny
  • B の scope: /src を持つが /src/core は denyD に分譲済み)
  • D の scope: /src/core

B が終了すると:

  • /src のうち D に分譲中の /src/core は返却されないD が生きている)
  • /src から /src/core を除いた部分が A に返却される
  • A の scope: /src/src/core 以外が復帰。/src/core は引き続き denyD が保持中)
  • D が終了すると /src/core が A に返却される

runtime scope state

  • Pod の scope は manifest の static 定義だけでは決まらない。分譲と回収による動的な state が加わる
  • この state を管理するレイヤーが必要Pod 内のメモリ状態 + 永続化の選択肢)
  • spawner がクラッシュして復帰した場合、分譲中の scope を復元する必要がある → scope ledger の永続化が事実上必須

Pod の設定

instruction

  • spawned Pod の instructionSpawnPod の引数で明示指定
  • 省略時は $insomnia/default
  • spawner の instruction を引き継ぐケースはまれ(分業目的の spawn なので、spawned Pod には固有の役割がある)

provider

  • API キーと provider 設定は通常ユーザー manifest / プロジェクト manifest 由来なので、spawned Pod は PodFactory 経由で同じカスケードから取得する
  • spawned Pod に異なるモデルを使わせたい場合は overlay で上書きする(例: 調査用 Pod には小さいモデル、コード生成にはフルモデル)

人間向けの監視

各 Pod は独立して観測可能

  • すべての Pod は通常の socket サーバーを持つ。人間は GUI / TUI で任意の Pod に接続して会話を閲覧・介入できる
  • Pod 間に主従関係が無いので、人間はどの Pod にも同格にアクセスできる

Pod ネットワークの可視化

  • GUI / TUI が Pod の一覧を表示spawn 関係をグラフまたはリストで表現)
  • 各ノードに statusrunning / idle / stopped、保持中の scope 要約、最終ターンの要約を表示
  • ノードをクリック/選択すると、その Pod の会話ビューに切り替わる

介入

  • 人間は任意の Pod に直接メッセージを送れる(通常のクライアントとして接続)
  • 人間が Pod を直接 stop できるscope は spawner に返却される)
  • spawner の LLM は spawned Pod に人間が介入したことを(次の通知で)知る

Pod の発見と登録

プロセスが独立しているため、起動中の Pod を発見する仕組みが要る。

候補:

  • A. runtime_dir convention: 各 Pod が socket path を runtime_dir/<pod_name>.sock に作る。ディレクトリを列挙すれば一覧が取れる(現行の仕組みの延長)
  • B. 明示的なレジストリ: Pod 起動時に共有レジストリ(ファイルまたは IPCに登録。終了時に削除
  • C. daemon: 軽量な daemon プロセスがレジストリ役を担う(crates/daemon は現在空)

現時点では A が最もシンプル。daemon が必要になるのは「リモート Pod」や「Pod の自動再起動」が必要になってから。

設計で決めること

  • Pod の起動方式: 独立プロセスとして pod バイナリを起動する形で確定。起動方法の詳細fork? 直接 exec? nix-shell 経由?
  • scope ledger の永続化方式: ファイルベース / session-store / 別の永続ストア
  • scope の分譲粒度: パス単位で分譲するか、permission レベルread / writeでも分譲できるか例: write だけ譲って read は共有する等)
  • 通知の注入方式: system message として注入するか、専用の Event としてターン開始前に Worker に流すか
  • 通知のバッファリング: spawner の1ターンが数分かかる場合、その間のイベントをどこに溜めるか
  • Pod の発見: A (runtime_dir) / B (レジストリファイル) / C (daemon)
  • リソース制限: 最大 spawned Pod 数、ネスト深さの上限
  • ツール名: SpawnPod / SendToPod / ReadPodOutput / StopPod / ListPods の命名はそのままで良いか
  • Pod ネットワークの可視化: GUI / TUI どちらに先行実装するか
  • spawner 復帰時の再接続: scope ledger を読んで spawned Pod のソケットに再接続する手順
  • 分譲チェーンの depth limit: 再帰的 spawn の深さ上限を設けるか

完了条件

  • Pod の LLM が SpawnPod ツールで別 Pod を起動でき、spawned Pod が独立プロセスとして動作する
  • spawn 時に scope が分譲され、spawner の effective scope が縮小されることを検証できる
  • SendToPod で spawned Pod にメッセージを送り、ReadPodOutput で応答を読み取れる
  • StopPod で spawned Pod を graceful に停止でき、scope が spawner に返却される
  • ListPods で spawned Pod の状態と scope 要約を一覧できる
  • spawned Pod のターン完了・エラー・停止が spawner に非同期通知され、次のターン開始時に LLM に伝わる
  • spawner が停止しても spawned Pod が続行し、spawner 復帰時に再接続できる
  • 人間が GUI または TUI で Pod ネットワークを閲覧でき、任意の Pod に接続して会話を見られる
  • spawned Pod がさらに別の Pod を spawn する再帰的なネットワークが動作する
  • scope 分譲チェーンの返却(中間ノードの終了時に部分返却)が正しく動作する

他チケットとの関係

  • tickets/scope-exclusion.md: scope 分譲モデルにより本チケットに吸収される。write 排他制御は分譲時に構造的に保証されるため、別の排他制御メカニズムは不要。scope-exclusion.md は本チケットの scope 設計がカバーする
  • tickets/protocol-design.md: Protocol に Pod 間通知のイベントspawned Pod の状態変化通知など)を追加する必要があるかもしれない。ただし各 Pod は通常の socket サーバーなので、protocol 拡張なしで「複数 socket に繋ぐクライアント」として実装できる可能性もある
  • tickets/native-gui-mvp.md: Pod ネットワーク可視化は GUI 側の拡張。MVP scope には含まれていないが、本チケットの監視要件は GUI の次フェーズに自然に接続する
  • tickets/tui-pod-spawn-ui.md: TUI からの Pod spawn UI。本チケットは LLM からの spawn だが、spawn のインフラPodFactory + プロセス起動)は共通
  • tickets/tui-pod-shutdown.md: Pod ネットワークの shutdown 戦略にも影響。Pod を停止したとき scope がどこに返るか
  • tickets/permission-extension-point.md: spawned Pod のツール実行パーミッションを spawner が制御する可能性

範囲外

  • Pod 間の直接メッセージパッシング: Pod 同士が spawner を介さずに直接通信する仕組み。すべてのやり取りは spawnerオーケストレーターを介す
  • 共有メモリ / 共有状態: Pod 間のデータ共有はツール経由のテキストメッセージのみ。ファイルシステムを介した暗黙の共有は scope が許す範囲で起きうるが、それは Pod の既存動作の延長であり、分譲により write 衝突は構造的に排除される
  • 自動スケーリング / 負荷分散: Pod の spawn 先は常にローカルマシン。リモート実行やクラウド分散は扱わない
  • 課金・トークン予算管理: spawned Pod が消費するトークンの予算制御や spawner への請求集約は扱わない
  • 人間がネットワークの構造を変更する操作Pod の spawn 元替え、ネットワークの組み替えなど)
  • daemon の導入: Pod の発見は runtime_dir convention または軽量レジストリで対応。daemon は必要が明確になってから