From 170e0c2099b35a338016701e4bffcbc466c5df06 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 19 Apr 2026 15:14:15 +0900 Subject: [PATCH] =?UTF-8?q?SpawnPod=E3=83=84=E3=83=BC=E3=83=AB=E3=81=8C?= =?UTF-8?q?=E8=90=BD=E3=81=A1=E3=82=8B=E5=95=8F=E9=A1=8C=E3=81=AE=E7=99=BA?= =?UTF-8?q?=E8=A6=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 + crates/pod/src/spawn_pod.rs | 58 +++++++++++++++++++++++++++++-- tickets/spawn-inherit-provider.md | 28 +++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tickets/spawn-inherit-provider.md diff --git a/TODO.md b/TODO.md index a6846208..916948c7 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) - [ ] Pod オーケストレーション - [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md) + - [ ] SpawnPod が親の provider 設定を引き継ぐ → [tickets/spawn-inherit-provider.md](tickets/spawn-inherit-provider.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/crates/pod/src/spawn_pod.rs b/crates/pod/src/spawn_pod.rs index ef0e9f48..731de656 100644 --- a/crates/pod/src/spawn_pod.rs +++ b/crates/pod/src/spawn_pod.rs @@ -194,7 +194,9 @@ impl Tool for SpawnPodTool { } }; - let start_outcome = self.exec_child(&overlay_toml, &predicted_socket).await; + let start_outcome = self + .exec_child(&input.name, &overlay_toml, &predicted_socket) + .await; if let Err(e) = start_outcome { self.release_reservation(&lock_path, &input.name); return Err(e); @@ -242,11 +244,31 @@ impl Tool for SpawnPodTool { impl SpawnPodTool { async fn exec_child( &self, + pod_name: &str, overlay_toml: &str, predicted_socket: &Path, ) -> Result<(), ToolError> { let pod_command = std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "pod".into()); + // Pre-create the child's runtime dir so we have a stable place to + // capture its stderr before it has had a chance to bind anything. + // The child's own `RuntimeDir::create` will `create_dir_all` the + // same path again — that's idempotent. On clean exit the child's + // RuntimeDir Drop tears the dir (and this log) down with it. + let pod_runtime_dir = self.runtime_base.join(pod_name); + tokio::fs::create_dir_all(&pod_runtime_dir) + .await + .map_err(|e| { + ToolError::ExecutionFailed(format!( + "create runtime dir {}: {e}", + pod_runtime_dir.display() + )) + })?; + let stderr_path = pod_runtime_dir.join("stderr.log"); + let stderr_file = std::fs::File::create(&stderr_path).map_err(|e| { + ToolError::ExecutionFailed(format!("open {}: {e}", stderr_path.display())) + })?; + let mut cmd = Command::new(&pod_command); cmd.arg("--adopt") .arg("--callback") @@ -256,7 +278,7 @@ impl SpawnPodTool { .current_dir(&self.spawner_pwd) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stderr(Stdio::from(stderr_file)) .process_group(0); let child = cmd @@ -269,7 +291,10 @@ impl SpawnPodTool { // orphans. Lifecycle tracking lives in `spawned_pods.json`. drop(child); - wait_for_socket(predicted_socket, SOCKET_WAIT_TIMEOUT).await + match wait_for_socket(predicted_socket, SOCKET_WAIT_TIMEOUT).await { + Ok(()) => Ok(()), + Err(e) => Err(annotate_with_stderr(e, &stderr_path).await), + } } fn release_reservation(&self, lock_path: &Path, pod_name: &str) { @@ -330,6 +355,33 @@ fn build_overlay_toml( toml::to_string(&overlay) } +/// Tail of the spawned child's `stderr.log` to splice into a startup +/// failure message. Capped so a chatty child can't blow up the LLM's +/// tool-result budget — debugging beyond this should read the file +/// directly. +const STDERR_TAIL_BYTES: usize = 4 * 1024; + +async fn annotate_with_stderr(err: ToolError, stderr_path: &Path) -> ToolError { + let tail = match tokio::fs::read(stderr_path).await { + Ok(bytes) => { + let start = bytes.len().saturating_sub(STDERR_TAIL_BYTES); + String::from_utf8_lossy(&bytes[start..]).into_owned() + } + Err(_) => return err, + }; + let trimmed = tail.trim(); + if trimmed.is_empty() { + return err; + } + match err { + ToolError::ExecutionFailed(msg) => ToolError::ExecutionFailed(format!( + "{msg}\n--- child stderr ({}) ---\n{trimmed}", + stderr_path.display() + )), + other => other, + } +} + async fn wait_for_socket(path: &Path, timeout: Duration) -> Result<(), ToolError> { let deadline = tokio::time::Instant::now() + timeout; loop { diff --git a/tickets/spawn-inherit-provider.md b/tickets/spawn-inherit-provider.md new file mode 100644 index 00000000..5471c833 --- /dev/null +++ b/tickets/spawn-inherit-provider.md @@ -0,0 +1,28 @@ +# SpawnPod が親の provider 設定を引き継ぐ + +## 背景 + +`SpawnPod` ツールは子 Pod に渡す overlay TOML に `pod.name` / `worker.instruction` / `scope` のみを含め、`provider` を含めない(`crates/pod/src/spawn_pod.rs::build_overlay_toml`)。 + +子 Pod は起動時に manifest cascade を一からやり直すため、デフォルトのインストラクション (`$insomnia/default`) を使うと `provider.kind` が見つからず `failed to resolve manifest cascade: missing required field: provider.kind` で起動失敗する。結果として `SpawnPod` ツールがデフォルト引数では事実上使えない状態になっている。 + +## ゴール + +`SpawnPod` で起動した子 Pod が、特に指定なしで親 Pod と同じ provider 設定(kind, model, api_key 等)を使って起動できる。 + +## 必要な変更 + +- 親 Pod の resolved provider を `SpawnPodTool` から参照できるようにする(`SpawnPodTool::new` の引数追加 等) +- `build_overlay_toml` が overlay に provider を含める +- ツール入力 (`SpawnPodInput`) からの provider 指定は今回は不要(範囲外を参照) + +## 完了条件 + +- `SpawnPod` を `provider` 指定なしで呼び出すと、子 Pod が親と同じ provider 設定で起動し、`Method::Run` を受けて応答できる +- 既存の spawn_pod 統合テスト (`crates/pod/tests/spawn_pod_test.rs`) が引き続きパス +- 親が provider を持たないケース(あり得るなら)の挙動が定まっている + +## 範囲外 + +- ツール入力からの provider override(モデルだけ差し替えたい等のマルチエージェント用途)。将来別チケットで `SpawnPodInput` に optional provider override を追加する形で扱う +- インストラクションファイル側で provider を指定する仕組み