マニフェストを継承してPodをスポーンさせる
This commit is contained in:
parent
25df7a79c1
commit
a89701bc43
1
TODO.md
1
TODO.md
|
|
@ -5,7 +5,6 @@
|
||||||
- [ ] パーミッション: パターンベースのツール実行制御 → [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)
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ impl PodController {
|
||||||
let scope_for_tools = pod.scope().clone();
|
let scope_for_tools = pod.scope().clone();
|
||||||
let pwd_for_tools = pod.pwd().to_path_buf();
|
let pwd_for_tools = pod.pwd().to_path_buf();
|
||||||
let spawner_name = pod.manifest().pod.name.clone();
|
let spawner_name = pod.manifest().pod.name.clone();
|
||||||
|
let spawner_provider = pod.manifest().provider.clone();
|
||||||
|
|
||||||
// Parent callback socket (this Pod's own parent, used for
|
// Parent callback socket (this Pod's own parent, used for
|
||||||
// `PodEvent` upward reports). `None` for top-level Pods.
|
// `PodEvent` upward reports). `None` for top-level Pods.
|
||||||
|
|
@ -229,6 +230,7 @@ impl PodController {
|
||||||
pwd_for_tools,
|
pwd_for_tools,
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
self_parent_socket.clone(),
|
self_parent_socket.clone(),
|
||||||
|
spawner_provider.clone(),
|
||||||
));
|
));
|
||||||
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
||||||
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ use std::time::Duration;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
use manifest::{
|
use manifest::{
|
||||||
Permission, PodManifestConfig, PodMetaConfig, ScopeConfig, ScopeRule, WorkerManifestConfig,
|
Permission, PodManifestConfig, PodMetaConfig, ProviderConfig, ProviderConfigPartial,
|
||||||
|
ScopeConfig, ScopeRule, WorkerManifestConfig,
|
||||||
};
|
};
|
||||||
use protocol::Method;
|
use protocol::Method;
|
||||||
use protocol::stream::JsonLineWriter;
|
use protocol::stream::JsonLineWriter;
|
||||||
|
|
@ -113,6 +114,11 @@ pub struct SpawnPodTool {
|
||||||
/// `None` for top-level Pods — in that case the re-emission is a
|
/// `None` for top-level Pods — in that case the re-emission is a
|
||||||
/// no-op.
|
/// no-op.
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
/// Spawner's resolved provider config — copied into every spawned
|
||||||
|
/// Pod's overlay TOML so the child does not need its own provider
|
||||||
|
/// configuration in the manifest cascade. Per-spawn override is
|
||||||
|
/// out of scope here (see `tickets/spawn-inherit-provider.md`).
|
||||||
|
spawner_provider: ProviderConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpawnPodTool {
|
impl SpawnPodTool {
|
||||||
|
|
@ -123,6 +129,7 @@ impl SpawnPodTool {
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
spawner_provider: ProviderConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
spawner_name,
|
spawner_name,
|
||||||
|
|
@ -131,6 +138,7 @@ impl SpawnPodTool {
|
||||||
spawner_pwd,
|
spawner_pwd,
|
||||||
registry,
|
registry,
|
||||||
parent_socket,
|
parent_socket,
|
||||||
|
spawner_provider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +192,12 @@ impl Tool for SpawnPodTool {
|
||||||
// it back — even if later steps (Method::Run delivery, record
|
// it back — even if later steps (Method::Run delivery, record
|
||||||
// write) fail, the child is running and will release its own
|
// write) fail, the child is running and will release its own
|
||||||
// entry on exit.
|
// entry on exit.
|
||||||
let overlay_toml = match build_overlay_toml(&input.name, &instruction, &scope_allow) {
|
let overlay_toml = match build_overlay_toml(
|
||||||
|
&input.name,
|
||||||
|
&instruction,
|
||||||
|
&scope_allow,
|
||||||
|
&self.spawner_provider,
|
||||||
|
) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.release_reservation(&lock_path, &input.name);
|
self.release_reservation(&lock_path, &input.name);
|
||||||
|
|
@ -337,11 +350,18 @@ fn build_overlay_toml(
|
||||||
name: &str,
|
name: &str,
|
||||||
instruction: &str,
|
instruction: &str,
|
||||||
scope_allow: &[ScopeRule],
|
scope_allow: &[ScopeRule],
|
||||||
|
provider: &ProviderConfig,
|
||||||
) -> Result<String, toml::ser::Error> {
|
) -> Result<String, toml::ser::Error> {
|
||||||
let overlay = PodManifestConfig {
|
let overlay = PodManifestConfig {
|
||||||
pod: PodMetaConfig {
|
pod: PodMetaConfig {
|
||||||
name: Some(name.to_string()),
|
name: Some(name.to_string()),
|
||||||
},
|
},
|
||||||
|
provider: ProviderConfigPartial {
|
||||||
|
kind: Some(provider.kind),
|
||||||
|
model: Some(provider.model.clone()),
|
||||||
|
api_key_file: provider.api_key_file.clone(),
|
||||||
|
base_url: provider.base_url.clone(),
|
||||||
|
},
|
||||||
worker: WorkerManifestConfig {
|
worker: WorkerManifestConfig {
|
||||||
instruction: Some(instruction.to_string()),
|
instruction: Some(instruction.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -438,6 +458,7 @@ pub fn spawn_pod_tool(
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
spawner_provider: ProviderConfig,
|
||||||
) -> ToolDefinition {
|
) -> ToolDefinition {
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema = schemars::schema_for!(SpawnPodInput);
|
let schema = schemars::schema_for!(SpawnPodInput);
|
||||||
|
|
@ -452,7 +473,38 @@ pub fn spawn_pod_tool(
|
||||||
spawner_pwd.clone(),
|
spawner_pwd.clone(),
|
||||||
registry.clone(),
|
registry.clone(),
|
||||||
parent_socket.clone(),
|
parent_socket.clone(),
|
||||||
|
spawner_provider.clone(),
|
||||||
));
|
));
|
||||||
(meta, tool)
|
(meta, tool)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use manifest::ProviderKind;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlay_inherits_spawner_provider() {
|
||||||
|
let provider = ProviderConfig {
|
||||||
|
kind: ProviderKind::Anthropic,
|
||||||
|
model: "claude-sonnet-4".into(),
|
||||||
|
api_key_file: Some(PathBuf::from("/etc/keys/anthropic")),
|
||||||
|
base_url: Some("https://example.test".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let toml_str = build_overlay_toml("child", "$insomnia/default", &[], &provider).unwrap();
|
||||||
|
let parsed = PodManifestConfig::from_toml(&toml_str).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parsed.provider.kind, Some(ProviderKind::Anthropic));
|
||||||
|
assert_eq!(parsed.provider.model.as_deref(), Some("claude-sonnet-4"));
|
||||||
|
assert_eq!(
|
||||||
|
parsed.provider.api_key_file.as_deref(),
|
||||||
|
Some(Path::new("/etc/keys/anthropic"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.provider.base_url.as_deref(),
|
||||||
|
Some("https://example.test")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
|
||||||
use std::sync::{LazyLock, Mutex};
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
use llm_worker::tool::{ToolError, ToolOutput};
|
use llm_worker::tool::{ToolError, ToolOutput};
|
||||||
use manifest::{Permission, ScopeRule};
|
use manifest::{Permission, ProviderConfig, ProviderKind, ScopeRule};
|
||||||
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
|
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
|
||||||
use pod::scope_lock::{self, LockFileGuard};
|
use pod::scope_lock::{self, LockFileGuard};
|
||||||
use pod::spawn_pod::spawn_pod_tool;
|
use pod::spawn_pod::spawn_pod_tool;
|
||||||
|
|
@ -132,6 +132,18 @@ fn which_true() -> String {
|
||||||
"/bin/true".into()
|
"/bin/true".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests don't exercise the provider — they intercept the spawned
|
||||||
|
/// child via a mock socket — but `spawn_pod_tool` needs a value to
|
||||||
|
/// embed in the overlay TOML. Any well-formed `ProviderConfig` works.
|
||||||
|
fn dummy_provider() -> ProviderConfig {
|
||||||
|
ProviderConfig {
|
||||||
|
kind: ProviderKind::Anthropic,
|
||||||
|
model: "claude-test".into(),
|
||||||
|
api_key_file: None,
|
||||||
|
base_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_env() {
|
fn clear_env() {
|
||||||
unsafe {
|
unsafe {
|
||||||
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
|
||||||
|
|
@ -159,6 +171,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
|
dummy_provider(),
|
||||||
);
|
);
|
||||||
let (_meta, tool) = def();
|
let (_meta, tool) = def();
|
||||||
|
|
||||||
|
|
@ -221,6 +234,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
|
dummy_provider(),
|
||||||
);
|
);
|
||||||
let (_meta, tool) = def();
|
let (_meta, tool) = def();
|
||||||
|
|
||||||
|
|
@ -279,6 +293,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
|
dummy_provider(),
|
||||||
);
|
);
|
||||||
let (_meta, tool) = def();
|
let (_meta, tool) = def();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# 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