From b685fedf1ac39f06f273efa5a6a9dcaa2a71f024 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 18 Apr 2026 18:48:26 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=81=E3=82=B1=E3=83=83=E3=83=88=E5=88=86?= =?UTF-8?q?=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 6 +- docs/pod-protocol.md | 19 --- tickets/pod-callback.md | 80 +++++++++ tickets/pod-comm-tools.md | 94 +++++++++++ tickets/pod-orchestration.md | 318 ----------------------------------- tickets/scope-lock.md | 89 ++++++++++ tickets/spawn-pod-tool.md | 68 ++++++++ 7 files changed, 336 insertions(+), 338 deletions(-) delete mode 100644 docs/pod-protocol.md create mode 100644 tickets/pod-callback.md create mode 100644 tickets/pod-comm-tools.md delete mode 100644 tickets/pod-orchestration.md create mode 100644 tickets/scope-lock.md create mode 100644 tickets/spawn-pod-tool.md diff --git a/TODO.md b/TODO.md index b638119d..fee70989 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,11 @@ - [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) -- [ ] Pod オーケストレーション: LLM によるマルチエージェント分業 → [tickets/pod-orchestration.md](tickets/pod-orchestration.md) +- [ ] Pod オーケストレーション + - [ ] Scope lock file: write 排他とスコープ分譲の記録基盤 → [tickets/scope-lock.md](tickets/scope-lock.md) + - [ ] SpawnPod ツール: LLM から Pod を生成 → [tickets/spawn-pod-tool.md](tickets/spawn-pod-tool.md) + - [ ] Pod 間通信ツール: SendToPod / ReadPodOutput / StopPod / ListPods → [tickets/pod-comm-tools.md](tickets/pod-comm-tools.md) + - [ ] Pod 間コールバック通知 → [tickets/pod-callback.md](tickets/pod-callback.md) - [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md) - [ ] TUI 拡充 - [ ] 新しい Pod を spawn する UI の設計 → [tickets/tui-pod-spawn-ui.md](tickets/tui-pod-spawn-ui.md) diff --git a/docs/pod-protocol.md b/docs/pod-protocol.md deleted file mode 100644 index bb2a25f4..00000000 --- a/docs/pod-protocol.md +++ /dev/null @@ -1,19 +0,0 @@ -# Pod Protocol - -Pod の制御・監視に使う JSONL ベースのメッセージプロトコル。トランスポートに依存しない。 - -``` -CLI → Pod Protocol (直接呼び出し) -Native App → Pod Protocol (直接呼び出し) -Web → 中央バックエンド → daemon (Unix socket) → Pod Protocol -``` - -## 設計判断 - -- **リクエストとレスポンスの紐付けはしない**: Pod は1つであり、Pod の状態遷移(イベント)を見れば何が起きているか分かる -- **イベントは全リスナーに broadcast**: 読み取り専用の監視も、操作側も同じストリームを受け取る -- **操作の競合は先勝ち**: run 中に別の run が来たらエラーイベントを返す - -## daemon - -daemon は Pod Protocol を Unix domain socket 上で中継する薄い層。接続=リスナー登録、切断=リスナー解除。それだけ。 diff --git a/tickets/pod-callback.md b/tickets/pod-callback.md new file mode 100644 index 00000000..a5d3726e --- /dev/null +++ b/tickets/pod-callback.md @@ -0,0 +1,80 @@ +# Pod 間コールバック通知 + +## 背景 + +spawned Pod がイベント(ターン完了、エラー、終了、scope 又貸し)を発生させたとき、spawner に自動通知する仕組みが必要。`Method::Notify`(実装済み)が受け口となり、spawned Pod がそこにコールバックを送る。 + +## 依存 + +- `tickets/spawn-pod-tool.md`: spawn 記録と callback address の受け渡し +- `Method::Notify`: 実装済み。コールバック受信 → LLM への system message 注入 + +## 仕組み + +### callback address + +spawn 時に spawner が自身の callback address を spawned Pod に渡す: +- ローカル: spawner の unix socket path +- リモート: `insomnia@host:pod-name` 形式(将来。`docs/network-peering.md` 参照) + +spawned Pod はこの address を保持し、イベント発生時に一発接続して通知を送り、即切断する(webhook モデル)。 + +### 通知の種類 + +| 通知 | タイミング | 含まれるデータ | +|---|---|---| +| ターン完了 | spawned Pod の1ターンが終了 | name | +| エラー | spawned Pod でエラー発生 | name, error_message | +| 終了 | spawned Pod が停止 | name(scope 返却のトリガー) | +| scope 又貸し | spawned Pod が自身の scope を別 Pod に委譲 | name, sub_name, sub_pod_socket, delegated_scope | + +### 通知は「シグナル」のみ + +通知にはイベント種類と最小限のメタデータだけを含める。応答テキスト全体は含まない。spawner が内容を知りたければ `ReadPodOutput` で取りに行く。 + +### spawner 側の処理 + +1. callback を受信 +2. `pod.push_notification(source, formatted_message)` でバッファに追加 +3. `PodInterceptor::pre_llm_request` が次の LLM リクエスト時に context に注入 +4. IDLE なら `Method::Notify` の IDLE パスで自動ターン起動 + +### scope 又貸し通知の特殊性 + +spawned Pod (B) が孫 Pod (D) に scope を又貸しした場合: +- B は spawner (A) に通知: 「`/src/core` を D に委譲した。D の socket は ...」 +- A は D の存在と address をこの通知で知る(= 親からの紹介) +- A の spawn 記録に D が追加される +- B が死亡しても A は D を直接把握しており、scope lock file の stale 回収で `delegated_from` が A に付け替わる + +### コールバック送信の実装箇所 + +spawned Pod 側に callback 送信の仕組みが必要: +- **ターン完了**: Controller の `RunEnd` イベント発行時に callback を fire +- **エラー**: Controller の `Error` イベント発行時に callback を fire +- **終了**: Controller の shutdown シーケンス内で callback を fire +- **scope 又貸し**: `SpawnPod` ツールの実行後に callback を fire + +### ポーリングでの代替 + +コールバックが失敗しても、spawner は `ListPods` のポーリングで状態を拾える。コールバックは最適化であり、唯一の手段ではない。 + +## 設計で決めること + +- **callback の message format**: JSON? protocol crate の型を再利用? 独自の軽量フォーマット? +- **callback 送信の非同期性**: ターン完了時に callback 送信を await するか、fire-and-forget で spawn するか +- **callback 失敗時のリトライ**: リトライするか、ログだけ出して諦めるか +- **callback address の更新**: spawner が再起動して socket path が変わった場合の再登録手順 + +## 完了条件 + +- spawned Pod のターン完了・エラー・終了時に spawner の callback address に通知が送信される +- scope 又貸し時に spawner に通知が送信され、spawner が孫 Pod の存在を把握する +- callback 受信が `Method::Notify` パイプラインに乗り、spawner の LLM に system message として伝わる +- callback 送信が失敗しても spawned Pod は続行する +- 単体テストで各通知種別の送受信が検証される + +## 範囲外 + +- リモートの callback(SSH 越し)。ローカル unix socket のみ +- callback の配信保証(at-least-once / exactly-once)。fire-and-forget で十分 diff --git a/tickets/pod-comm-tools.md b/tickets/pod-comm-tools.md new file mode 100644 index 00000000..021ff6c3 --- /dev/null +++ b/tickets/pod-comm-tools.md @@ -0,0 +1,94 @@ +# Pod 間通信ツール: SendToPod / ReadPodOutput / StopPod / ListPods + +## 背景 + +`SpawnPod` で Pod を生成した後、spawner の LLM が spawned Pod に指示を送り、結果を読み、完了したら停止させる手段が必要。 + +## 依存 + +- `tickets/spawn-pod-tool.md`: SpawnPod による Pod 生成と spawn 記録 + +## ツール群 + +すべて**都度接続の request-response** で動作する。spawned Pod の socket に接続し、操作を行い、切断する。 + +### `SendToPod` + +既知の Pod にメッセージを送る。 + +入力: +- `name`: 対象の Pod +- `message`: 送信するテキスト + +出力: +- 送信確認 + +内部動作: +- spawn 記録から `name` の socket path を引く +- socket に接続 → `Method::Run { input: message }` を送信 → ack 受信 → 切断 + +### `ReadPodOutput` + +既知の Pod の出力を読む。前回読んだ位置を自動追跡(カーソルベース)。 + +入力: +- `name`: 対象の Pod + +出力: +- 前回読んだ位置以降の assistant テキスト出力 +- 現在の状態(`running` / `idle` / `stopped`) + +内部動作: +- spawn 記録から socket path を引く +- socket に接続 → `Method::GetHistory` で履歴取得 → 前回カーソル以降の assistant text を抽出 → カーソル更新 → 切断 +- 接続できなければ `stopped` として返す + +### `StopPod` + +既知の Pod を終了させ、譲渡した scope を回収する。 + +入力: +- `name`: 対象の Pod + +出力: +- 終了確認 +- 回収された scope の要約 + +内部動作: +- socket に接続 → `Method::Shutdown` 送信 → 終了確認受信 → 切断 +- scope lock file を flock → 対象の allocation 削除 → spawner の deny を解除 → unlock +- spawn 記録から対象を削除 + +### `ListPods` + +自分が知っている Pod の一覧と状態を返す。 + +入力: なし + +出力: +- 各 Pod の `name`、`name`、`status`、譲渡中の scope 要約、最終応答の要約 + +内部動作: +- spawn 記録を元にリストを構築 +- 各 Pod に都度接続して health check(`Method::GetHistory` でステータス取得、接続できなければ `stopped`) +- stopped な Pod は scope lock file の stale 回収をトリガー + +## 設計で決めること + +- **ReadPodOutput のカーソル管理**: spawn 記録内に `last_read_hash` を持つか、別の場所で管理するか +- **StopPod と scope 回収の順序**: 先に Shutdown してから lock file を更新するか、lock file を先に更新するか(Shutdown が失敗した場合の整合性) +- **ListPods の health check コスト**: 大量の Pod がいるとき全部に接続するのは重い。キャッシュ戦略 + +## 完了条件 + +- `SendToPod` で spawned Pod にメッセージを送り、Pod がそれを処理する +- `ReadPodOutput` で spawned Pod の最新出力を読め、カーソルベースの差分取得が動作する +- `StopPod` で spawned Pod を graceful に停止でき、scope が spawner に返却される +- `ListPods` で既知の Pod の状態を一覧でき、health check が機能する +- 接続できない Pod は `stopped` として扱われ、scope の stale 回収がトリガーされる +- 単体テストで各ツールの正常系・異常系が検証される + +## 範囲外 + +- コールバック通知は `tickets/pod-callback.md` +- Pod ネットワークの GUI / TUI 可視化 diff --git a/tickets/pod-orchestration.md b/tickets/pod-orchestration.md deleted file mode 100644 index 99428583..00000000 --- a/tickets/pod-orchestration.md +++ /dev/null @@ -1,318 +0,0 @@ -# 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 が���小される -- 同一パスへの 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 が制御��る可能性 -- **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 の責務外 diff --git a/tickets/scope-lock.md b/tickets/scope-lock.md new file mode 100644 index 00000000..85aac726 --- /dev/null +++ b/tickets/scope-lock.md @@ -0,0 +1,89 @@ +# Scope lock file: write 排他とスコープ分譲の記録基盤 + +## 背景 + +Pod オーケストレーションでは scope の分譲(spawner が自身の scope を spawned Pod に譲渡)が発生する。また、人間が独立に複数の Pod を起動した場合にも同一パスへの write 衝突を検出する必要がある。 + +これらを解決するため、マシン上の全 Pod の scope 割り当てを**単一の lock file**で一元管理する。 + +## 仕様 + +### lock file + +置き場: `$XDG_RUNTIME_DIR/insomnia/scope.lock` + +内容: +```json +{ + "allocations": [ + { + "name": "abc123", + "pid": 12345, + "socket": "/run/insomnia/.../pod-a.sock", + "scope_allow": ["/project/src:write:recursive"], + "delegated_from": null + }, + { + "name": "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 分譲** | 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 がクラッシュした場合、lock file にエントリが残る。次に lock file を開いた Pod が stale を検出し自動回収する: + +- 死亡 Pod の scope のうち、生存中の子 Pod が持つ分を除外 +- 残りを `delegated_from` の親に返却 +- 死亡 Pod のエントリを削除 +- 子 Pod の `delegated_from` を親に付け替え + +### effective scope の導出 + +``` +effective_scope = 自分の allocation - Σ(delegated_from が自分を指す子の allocation) +``` + +### セキュリティとアクセス + +- ファイルパーミッション `0600`(owner only)、ディレクトリは `0700`。他ユーザーからの読み取りを防ぐ +- owner(Pod を動かしているユーザー)は当然読める。JSON なので直接確認も可能 +- Pod による lock file 探索は排他制御の目的に限定する。Pod 発見のための lock file スキャンは行わない(Pod の発見は spawn 記録 + 明示的な紹介のみ) +- 衝突で Pod 起動が拒否されたとき、競合相手の name をエラーメッセージに含める + +## 実装 + +- 新規モジュール `crates/pod/src/scope_lock.rs`(または `crates/scope-lock/`) +- Pod 起動時(`Pod::from_manifest` / `Pod::from_manifest_toml`)に lock 取得 +- Pod 終了時(`Drop` または明示的 release)に lock 解放 +- Controller 層でのエラー伝搬 + +## 完了条件 + +- Pod 起動時に scope lock file に allocation が記録される +- 同一パスへの write 衝突が検出され、Pod 起動が拒否される(競合相手の name がエラーに含まれる) +- Pod 正常終了時に allocation が削除される +- stale エントリ(PID 死亡)が自動回収され、scope が親に戻る +- 分譲チェーン(A→B→D)の部分回収が正しく動作する +- 単体テストで衝突検出・stale 回収・分譲/返却が検証される + +## 範囲外 + +- SpawnPod ツール自体の実装(`tickets/spawn-pod-tool.md`) +- scope の分譲粒度(permission レベルでの分譲等)は当面パス単位のみ diff --git a/tickets/spawn-pod-tool.md b/tickets/spawn-pod-tool.md new file mode 100644 index 00000000..643e5e35 --- /dev/null +++ b/tickets/spawn-pod-tool.md @@ -0,0 +1,68 @@ +# SpawnPod ツール: LLM から Pod を生成する + +## 背景 + +オーケストレーションの起点。LLM が「このタスクを別 Pod に任せたい」と判断したとき、`SpawnPod` ツールを呼び出して新しい Pod プロセスを起動する。 + +## 依存 + +- `tickets/scope-lock.md`: scope 分譲の記録基盤 + +## 仕様 + +### ツール定義 + +`SpawnPod` は通常の `Tool` trait 実装として Worker に登録される。 + +入力: +- `name`: spawned Pod の識別名 +- `instruction`: instruction ファイル参照(省略時は `$insomnia/default`) +- `task`: 最初のメッセージ(spawn 後に即座に run される) +- `scope`: 譲渡する scope 定義(allow ルール)。spawner の effective scope のサブセットでなければならない + +出力: +- spawned Pod の `name` と接続先 address(socket path) + +### 内部動作 + +1. scope lock file を flock → spawner の effective scope を確認 → 要求された scope がサブセットか検証 +2. spawner の allocation に deny を追記 + 新 Pod の allocation を登録(`delegated_from` = spawner)→ unlock +3. PodFactory のカスケード(user / project manifest)に spawner からの overlay(name, pwd, scope, instruction)を重ねて PodManifest を構築 +4. `pod` バイナリを独立プロセスとして起動(`Command::new(pod_command)`) +5. spawned Pod の socket が利用可能になるまで待機 +6. spawner の callback address を spawned Pod に渡す(`Method::Notify` 経由の受け口として) +7. `task` を `Method::Run` で送信 +8. spawn 記録を Pod のランタイム状態に保存 + +### Pod 起動コマンド + +- デフォルト: `pod`(PATH 上のバイナリ) +- 環境変数 `INSOMNIA_POD_COMMAND` またはユーザー manifest で上書き可能 +- 引数: `--overlay ` で scope / instruction / name / pwd を渡す + +### spawn 記録 + +Pod が保持する既知の Pod リスト。各エントリ: +- `name`, `name`, `socket_path`, `scope_delegated`, `callback_address` +- spawner 復帰時にこの記録を読んで再接続する + +## 設計で決めること + +- **spawn 記録の永続化**: session-store に載せるか、runtime_dir にファイルとして書くか +- **socket 待機のタイムアウト**: spawned Pod が socket を開くまでの待機時間 +- **callback address の形式**: ローカルでは spawner の socket path、リモートでは `insomnia@host:pod-name` + +## 完了条件 + +- LLM が `SpawnPod` ツールを呼び出すと、新しい Pod プロセスが独立して起動する +- scope lock file に分譲が記録され、spawner の effective scope が縮小する +- 要求 scope が spawner の effective scope を超えていたらツールエラー +- spawned Pod の socket に接続でき、`task` が `Method::Run` で送信される +- spawn 記録が保存され、`ListPods` で参照できる +- spawner が停止しても spawned Pod は続行する + +## 範囲外 + +- Pod 間通信ツール(SendToPod / ReadPodOutput / StopPod / ListPods)は `tickets/pod-comm-tools.md` +- コールバック通知は `tickets/pod-callback.md` +- リモート spawn(SSH 越し)は `docs/network-peering.md` を参照