yoi/tickets/pod-orchestration.md
2026-04-18 17:19:59 +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)
                          ├── callback addr ──→ Pod B (researcher)
                          ├── callback addr ──→ Pod C (coder)
                          │                        └── callback addr ──→ Pod D (reviewer)
                          └── callback addr ──→ Pod E (tester)

プロセス独立

  • spawn された Pod は 完全に独立したプロセス として動作する。OS レベルの親子関係subprocessは持たない
  • spawner が落ちても、spawned Pod は続行する
  • すべての Pod は同格。「誰が spawn したか」は Pod の runtime 属性であり、プロセスの従属関係ではない

Pod の発見と知識

  • Pod は自分が spawn した相手親から明示的に教えてもらった相手しか知らない
  • runtime_dir のスキャンや共有レジストリによる探索は行わない(セキュリティリスク)
  • 親抜きで会話すべき Pod があるなら、それは親が明示的に紹介する
  • Pod が知っている Pod のリスト = spawn 記録 + 紹介された相手の記録

接続モデル: 常時接続なし

  • Pod 間の接続はすべて一時的。常時接続は張らない
  • 操作(メッセージ送信・出力読み取り・停止)は都度接続の request-response:
    • ローカル: unix socket に接続 → request → response → 切断
    • リモート: SSH 経由で接続 → request → response → 切断
  • 非同期通知はコールバック方式: spawned Pod がイベント発生時に spawner のアドレスに一発接続して通知し、即切断webhook と同じモデル)
  • 接続は常に送信側が開始する

Scope の分譲と排他制御

原則

  • Pod A が Pod B を spawn するとき、A は自身の scope の一部を B に譲渡する
  • 譲渡した scope 領域は A の effective scope から deny されるA は自分が譲った部分にアクセスできなくなる)
  • write scope の排他制御が保証される:同一パスに対して write 権限を持つ Pod は常に高々1つ
  • read は衝突しない(複数 Pod が同じパスを同時に read 可能)

Scope lock file

scope の割り当てをマシン上の単一の lock file で一元管理する。spawn 系譜を持たない Pod 同士(人間が独立に起動した Pod 等)でも write 衝突を検出できる。

置き場: $XDG_RUNTIME_DIR/insomnia/scope.lock

内容:

{
  "allocations": [
    {
      "pod_id": "abc123",
      "pid": 12345,
      "socket": "/run/insomnia/.../pod-a.sock",
      "scope_allow": ["/project/src:write:recursive"],
      "delegated_from": null
    },
    {
      "pod_id": "def456",
      "pid": 12346,
      "socket": "/run/insomnia/.../pod-b.sock",
      "scope_allow": ["/project/src/core:write:recursive"],
      "delegated_from": "abc123"
    }
  ]
}

操作は flock(2) による advisory lock で排他アクセスする:

