This commit is contained in:
Keisuke Hirata 2026-04-11 03:23:48 +09:00
parent 0fe05e502e
commit f4f398279e
10 changed files with 411 additions and 0 deletions

View File

@ -0,0 +1,67 @@
# コンテキスト圧縮: Prune + Compact
## 背景
長時間実行エージェントにとって、コンテキストウィンドウの管理はコア要件。
現状の Worker は history をそのまま保持し、オーバーフロー時の対策がない。
OpenCode は2段階のアプローチを採る:
1. **Prune**: 古いツール出力を削除してトークンを回収
2. **Compact**: 専用エージェントで会話全体を構造化要約に圧縮
Insomnia では Hook ベースで同等の機能を実現できる。
## 方針
### Phase 1: PruneWorker 層)
`PreLlmRequest` 相当のポイントで、古いツール出力を除去する。
```
history 内のツール出力を走査:
- 直近 N ターン以内 → 保護
- それ以前 → 出力を "[pruned — stored as blob {id}]" に置換
```
- Blob Storage にはすでに退避済みllm-worker-persistence の Stored 出力)
- Prune は参照を残して本文だけ削る操作
- Worker の `Mutable` 状態で history 編集が可能
### Phase 2: CompactPod 層)
トークン数が閾値を超えた場合、Controller が要約を挿入する。
```
1. OnTurnEnd でトークン使用量をチェック
2. 閾値超過 → Controller が要約生成を実行
3. history を [system_prompt, compaction_summary, 直近の会話] に圧縮
4. resume で作業を継続
```
要約フォーマットOpenCode の構造化要約を参考):
```
## Goal
(元のユーザー指示)
## Accomplished
(完了した作業の箇条書き)
## Key Discoveries
(判明した事実・制約)
## Current State
(ファイル変更・残タスク)
```
## 設計ポイント
- Phase 1 は Worker 層の拡張。llm-worker に `prune` 機能を追加
- Phase 2 は Pod 層の制御。Controller が別の Worker要約用を起動する
- 要約用 Worker は短命で、ツールなし・プロンプトのみ
- OpenCode の「Replay」圧縮後に前回のメッセージを再送`resume()` で自然に実現可能
- 設計原則3: 新しい trait は不要。Worker の history 操作 + Controller の制御で完結
## 依存チケット
- [remove-hook-module.md](remove-hook-module.md) — Hook が insomnia 層に移動した後、PreLlmRequest で Prune を差し込む

44
tickets/max-turns.md Normal file
View File

@ -0,0 +1,44 @@
# 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

@ -0,0 +1,54 @@
# パーミッション: パターンベースのツール実行制御
## 背景
現状の `Scope` はディレクトリ単位の書き込み制約で、静的な境界線。
実際のエージェント運用では、ツール単位・引数パターン単位の動的な権限制御が必要になる。
OpenCode はパターンベースのルールtool × pattern → allow/deny/askを持ち、
`*.env` への書き込み拒否や `rm -rf` の実行拒否を宣言的に設定できる。
## 方針
`PreToolCall` Hook として実装する。マニフェストにルールを宣言し、
insomnia 層の Hook 実装がツール呼び出し時に評価する。
```toml
[[permission]]
tool = "bash"
pattern = "rm *"
action = "deny"
[[permission]]
tool = "file_write"
pattern = "*.env"
action = "deny"
[[permission]]
tool = "*"
pattern = "*"
action = "allow"
```
評価順序OpenCode に倣う):
1. 最初にマッチした `deny` → 拒否
2. すべてマッチする `allow` → 許可
3. それ以外 → `ask`(ユーザー確認)
## 設計ポイント
- 設計原則3: 新しい trait は作らない。`PreToolCall` Hook として実装
- 設計原則2: マニフェストに宣言した以上、insomnia 層が解決する
- `ask` アクションは Pod Protocol の拡張が必要Method に `PermissionReply` を追加)
- `Scope` との関係: Scope は書き込みの物理的境界、Permission はツール実行のポリシー。補完関係
- ルール評価はパターンマッチのみ。コンテキスト依存の判断はしない(シンプルに保つ)
## 段階的実装
1. **拡張ポイントの記録**(今): docs/pod.md の拡張ポイント表に追加
2. **deny/allow の実装**(ツール実装時): PreToolCall Hook でパターン評価
3. **ask の実装**Protocol 拡張時): Method/Event に Permission 関連メッセージを追加
## 依存チケット
- [remove-hook-module.md](remove-hook-module.md) — PreToolCall が insomnia 層に移動してから実装

