SpawnPodツールが落ちる問題の発見
This commit is contained in:
parent
ddd7327290
commit
25df7a79c1
1
TODO.md
1
TODO.md
|
|
@ -5,6 +5,7 @@
|
||||||
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||||
- [ ] Pod オーケストレーション
|
- [ ] Pod オーケストレーション
|
||||||
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
|
- [ ] 動的 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)
|
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
|
||||||
- [ ] TUI 拡充
|
- [ ] TUI 拡充
|
||||||
- [ ] 新しい Pod を spawn する UI の設計 → [tickets/tui-pod-spawn-ui.md](tickets/tui-pod-spawn-ui.md)
|
- [ ] 新しい Pod を spawn する UI の設計 → [tickets/tui-pod-spawn-ui.md](tickets/tui-pod-spawn-ui.md)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
if let Err(e) = start_outcome {
|
||||||
self.release_reservation(&lock_path, &input.name);
|
self.release_reservation(&lock_path, &input.name);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
|
|
@ -242,11 +244,31 @@ impl Tool for SpawnPodTool {
|
||||||
impl SpawnPodTool {
|
impl SpawnPodTool {
|
||||||
async fn exec_child(
|
async fn exec_child(
|
||||||
&self,
|
&self,
|
||||||
|
pod_name: &str,
|
||||||
overlay_toml: &str,
|
overlay_toml: &str,
|
||||||
predicted_socket: &Path,
|
predicted_socket: &Path,
|
||||||
) -> Result<(), ToolError> {
|
) -> Result<(), ToolError> {
|
||||||
let pod_command = std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "pod".into());
|
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);
|
let mut cmd = Command::new(&pod_command);
|
||||||
cmd.arg("--adopt")
|
cmd.arg("--adopt")
|
||||||
.arg("--callback")
|
.arg("--callback")
|
||||||
|
|
@ -256,7 +278,7 @@ impl SpawnPodTool {
|
||||||
.current_dir(&self.spawner_pwd)
|
.current_dir(&self.spawner_pwd)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::from(stderr_file))
|
||||||
.process_group(0);
|
.process_group(0);
|
||||||
|
|
||||||
let child = cmd
|
let child = cmd
|
||||||
|
|
@ -269,7 +291,10 @@ impl SpawnPodTool {
|
||||||
// orphans. Lifecycle tracking lives in `spawned_pods.json`.
|
// orphans. Lifecycle tracking lives in `spawned_pods.json`.
|
||||||
drop(child);
|
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) {
|
fn release_reservation(&self, lock_path: &Path, pod_name: &str) {
|
||||||
|
|
@ -330,6 +355,33 @@ fn build_overlay_toml(
|
||||||
toml::to_string(&overlay)
|
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> {
|
async fn wait_for_socket(path: &Path, timeout: Duration) -> Result<(), ToolError> {
|
||||||
let deadline = tokio::time::Instant::now() + timeout;
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
28
tickets/spawn-inherit-provider.md
Normal file
28
tickets/spawn-inherit-provider.md
Normal file
|
|
@ -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 を指定する仕組み
|
||||||
Loading…
Reference in New Issue
Block a user