タイミング 動作
Pod 起動 lock → stale 検出PID 死活)→ 自動回収 → write 衝突チェック → 自分の scope を登録 → unlock
scope 分譲 (SpawnPod) lock → spawner の allocation に deny 追記 → 新 Pod の allocation を追加(delegated_from に spawner→ unlock
Pod 正常終了 lock → 自分の allocation を削除 → delegated_from が自分の子が残っていなければ親の deny を解除 → unlock
stale 検出 kill(pid, 0) で生存確認。死んでいたら allocation を削除し、scope を delegated_from の親に返却

stale の自動回収

Pod がクラッシュ(正常終了せず PID が消えたした場合、lock file にエントリが残る。次に lock file を開いた Pod が stale を検出し自動回収する:

  • Pod B (pid=200, dead): /project/src write, delegated_from=A
  • Pod D (pid=300, alive): /project/src/core write, delegated_from=B

→ B が死んでいる:

  1. B の scope /project/src のうち D が持つ /project/src/core を除いた部分を A に返却
  2. B のエントリを削除
  3. D の delegated_from を A に付け替え

能動的なコールバックが届かなくても、いつか誰かが lock file を開けば回収が走る。

Effective scope の導出

Pod の effective scope は lock file から導出される:

effective_scope = 自分の allocation - Σ(delegated_from が自分を指す子の allocation)

Pod は lock file を読むことで自分の effective scope を知れる。ただし常時読むとパフォーマンスに影響するため、キャッシュして scope 変動のコールバック通知で更新するのが実用的。

返却

  • Pod B が終了すると、scope は delegated_from が指す spawner (A) に自動返却される
  • 返却先は常に delegated_from。プールへの返却や他者への再分配はない

又貸し

  • Pod B が /src/core を Pod D に又貸しする場合、lock file 上で:
    • B の allocation から /src/core を除外
    • D の allocation を追加(delegated_from=B
  • B は spawner (A) にコールバックで通知:「/src/core を D に委譲した」
    • A は D の存在と D の socket address をこの通知で知る(= 親からの紹介)
  • B が終了したとき: lock file の stale 回収で D の delegated_from が A に付け替わり、D が保持しない残りの scope が A に戻る

観測可能性

  • insomnia scope list 的なコマンドで lock file を読めば、マシン上の全 Pod の scope 割り当てを一覧できる
  • 人間が「今どの Pod がどこを write 占有しているか」を確認する手段になる
  • 衝突で Pod 起動が拒否されたとき、競合相手の pod_id を lock file から取得してエラーメッセージに含める

LLM に公開するツール群

Pod の LLM が使えるツールとして以下を設計する。すべて都度接続の request-response で動作する。

SpawnPod

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

入力:

  • name: spawned Pod の識別名
  • instruction: instruction ファイル参照(省略時は $insomnia/default
  • task: 最初のメッセージspawn 後に即座に run される)
  • scope: 譲渡する scope 定義。spawner の effective scope のサブセットでなければならない
  • address: (自動注入) spawner の callback address

出力:

  • spawned Pod の pod_id と接続先 address

内部動作:

  • scope lock file を flock → write 衝突チェック → spawner の allocation に deny 追記 + 新 Pod の allocation を登録 → unlock
  • PodFactory のカスケードに spawner からの overlay を重ねて PodManifest を構築
  • 独立プロセスとして Pod を起動
  • spawner の callback address を spawned Pod に渡す
  • taskMethod::Run で送信

SendToPod

既知の Pod にメッセージを送る(都度接続)。

入力:

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

出力:

  • 送信確認

ReadPodOutput

既知の Pod の最新の出力を読む(都度接続)。

入力:

  • pod_id: 対象の Pod
  • since: 前回読んだ時点からの差分のみ取得するオプション

出力:

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

StopPod

既知の Pod を終了させ、譲渡した scope を回収する(都度接続)。

入力:

  • pod_id: 対象の Pod

出力:

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

ListPods

自分が知っている Pod の一覧と状態を返す。

入力: なし

出力:

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

内部動作:

  • spawn 記録を元にリストを構築
  • 各 Pod に都度接続して health check接続できなければ stopped 扱い)
  • Hook で定期的に自動実行することも可能

非同期通知: コールバック方式

仕組み

  • spawn 時に spawner が自分の callback address を spawned Pod に渡す
    • ローカル: unix socket path
    • リモート: insomnia@host:pod-name 形式の SSH address
  • spawned Pod がイベント発生時に、spawner の callback address に一発接続して通知を送り、即切断する
  • spawner が落ちていたら callback が失敗するだけspawned Pod は続行する)

通知の種類

  • ターン完了: spawned Pod の1ターンが終わったspawner は ReadPodOutput で内容を取りに行く)
  • scope 又貸し: spawned Pod が自身の scope の一部をさらに別の Pod に委譲したspawner の scope 記録を更新する材料)
  • エラー: spawned Pod でエラーが発生
  • 終了: spawned Pod が停止したscope 返却のトリガー)

通知は「シグナル」のみ

通知にはイベントの種類と最小限のメタデータpod_id、scope 変動等のみを含める。応答テキスト全体のような大きなデータは含まない。spawner が内容を知りたければ ReadPodOutput で取りに行く。

通知の LLM への伝達

  • spawner が受け取ったコールバック通知はバッファに溜められる
  • spawner の次のターン開始前に、Hook が溜まった通知を集約してターンの先頭に system message として注入する
  • 通知が無い場合は何もしない

ポーリングでも代替可能

コールバックが届かなくても、spawner は ListPods のポーリングで状態を拾える。コールバックは「早く気付くための最適化」であって唯一の手段ではない。

Pod の設定

instruction

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

provider

  • API キーと provider 設定は PodFactory のカスケードuser / project manifestから取得
  • spawned Pod に異なるモデルを使わせたい場合は overlay で上書き

Pod 起動コマンド

  • 環境変数またはユーザー設定で上書き可能
  • デフォルトは podPATH 上の pod バイナリ)
  • リモートの場合は SSH 越しに実行

