yoi/tickets/pod-orchestration.md
2026-04-18 17:19:59 +09:00

319 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
内容:
```json
{
"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 に渡す
- `task``Method::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_id`、`name`、`status`running / 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 の `instruction``SpawnPod` の引数で明示指定
- 省略時は `$insomnia/default`
- spawner の instruction を引き継ぐケースはまれ(分業目的の spawn なので固有の役割がある)
### provider
- API キーと provider 設定は PodFactory のカスケードuser / project manifestから取得
- spawned Pod に異なるモデルを使わせたい場合は overlay で上書き
### Pod 起動コマンド
- 環境変数またはユーザー設定で上書き可能
- デフォルトは `pod`PATH 上の `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 の責務外