33
tickets/pod-binary.md Normal file
View File

@ -0,0 +1,33 @@
# 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` — エントリポイント新規作成

View File

@ -0,0 +1,42 @@
# protocol: JSONL ストリーム変換ユーティリティ
## 背景
protocol クレートは現在型定義のみ。JSONL の読み書き処理BufReader + lines + デシリアライズ / シリアライズ + `\n` + write_allが socket_server.rs と client.rs で重複している。空行スキップやエラー変換のロジックも各所にベタ書き。
## 方針
`protocol::stream` モジュールに `JsonLineReader` / `JsonLineWriter` を追加し、JSONL 変換を protocol クレートの責務に含める。
```rust
// protocol::stream
pub struct JsonLineReader<R> { /* BufReader<R> */ }
pub struct JsonLineWriter<W> { /* W */ }
impl<R: AsyncBufRead + Unpin> JsonLineReader<R> {
pub fn new(reader: R) -> Self;
pub async fn next<T: DeserializeOwned>(&mut self) -> Result<Option<T>, Error>;
}
impl<W: AsyncWrite + Unpin> JsonLineWriter<W> {
pub fn new(writer: W) -> Self;
pub async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), Error>;
}
```
## 設計ポイント
- feature gate にはしない。このプロジェクトで protocol を tokio なしで使う場面がない
- tokio の依存は `io-util` のみ(`AsyncBufRead`, `AsyncWrite` に必要な最小限)
- 空行スキップ、改行付与、serde エラーの IO エラー変換を一箇所に集約
- `JsonLineReader` は内部で `BufReader` を持つ。呼び出し側で `BufReader` を作る必要がない
## 変更対象
- `crates/protocol/Cargo.toml` — tokio (io-util) 依存を追加
- `crates/protocol/src/stream.rs` — 新規モジュール
- `crates/protocol/src/lib.rs``pub mod stream`
- `crates/pod/src/socket_server.rs``JsonLineReader` / `JsonLineWriter` に置き換え
- `crates/tui/src/client.rs` — 同上
- `crates/pod/tests/controller_test.rs` — テストのストリーム処理も置き換え

View File

@ -0,0 +1,42 @@
# Hook モジュールの llm-worker からの除去
## 背景
llm-worker は低レベル基盤に徹するべきだが、現行の `hook` モジュールは
高レベルのオーケストレーション関心(承認フロー、ターン制御等)を含んでいる。
Claude Code の Hooks のような機能は insomnia 層の責務。
低レベルのストリーム介入は、クロージャベースの Subscriber API で十分カバーできる。
## 方針
`hook` モジュールを llm-worker から除去し、責務を分離する。
### Subscriber で代替(削除)
ストリーム観測・介入はクロージャ Subscriber で対応:
- `OnTextDelta`
- `OnToolCallDelta`
- `OnStreamChunk`
- `OnStreamComplete`
### insomnia 層に移動
高レベルオーケストレーションは上位層が担う:
- `OnPromptSubmit`
- `PreLlmRequest`
- `PreToolCall` / `PostToolCall`
- `OnTurnEnd`
- `OnAbort`
## 設計ポイント
- Worker の実行ループは「ストリーム受信 → ツール実行 → 次ターン」に集中させる
- 介入ポイント(承認、中断、ターン継続判断)は insomnia 層が提供する
- `HookEventKind` / `Hook<E>` の型設計自体は良いので、insomnia 層で再利用可能
## 依存チケット
- [subscriber-closure-api.md](subscriber-closure-api.md) — ストリーム系 Hook の代替先

View File

