Podのバイナリ実装

This commit is contained in:
Keisuke Hirata 2026-04-11 03:26:38 +09:00
parent f4f398279e
commit 9363c76354
6 changed files with 135 additions and 81 deletions

1
Cargo.lock generated
View File

@ -1159,6 +1159,7 @@ name = "pod"
version = "0.1.0"
dependencies = [
"async-trait",
"clap",
"dotenv",
"futures",
"llm-worker",

View File

@ -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 バイナリエントリポイント

View File

@ -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"] }

127
crates/pod/src/main.rs Normal file
View File

@ -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<PathBuf>,
}
fn default_store_dir() -> Result<PathBuf, std::io::Error> {
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<PathBuf, std::io::Error> {
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
}

View File

@ -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<u32>` を追加
- `apply_worker_manifest` で Worker に設定を反映
- Worker の turn loop でカウント・判定

View File

@ -1,33 +0,0 @@
# pod: バイナリエントリポイントの追加
## 背景
pod クレートは現在ライブラリのみ(`lib.rs`)。バイナリとしての起動ルートがなく、実行には `examples/pod_cli.rs` を使うか外部クレートから呼ぶしかない。pod 単体で起動できる `main.rs` を追加する。
## 方針
`-m <path>` でマニフェストファイルのパスを受け取って起動する。
```
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` — エントリポイント新規作成