diff --git a/Cargo.lock b/Cargo.lock index 6e27ff8e..1476a2b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,6 +1159,7 @@ name = "pod" version = "0.1.0" dependencies = [ "async-trait", + "clap", "dotenv", "futures", "llm-worker", diff --git a/TODO.md b/TODO.md index dcb777d6..c57d59d7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,9 @@ - [x] 永続化データ構造の制定 -- [ ] テスト設計 +- [ ] テスト設計 → [tickets/test-design.md](tickets/test-design.md) - [x] ツール出力の遅延読み込み設計 (ToolOutput / BlobStore / auto_summarize) - [ ] ツール設計 - - [ ] ツールの動的追加/削除 (unregister, replace) - - [ ] ToolDefinition ファクトリの遅延初期化修正 (現状 register 時に即時呼び出しされている。セッション開始=初回メッセージ送信時まで遅延させる) + - [ ] ツールの動的追加/削除 → [tickets/tool-dynamic-registry.md](tickets/tool-dynamic-registry.md) + - [ ] ToolDefinition ファクトリの遅延初期化修正 → [tickets/worker-builder-api.md](tickets/worker-builder-api.md) - [x] inspect ツール実装 +- [x] max_turns: マニフェストによるターン数制限 +- [x] pod バイナリエントリポイント diff --git a/crates/pod/Cargo.toml b/crates/pod/Cargo.toml index 524a7cff..a9b15811 100644 --- a/crates/pod/Cargo.toml +++ b/crates/pod/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true license.workspace = true [dependencies] +clap = { version = "4.6.0", features = ["derive"] } llm-worker = { version = "0.2.1", path = "../llm-worker" } llm-worker-persistence = { version = "0.1.0", path = "../llm-worker-persistence" } manifest = { version = "0.1.0", path = "../manifest" } @@ -13,7 +14,7 @@ provider = { version = "0.1.0", path = "../provider" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" thiserror = "2.0" -tokio = { version = "1.49", features = ["fs", "io-util", "net", "sync"] } +tokio = { version = "1.49", features = ["fs", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync"] } toml = "1.1.2" uuid = { version = "1.23.0", features = ["v7", "serde"] } diff --git a/crates/pod/src/main.rs b/crates/pod/src/main.rs new file mode 100644 index 00000000..45da5376 --- /dev/null +++ b/crates/pod/src/main.rs @@ -0,0 +1,127 @@ +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::Parser; +use llm_worker_persistence::FsStore; +use pod::{Pod, PodController}; + +#[derive(Parser)] +#[command(name = "pod", about = "Run a Pod process from a manifest file")] +struct Cli { + /// Path to the manifest TOML file + #[arg(short, long)] + manifest: PathBuf, + + /// Directory for session persistence (default: ~/.insomnia/sessions/) + #[arg(short, long)] + store: Option, +} + +fn default_store_dir() -> Result { + let home = std::env::var("HOME").map_err(|_| { + std::io::Error::new(std::io::ErrorKind::NotFound, "HOME is not set") + })?; + Ok(PathBuf::from(home).join(".insomnia").join("sessions")) +} + +fn default_runtime_dir() -> Result { + if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { + Ok(PathBuf::from(runtime_dir).join("insomnia")) + } else if let Ok(home) = std::env::var("HOME") { + Ok(PathBuf::from(home).join(".insomnia").join("run")) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "neither XDG_RUNTIME_DIR nor HOME is set", + )) + } +} + +#[tokio::main] +async fn main() -> ExitCode { + let cli = Cli::parse(); + + // Read and parse the manifest + let toml_str = match tokio::fs::read_to_string(&cli.manifest).await { + Ok(s) => s, + Err(e) => { + eprintln!("error: failed to read manifest {:?}: {e}", cli.manifest); + return ExitCode::FAILURE; + } + }; + let manifest = match manifest::PodManifest::from_toml(&toml_str) { + Ok(m) => m, + Err(e) => { + eprintln!("error: invalid manifest: {e}"); + return ExitCode::FAILURE; + } + }; + + let pod_name = manifest.pod.name.clone(); + + // Initialize persistent store + let store_dir = cli.store.unwrap_or_else(|| { + default_store_dir().unwrap_or_else(|_| PathBuf::from(".insomnia/sessions")) + }); + let store = match FsStore::new(&store_dir).await { + Ok(s) => s, + Err(e) => { + eprintln!("error: failed to initialize store at {store_dir:?}: {e}"); + return ExitCode::FAILURE; + } + }; + + // Build scope from manifest + let scope = match manifest.scope.as_ref() { + Some(sc) => match manifest::Scope::new(&sc.root) { + Ok(s) => Some(s), + Err(e) => { + eprintln!("error: invalid scope root {:?}: {e}", sc.root); + return ExitCode::FAILURE; + } + }, + None => None, + }; + + // Build the Pod + let pod = match Pod::from_manifest(manifest, store, scope).await { + Ok(p) => p, + Err(e) => { + eprintln!("error: failed to create pod: {e}"); + return ExitCode::FAILURE; + } + }; + + // Spawn the controller (starts socket server) + let runtime_base = match default_runtime_dir() { + Ok(d) => d, + Err(e) => { + eprintln!("error: {e}"); + return ExitCode::FAILURE; + } + }; + let handle = match PodController::spawn(pod, &runtime_base).await { + Ok(h) => h, + Err(e) => { + eprintln!("error: failed to start pod controller: {e}"); + return ExitCode::FAILURE; + } + }; + + eprintln!("pod: {pod_name} listening on {:?}", handle.runtime_dir.socket_path()); + + // Wait for shutdown signal + match tokio::signal::ctrl_c().await { + Ok(()) => { + eprintln!("pod: {pod_name} shutting down"); + } + Err(e) => { + eprintln!("error: failed to listen for signal: {e}"); + } + } + + // TODO: handle.shutdown().await — PodController に採用しないスフルシャットダウン機構を追加したら組み込む + drop(handle); + + ExitCode::SUCCESS +} diff --git a/tickets/max-turns.md b/tickets/max-turns.md deleted file mode 100644 index 198c913c..00000000 --- a/tickets/max-turns.md +++ /dev/null @@ -1,44 +0,0 @@ -# max_turns: マニフェストによるターン数制限 - -## 背景 - -Pod は長時間自律実行を前提としているが、暴走防止のガードレールがない。 -OpenCode は Agent 定義に `steps`(最大ツール呼び出し回数)を持ち、 -サブエージェントが無限ループに陥ることを構造的に防いでいる。 - -Insomnia では Worker の `OnTurnEnd` 相当の制御ポイントで同様の保護が可能だが、 -マニフェストに宣言がないため「設定忘れ」が暴走を許す。 - -## 方針 - -`[worker]` セクションに `max_turns` を追加し、Worker の実行ループで強制する。 - -```toml -[worker] -system_prompt = "You are a code reviewer." -max_tokens = 4096 -max_turns = 50 # 省略時: 制限なし(明示的な無制限) -``` - -## 設計ポイント - -- Worker の turn loop 内でカウントし、超過時は `PodRunResult::Finished` で正常終了 -- 「制限に達した」ことを Event として通知(`TurnEnd` の `result` に `LimitReached` を追加) -- 省略時は制限なし。長時間実行 Pod は意図的に省略する -- `max_turns = 0` はエラー(0ターンの Pod に意味はない) - -## TurnResult の拡張 - -```rust -pub enum TurnResult { - Finished, - Paused, - LimitReached, // 追加 -} -``` - -## 実装場所 - -- `WorkerManifest` に `max_turns: Option` を追加 -- `apply_worker_manifest` で Worker に設定を反映 -- Worker の turn loop でカウント・判定 diff --git a/tickets/pod-binary.md b/tickets/pod-binary.md deleted file mode 100644 index cd7c1ae2..00000000 --- a/tickets/pod-binary.md +++ /dev/null @@ -1,33 +0,0 @@ -# pod: バイナリエントリポイントの追加 - -## 背景 - -pod クレートは現在ライブラリのみ(`lib.rs`)。バイナリとしての起動ルートがなく、実行には `examples/pod_cli.rs` を使うか外部クレートから呼ぶしかない。pod 単体で起動できる `main.rs` を追加する。 - -## 方針 - -`-m ` でマニフェストファイルのパスを受け取って起動する。 - -``` -pod -m manifest.toml -``` - -### stdin 対応 - -`-m -` で stdin からマニフェストを読む(jq / kubectl と同じ慣習)。優先度は低い。 - -### CLI args でのマニフェスト指定は採用しない - -PodManifest は `[pod]` / `[provider]` / `[worker]` のネスト構造。フラットな引数に展開すると煩雑で、スキーマ変更に引数パーサーが追従し続ける必要がある。 - -## 設計ポイント - -- daemon が pod プロセスを spawn する際、RuntimeDir に書き出し済みのマニフェストファイルのパスをそのまま渡す流れを想定 -- マニフェストがファイルとして残るため、`ps` での確認・再起動・デバッグが容易 -- stdin を占有しないので将来の対話入力と競合しない -- マニフェストの再読み込み(将来的なホットリロード)にもパスがあれば対応可能 - -## 変更対象 - -- `crates/pod/Cargo.toml` — `[[bin]]` セクション追加、clap 依存追加 -- `crates/pod/src/main.rs` — エントリポイント新規作成