@ -0,0 +1,43 @@
# Subscriber API: クロージャベースのスコープ表現
## 背景
Block系イベントは時間的に排他TextBlock中にToolUseBlockは来ないで、
Meta系イベントUsage等はいつでも流れ得る。
現行の `Handler<K>` + `Scope: Default` はこの保証を実現しているが、
ユーザーから見ると Kind/Scope/型消去のボイラープレートが重く、
`Timeline``WorkerSubscriber` の2層が「どちらを使えばいいか」分かりにくい。
## 方針
クロージャでスコープの寿命を表現し、ブロックの時間的排他性を Rust の借用で自然に保証する。
```rust
// Block系: クロージャ引数 = スコープの寿命保証
worker.on_text_block(|block| {
block.on_delta(|text| print!("{}", text));
block.on_stop(|reason| println!("\n---"));
});
worker.on_tool_use_block(|block| {
block.on_delta(|json| { /* ... */ });
block.on_stop(|call| { /* ... */ });
});
// Meta系: スコープ不要(いつでも来る)
worker.on_usage(|usage| { /* ... */ });
```
## 設計ポイント
- ブロックのスコープ = クロージャの借用寿命。ユーザーは `Kind` / `Scope: Default` を知らなくていい
- Block系の排他性が「ブロックが始まったらクロージャが呼ばれ、終わったら抜ける」という直感に一致する
- Meta系はフラットなコールバック。スコープ管理不要
- 内部的には現行の `Handler<K>` + `Timeline` ディスパッチ機構を維持し、クロージャを Handler に変換するアダプタを挟む
- `WorkerSubscriber` トレイト + 5種の SubscriberAdapter ボイラープレートが不要になる
## 現行からの変更
- `Timeline``pub mod` → 内部実装に格下げWorker の実装詳細に閉じ込める)
- `WorkerSubscriber` トレイト → 廃止。クロージャ登録 API に置き換え
- `Handler<K>` トレイトは内部で維持(クロージャからの変換先として)

14
tickets/test-design.md Normal file
View File

@ -0,0 +1,14 @@
# テスト設計
## 背景
各クレートのテスト方針が未策定。クレート間の依存関係と非同期処理が絡むため、
テストの層(単体/結合/E2Eと mock 境界を明確にする必要がある。
## 検討事項
- `llm-worker`: LlmClient の mock 実装によるターンループ・ツール実行のテスト
- `llm-worker-persistence`: FsStore / FsBlobStore のファイルシステムテストtempdir
- `pod`: PodController / SocketServer の結合テスト
- `protocol`: シリアライズ/デシリアライズの往復テスト
- `manifest`: パースのバリエーション(既存テストの拡充)

View File

@ -0,0 +1,24 @@
# ツールの動的追加/削除
## 背景
現状の `ToolServer` はツールの登録のみで、実行中の unregister / replace ができない。
エージェントが状況に応じてツールセットを切り替えるユースケース(例: フェーズ遷移、権限変更)に対応できない。
## 方針
`ToolServer` / `ToolServerHandle` に動的操作を追加する。
```rust
// 削除
tool_server.unregister("tool_name")?;
// 置換(同名ツールを新しい実装で上書き)
tool_server.replace(new_tool_definition)?;
```
## 設計ポイント
- 実行中のツール呼び出しとの競合を考慮(呼び出し中のツールは削除をブロックするか、完了を待つか)
- LLM に渡すツール定義リストは次の `PreLlmRequest` 時点で反映される(遅延反映で十分)
- Builder APIworker-builder-api.mdとの整合: `reconfigure()` で静的に差し替える方法と、動的に差し替える方法の使い分け

View File

@ -0,0 +1,48 @@
# Worker API: Builder パターンによるキャッシュ保護の自動化
## 背景
現状の `Worker` は Type-state パターンで `Mutable` / `CacheLocked` の2状態を持つが、
`lock()` を呼ばなくても `run()` できてしまうため、知らないユーザーは最適でないパスを通る。
"Pit of success" になっていない。
## 方針
Builder パターンで「設定フェーズ」と「実行フェーズ」を自然に分離する。
```rust
// 設定フェーズ(自由に編集可能)
let worker = Worker::builder(client)
.system_prompt("You are a helpful assistant.")
.tool(my_tool)?
.hook(my_hook)
.build(); // ← ここで自動的にcache-protected状態へ
// 実行フェーズprefix不変、キャッシュ保護済み
worker.run("Hello").await?;
// 再設定が必要な場合
let builder = worker.reconfigure();
builder.tool(another_tool)?;
let worker = builder.build();
```
## 設計ポイント
- `build()` 後は常にキャッシュ保護状態。lock の存在をユーザーが意識する必要がない
- 設定と実行の分離が型で強制される(コンパイル時保証)
- `reconfigure()` でビルダーに戻せるため、動的なツール追加等にも対応可能
- Type-state の恩恵は維持しつつ、APIの認知負荷を下げる
## ToolDefinition ファクトリの遅延初期化
現状 `register_tool()` 時に `ToolDefinition` のファクトリクロージャが即時呼び出しされている。
Builder パターンへの移行に伴い、`build()` 時(= セッション開始時)まで遅延させる。
- `builder.tool(definition)` は定義を蓄積するだけ
- `build()` でファクトリを一括実行し、ToolServer を構築
## 移行
- 現行の `lock()` / `unlock()` は deprecated → 次メジャーで削除
- `Worker::new()``Worker::builder()` へ移行