人間向けの監視

各 Pod に個別接続

  • すべての Pod は通常の socket サーバーを持つ。人間は GUI / TUI で Pod に接続して会話を閲覧・介入できる
  • ただし、Pod の存在を知る手段はspawn 記録(+ 紹介)のみ。人間もオーケストレーター Pod 経由か、事前に Pod の address を知っている必要がある

Pod ネットワークの可視化

  • GUI / TUI がオーケストレーター Pod の spawn 記録を読んで Pod リストを表示
  • 各エントリに status、scope 要約、最終応答の要約を表示
  • エントリを選択すると、その Pod に接続して会話ビューに切り替わる

介入

  • 人間は既知の Pod に直接メッセージを送れる(通常のクライアントとして都度接続)
  • 人間が Pod を直接 stop できるscope は spawner に返却される)

設計で決めること

  • callback の protocol: 通知メッセージの具体的なフォーマット。既存の protocol crate (Event enum) を拡張するか、別の軽量フォーマットにするか
  • scope の分譲粒度: パス単位で分譲するか、permission レベルwrite だけ渡して read は共有等)でも制御できるか
  • 通知のバッファリング: spawner のターン実行中に溜まるコールバック通知をどこに保持するか
  • リソース制限: 最大 spawned Pod 数、ネスト深さの上限
  • ツール名: SpawnPod / SendToPod / ReadPodOutput / StopPod / ListPods の命名
  • Pod ネットワークの可視化: GUI / TUI どちらに先行実装するか
  • spawner 復帰時の再接続: spawn 記録を読んで spawned Pod に再接続する手順。callback address の再登録
  • リモート spawn: SSH-only モデルの詳細(docs/network-peering.md を参照)

完了条件

  • Pod の LLM が SpawnPod ツールで別 Pod を起動でき、spawned Pod が独立プロセスとして動作する
  • spawn 時に scope lock file に allocation が記録され、spawner の effective scope が<><E3818C><EFBFBD>小される
  • 同一パスへの write 衝突が lock file で検出され、Pod 起動が拒否される(競合相手の pod_id がエラーに含まれる)
  • stale エントリPID が死亡が自動回収され、scope が delegated_from の親に戻る
  • SendToPod / ReadPodOutput が都度接続の request-response で動作する
  • StopPod で spawned Pod を graceful に停止でき、lock file から allocation が削除されて scope が返却される
  • ListPods で既知の Pod の状態を一覧でき、health check が機能する
  • spawned Pod のターン完了・エラー・停止がコールバックで spawner に通知され、Method::Notify 経由で LLM に伝わる
  • spawner が停止しても spawned Pod が続行する
  • 又貸しが lock file に記録され、コールバック通知により spawner が孫 Pod の存在と scope を把握できる
  • insomnia scope list 相当のコマンドで lock file を読み、マシン上の全 Pod の scope 割り当てを一覧できる
  • 人間が GUI または TUI で Pod リストを閲覧でき、任意の既知 Pod に接続して会話を見られる

他チケットとの関係

  • tickets/method-notify.md: コールバック通知を親 Pod に注入する仕組み。オーケストレーションの非同期通知に必須
  • tickets/protocol-design.md: コールバック通知の protocol 拡張が必要になりうる
  • tickets/native-gui-mvp.md: Pod リスト可視化は GUI 側の拡張
  • tickets/tui-pod-spawn-ui.md: TUI からの Pod spawn UI。spawn のインフラは共通
  • tickets/permission-extension-point.md: spawned Pod のツール実行パーミッションを spawner が制御<E588B6><E5BEA1>る可能性
  • scope-exclusion (削除済み): scope lock file による write 排他で吸収された

範囲外

  • Pod 間の直接メッセージパッシング: すべてのやり取りは spawner を介するか、spawner が明示的に紹介した相手とのみ行う
  • 常時接続: Pod 間の接続はすべて一時的(都度接続 or コールバック一発)
  • runtime_dir のスキャンによる Pod 探索: セキュリティリスクのため行わない。Pod の存在は spawn 記録 + 明示的な紹介 + lock file のみで把握
  • 自動スケーリング / 負荷分散 / リモート実行: ローカルの単一マシン上での動作に集中。リモート spawn は docs/network-peering.md を参照
  • 課金・トークン予算管理
  • 環境再現git clone, コンテナ構築等): insomnia の責務外