チケット更新
This commit is contained in:
parent
3d0d5ffe85
commit
5bc4a6d6d6
6
TODO.md
6
TODO.md
|
|
@ -5,7 +5,9 @@
|
|||
- [ ] ツール設計
|
||||
- [x] ツールの動的追加/削除 → [tickets/tool-dynamic-registry.md](tickets/tool-dynamic-registry.md)
|
||||
- [x] run() 自動ロックとファクトリ遅延初期化 → [tickets/worker-auto-lock.md](tickets/worker-auto-lock.md)
|
||||
- [ ] 組み込みツール実装 (tools クレート) → [tickets/builtin-tools.md](tickets/builtin-tools.md)
|
||||
- [x] 組み込みツール実装 (tools クレート、Bash 除く) → [tickets/builtin-tools.md](tickets/builtin-tools.md)
|
||||
- [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md)
|
||||
- [ ] Scope の再設計 (pwd + writable、必須化) → [tickets/scope-redesign.md](tickets/scope-redesign.md)
|
||||
- [x] inspect ツール実装
|
||||
- [x] max_turns: マニフェストによるターン数制限
|
||||
- [x] pod バイナリエントリポイント
|
||||
|
|
@ -16,7 +18,7 @@
|
|||
- [x] api_key_file: ファイルパスによるAPIキー解決 → [tickets/api-key-file.md](tickets/api-key-file.md)
|
||||
- [x] コンテキスト圧縮 (Prune + Compact) → [tickets/context-compaction.md](tickets/context-compaction.md)
|
||||
- [ ] LlmClient へ Tokenizer の導入 → [tickets/token-counter.md](tickets/token-counter.md)
|
||||
- [ ] ToolOutput.referenced_files: ツール参照ファイル追跡 → [tickets/tool-output-referenced-files.md](tickets/tool-output-referenced-files.md)
|
||||
- [ ] Tracker: ReadTracker リネーム + recent_files 追加 → [tickets/tracker.md](tickets/tracker.md)
|
||||
- [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md)
|
||||
- [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md)
|
||||
- [x] Protocol: request-response パターン (GetHistory等) → [tickets/request-response-protocol.md](tickets/request-response-protocol.md)
|
||||
|
|
|
|||
27
tickets/bash-tool.md
Normal file
27
tickets/bash-tool.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Bash ツール
|
||||
|
||||
## 背景
|
||||
|
||||
builtin-tools チケットで Read/Write/Edit/Glob/Grep の5ツールは実装済み。
|
||||
Bash ツールは子プロセスが直接 fs を触るため ScopedFs では保護できず、
|
||||
Permission 層(deny/allow ルール)との統合が前提。
|
||||
|
||||
## 実装内容
|
||||
|
||||
- コマンド実行(`tokio::process::Command`)
|
||||
- タイムアウト(`timeout` パラメータ、デフォルト 120秒、最大 600秒)
|
||||
- 作業ディレクトリの永続(ツール内部で `pwd` 状態を保持、`cd` で変更可能)
|
||||
- stdout/stderr の結合出力
|
||||
- ToolOutput の summary(コマンド + exit code)+ content(出力テキスト)
|
||||
|
||||
## Scope との関係
|
||||
|
||||
Bash の子プロセスは ScopedFs を経由しない。Scope による保護は不可能。
|
||||
|
||||
代わりに:
|
||||
- `PreToolCall` Hook + Permission ルール(`[permission]` マニフェストセクション)で制御
|
||||
- Permission 未実装の間は制約なしで動作
|
||||
|
||||
## 依存チケット
|
||||
|
||||
- [permission-extension-point.md](permission-extension-point.md) — deny/allow ルールによる Bash コマンド制御
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# 組み込みツール実装 (tools クレート)
|
||||
|
||||
## 背景
|
||||
|
||||
リファレンス仕様 (`docs/ref/reference-tool-spec.md`) に基づき、エージェントの基本操作となる
|
||||
ファイル操作ツール群と Bash ツールを実装する。
|
||||
|
||||
新規 `tools` クレートを作成し、llm-worker のツール基盤(`Tool` trait, `ToolServer`)の上に
|
||||
具体的なツール実装を載せる。
|
||||
|
||||
## 実装対象
|
||||
|
||||
| ツール | 概要 | Scope |
|
||||
|--------|------|-------|
|
||||
| Read | ファイル読み取り(offset/limit対応) | 制限なし |
|
||||
| Write | ファイル新規作成/全体上書き | ScopedFs で書き込み制限 |
|
||||
| Edit | 部分文字列置換(一意性チェック, replace_all) | ScopedFs で書き込み制限 |
|
||||
| Glob | glob パターンによるファイル検索 | 制限なし |
|
||||
| Grep | 正規表現によるファイル内容検索 | 制限なし |
|
||||
| Bash | シェルコマンド実行 | Permission 層で制御(別チケット) |
|
||||
|
||||
## ScopedFs
|
||||
|
||||
ファイル操作の Scope 境界を構造的に保証するラッパー。
|
||||
|
||||
```rust
|
||||
pub struct ScopedFs {
|
||||
scope: Scope,
|
||||
}
|
||||
|
||||
impl ScopedFs {
|
||||
// Read 系 — 制限なし
|
||||
fn read(&self, path: &Path) -> io::Result<String>;
|
||||
fn read_lines(&self, path: &Path, offset: usize, limit: usize) -> io::Result<String>;
|
||||
fn glob(&self, pattern: &str, base: Option<&Path>) -> Vec<PathBuf>;
|
||||
fn grep(&self, pattern: &str, path: Option<&Path>, opts: GrepOpts) -> GrepResult;
|
||||
|
||||
// Write 系 — Scope チェック付き
|
||||
fn write(&self, path: &Path, content: &str) -> io::Result<()>;
|
||||
fn replace(&self, path: &Path, old: &str, new: &str, all: bool) -> io::Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
- 各ファイル操作ツール(Read/Write/Edit/Glob/Grep)は ScopedFs を通してのみ fs に触る
|
||||
- Bash は子プロセスが直接 fs を触るため ScopedFs では守れない → Permission チケットの deny/allow ルールで対応
|
||||
- 既存の `manifest::Scope` を ScopedFs 内部で利用。Scope 自体は manifest に残す
|
||||
|
||||
## ツール実装パターン
|
||||
|
||||
```rust
|
||||
// tools クレートで実装
|
||||
pub struct ReadTool {
|
||||
fs: ScopedFs,
|
||||
}
|
||||
|
||||
impl Tool for ReadTool {
|
||||
async fn execute(&self, input: &str) -> Result<String, ToolError> {
|
||||
let params: ReadParams = serde_json::from_str(input)?;
|
||||
self.fs.read_lines(¶ms.file_path, params.offset, params.limit)
|
||||
.map_err(ToolError::from)
|
||||
}
|
||||
}
|
||||
|
||||
// ToolDefinition ファクトリで登録
|
||||
pub fn read_tool(fs: ScopedFs) -> ToolDefinition { ... }
|
||||
```
|
||||
|
||||
全ツールのファクトリをまとめた登録関数を公開:
|
||||
|
||||
```rust
|
||||
pub fn builtin_tools(fs: ScopedFs) -> Vec<ToolDefinition> {
|
||||
vec![read_tool(fs.clone()), write_tool(fs.clone()), ...]
|
||||
}
|
||||
```
|
||||
|
||||
## Bash ツール
|
||||
|
||||
- コマンド実行、タイムアウト、バックグラウンド実行をサポート
|
||||
- 作業ディレクトリは永続(ツール内部で状態保持)
|
||||
- Scope による保護は不可 → `PreToolCall` Hook + Permission ルールで制御
|
||||
- Permission チケット未実装の間は、ツール自体は登録可能だが制約なしで動作
|
||||
|
||||
## 依存関係
|
||||
|
||||
- `tools` クレートは `llm-worker`(Tool trait)と `manifest`(Scope)に依存
|
||||
- Permission による Bash 制御 → [permission-extension-point.md](permission-extension-point.md)
|
||||
|
||||
## 将来の拡張
|
||||
|
||||
- ScopedFs をスクリプティング言語ランタイムに公開し、ユーザー定義ツールからも同じ Scope 境界で fs 操作を可能にする
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
## 前提チケット
|
||||
|
||||
- [token-counter.md](token-counter.md) — LlmClient に Tokenizer 導入。retained_tokens / auto-read budget がこれに依存
|
||||
- [tool-output-referenced-files.md](tool-output-referenced-files.md) — ToolOutput にファイル追跡フィールド追加。デフォルトリファレンスがこれに依存
|
||||
- [tracker.md](tracker.md) — `ReadTracker` → `Tracker` リネーム + `recent_files(n)` 追加。デフォルトリファレンスがこれに依存
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -37,29 +37,100 @@
|
|||
|
||||
---
|
||||
|
||||
## 用語の定義(重要 — 混乱防止のため明記)
|
||||
|
||||
- **run = turn**: 同じ概念を指す。1 ユーザープロンプト → 完了までの単位
|
||||
- **リクエスト**: 1 run/turn 内で投げる個別の LLM 呼び出し。ツール使用で 1 turn に複数リクエストが発生する
|
||||
- **リクエストの合間** (between requests): 1 turn 内、次の LLM リクエストを投げる前の地点。`CompactInterceptor::pre_llm_request` で観測される
|
||||
- **ターンの合間** (between turns): turn が完了して次の turn を待つ状態。`Controller::try_post_run_compact` で観測される
|
||||
|
||||
この 2 つを区別することに意味がある:
|
||||
- **ターンの合間**は自然なタスクの区切り。次の turn に入る前に **先を見越して早めに** compact すべき
|
||||
- **リクエストの合間**は turn 内部の中継点。通常は proactive な必要はなく、暴走的な膨張を拾う **safety net** として **遅めに** 発動すれば十分
|
||||
|
||||
---
|
||||
|
||||
## 閾値の修正(重要)
|
||||
|
||||
現状の実装は閾値の大小関係が意図と逆になっている。修正する。
|
||||
現状の実装は:
|
||||
1. 閾値の大小関係が意図と逆
|
||||
2. `turn_threshold` が pre_llm_request 側で使われていて命名がミスリード
|
||||
3. もう片方を `turn_threshold * 9 / 8` で導出しているが、9/8 に根拠がない
|
||||
|
||||
これらをまとめて修正する。値入れ替え + リネーム + マニフェストで両閾値を個別指定。
|
||||
|
||||
### 正しい方針
|
||||
|
||||
- **post-run (タスク区切り) = 早めの閾値**: タスクの区切りで先を見越して compact
|
||||
- **mid-turn (pre_llm_request) = 遅めの閾値**: ターン中は最終防衛ラインとして、遅くなっても止まらないよう
|
||||
| チェックポイント | 変数名 (コード) | マニフェスト | 役割 |
|
||||
|----------------|---------------|------------|------|
|
||||
| `Controller::try_post_run_compact` (ターンの合間) | `post_run_threshold` | `compact_threshold` | proactive (小) |
|
||||
| `CompactInterceptor::pre_llm_request` (リクエストの合間) | `request_threshold` | `compact_request_threshold` | safety net (大) |
|
||||
|
||||
両方とも manifest で個別指定する。導出はしない。
|
||||
|
||||
```toml
|
||||
[compaction]
|
||||
compact_threshold = 80000 # ターンの合間, proactive
|
||||
compact_request_threshold = 90000 # リクエストの合間, safety net
|
||||
```
|
||||
manifest.compact_threshold → post_run_threshold (基本ライン, 早め)
|
||||
turn_threshold = post_run_threshold * 9 / 8 (safety net, 遅め)
|
||||
```
|
||||
|
||||
想定: `compact_threshold < compact_request_threshold`。逆転していてもエラーにはしないが、
|
||||
warn を出す。両方 None なら compact 無効(今まで通り)。片方だけ None なら...
|
||||
|
||||
**片方だけ指定されたときの挙動**:
|
||||
- `compact_threshold` のみ設定 → `compact_request_threshold` は無効 (リクエスト間チェック無し)
|
||||
- `compact_request_threshold` のみ設定 → `compact_threshold` は無効 (post_run チェック無し)
|
||||
- 両方設定 → 両方有効
|
||||
|
||||
→ `CompactState` 内部では `Option<u64>` 2 本持ち。`exceeds_*` メソッドは `Option` が `None` なら常に `false`。
|
||||
|
||||
### 影響箇所
|
||||
|
||||
- `crates/pod/src/compact_state.rs`
|
||||
- フィールド名と初期化を入れ替え: `manifest compact_threshold` は `post_run_threshold` に代入
|
||||
- `turn_threshold` は `post_run_threshold * 9 / 8` として導出
|
||||
- テストの `assert_eq!(state.post_run_threshold, 90_000)` を逆転(`turn_threshold = 90_000`, `post_run_threshold = 80_000` が正)
|
||||
- `crates/pod/src/compact_interceptor.rs` — そのまま(`exceeds_turn` を呼ぶだけ)
|
||||
- `crates/pod/src/pod.rs:371` の `exceeds_post_run` 判定 — そのまま
|
||||
- `docs/compaction.md` — 「ターン間は早めの閾値」の記述を逆に修正
|
||||
- **`crates/manifest/src/lib.rs`**
|
||||
- `CompactionConfig` に `compact_request_threshold: Option<u64>` フィールドを追加
|
||||
- デフォルトは `None`
|
||||
- テスト更新 (両閾値が読めること)
|
||||
|
||||
- **`crates/pod/src/compact_state.rs`**
|
||||
- `turn_threshold` フィールドを `request_threshold: Option<u64>` にリネーム + `Option` 化
|
||||
- `post_run_threshold: u64` → `Option<u64>` に変更
|
||||
- コンストラクタシグネチャ変更:
|
||||
```rust
|
||||
// Before
|
||||
pub fn new(turn_threshold: u64, retained_turns: usize) -> Self
|
||||
// After
|
||||
pub fn new(
|
||||
post_run_threshold: Option<u64>,
|
||||
request_threshold: Option<u64>,
|
||||
retained_turns: usize,
|
||||
) -> Self
|
||||
```
|
||||
- `exceeds_turn()` → `exceeds_request()` にリネーム。中身:
|
||||
```rust
|
||||
pub(crate) fn exceeds_request(&self) -> bool {
|
||||
self.request_threshold
|
||||
.map(|t| self.last_input_tokens() > t)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
```
|
||||
- `exceeds_post_run()` も同様に Option 対応
|
||||
- `turn_threshold()` getter → `request_threshold()`、戻り値は `Option<u64>`
|
||||
- ドックコメントを「proactive = post_run」「safety net = request」で書き直し
|
||||
- テスト: 両方設定/片方だけ/両方 None の 3 ケース
|
||||
|
||||
- **`crates/pod/src/compact_interceptor.rs`**
|
||||
- `exceeds_turn()` 呼び出しを `exceeds_request()` に
|
||||
- ログメッセージ "Between-turns ..." → "Between-requests ..."
|
||||
- コメント "Step 2: Check between-turns compaction threshold" → "Step 2: Check between-requests compaction threshold (safety net)"
|
||||
|
||||
- **`crates/pod/src/pod.rs`**
|
||||
- `ensure_interceptor_installed` で `compact_threshold` + `compact_request_threshold` の両方を manifest から読み、`CompactState::new` に渡す
|
||||
- wrap 条件: 両方 None なら CompactInterceptor を挟まない (+ Controller の post_run チェックも実質無効)。片方でも Some なら挟む
|
||||
- Disjoint チェックで `post_run_threshold > request_threshold` の場合 warn ログ
|
||||
|
||||
- **`docs/compaction.md`**
|
||||
- TOML 例に `compact_request_threshold` を追加
|
||||
- トリガーセクションから「9/8 で導出」の記述を削除、個別指定である旨に修正
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -130,9 +201,9 @@ token-counter チケットが前提。
|
|||
|
||||
### デフォルトリファレンスの抽出
|
||||
|
||||
Pod は `ToolOutput.referenced_files` を `HookInterceptor::post_tool_call` で観察し、
|
||||
LRU 的な履歴バッファに積む(→ tool-output-referenced-files チケット)。
|
||||
Compact 時は先頭 5 件を compact worker のデフォルトリファレンスとして渡す。
|
||||
`tools::Tracker` (既存の `ReadTracker` を拡張したもの → [tracker.md](tracker.md)) が
|
||||
Read/Write/Edit で触られたファイルを LRU で保持している。Compact 時は
|
||||
`self.tracker.recent_files(5)` で先頭 5 件を compact worker のデフォルトリファレンスとして渡す。
|
||||
|
||||
### compact worker のツール
|
||||
|
||||
|
|
@ -148,7 +219,7 @@ write_summary(text) — 構造化要約を出力/上書き
|
|||
|
||||
### フロー
|
||||
|
||||
1. Pod が referenced_files バッファから先頭 5 件を抽出(デフォルトリファレンス)
|
||||
1. Pod が `Tracker::recent_files(5)` で最近触られたファイルを抽出(デフォルトリファレンス)
|
||||
2. compact worker のプロンプトに含める:
|
||||
|
||||
```
|
||||
|
|
@ -270,17 +341,17 @@ pruned history から:
|
|||
|
||||
### Yield のタイミング精度
|
||||
|
||||
現状 `pre_llm_request`(リクエストの切れ目)でのみチェック。
|
||||
1ターン内でツール呼び出しが多く途中でコンテキストが膨らむケースは次のリクエストまで待つ。
|
||||
現状 `pre_llm_request`(リクエストの合間)でのみチェック。
|
||||
1 turn 内でツール呼び出しが多く途中でコンテキストが膨らむケースは次のリクエストまで待つ。
|
||||
|
||||
検討: `post_tool_call` でもチェックする?
|
||||
|
||||
### 閾値の比率
|
||||
### 閾値の推奨値
|
||||
|
||||
- `post_run_threshold` = マニフェストの `compact_threshold`
|
||||
- `turn_threshold` = `post_run_threshold * 9 / 8`(≈ 112.5%)
|
||||
- `compact_threshold` (post_run, proactive): モデルのコンテキスト上限の 70-80% あたりが目安
|
||||
- `compact_request_threshold` (request, safety net): `compact_threshold` より少し上、85-95% あたり
|
||||
|
||||
9/8 の根拠はない(安全マージン)。要調整。
|
||||
両方 manifest で個別指定する(導出はしない)。要調整の余地あり。
|
||||
|
||||
### Prune と Compact の相互作用
|
||||
|
||||
|
|
@ -302,12 +373,12 @@ compact 後の新セッションが存在する場合、どちらを restore す
|
|||
## 実装順序
|
||||
|
||||
0. **[前提] token-counter** — LlmClient に Tokenizer
|
||||
0. **[前提] tool-output-referenced-files** — ToolOutput + Pod の LRU バッファ
|
||||
1. **閾値逆転の修正** — `compact_state.rs` のフィールド入れ替え、テスト修正、docs 更新
|
||||
0. **[前提] tracker** — `ReadTracker` → `Tracker` リネーム + `recent_files` 追加 + Pod 接続
|
||||
1. **閾値の修正 + リネーム + 個別指定化** — manifest に `compact_request_threshold` 追加、`compact_state.rs` の 2 閾値を `Option<u64>` 化、`turn_threshold` → `request_threshold` リネーム、`exceeds_turn()` → `exceeds_request()`。compact_state.rs / compact_interceptor.rs / pod.rs / manifest / テスト / docs 更新
|
||||
2. **要約入力の削減** — `build_summary_prompt` から content/arguments/reasoning を除去
|
||||
3. **retained_tokens 化** — retained_turns → retained_tokens に変更。マニフェスト設定追加
|
||||
4. **compact worker のツール化** — read_file + mark_read_required + add_reference + write_summary (上書き可)
|
||||
5. **Auto-Read + リファレンス** — デフォルト5ファイル抽出 (referenced_files バッファから)、compact worker による選定、system message での注入
|
||||
5. **Auto-Read + リファレンス** — デフォルト5ファイル抽出 (`Tracker::recent_files` から)、compact worker による選定、system message での注入
|
||||
6. **Auto-Read Budget** — `mark_read_required` のトークン会計、残量通知、超過エラー
|
||||
7. **compact worker の累計入力トークン制限** — `compact_worker_max_input_tokens`
|
||||
8. **要約フォーマット** — タスク分類の要約プロンプト調整
|
||||
|
|
|
|||
86
tickets/scope-redesign.md
Normal file
86
tickets/scope-redesign.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Scope の再設計
|
||||
|
||||
## 背景
|
||||
|
||||
Scope は Pod の作業ディレクトリとアクセス権限を定義するもの。全ての Pod が必ず持つ。
|
||||
|
||||
現状の問題:
|
||||
1. `Pod.scope` が `Option<Scope>` — scope なしの Pod が存在しうる(ツールが登録されない)
|
||||
2. `Scope` が `root: PathBuf` しか持たない — 書き込み許可/禁止の概念がない
|
||||
3. Scope なしの場合、Glob/Grep のデフォルト検索パスがない
|
||||
4. マニフェストで `[scope]` 省略時のフォールバックが定義されていない
|
||||
|
||||
## 方針
|
||||
|
||||
### Scope の拡張
|
||||
|
||||
```rust
|
||||
pub struct Scope {
|
||||
pwd: PathBuf, // 作業ディレクトリ
|
||||
writable: bool, // false = 読み取り専用 Pod
|
||||
}
|
||||
```
|
||||
|
||||
- `writable: true`(デフォルト): Read/Write/Edit/Glob/Grep 全て使える
|
||||
- `writable: false`: Read/Glob/Grep のみ。Write/Edit はエラー
|
||||
- `pwd` は Glob/Grep のデフォルト検索パスとしても使われる
|
||||
|
||||
### Pod で必須化
|
||||
|
||||
```rust
|
||||
pub struct Pod<C, St> {
|
||||
scope: Scope, // Option ではない
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### マニフェスト
|
||||
|
||||
```toml
|
||||
# 明示指定
|
||||
[scope]
|
||||
pwd = "./src"
|
||||
writable = false # 省略時 true
|
||||
|
||||
# [scope] 省略時 → マニフェストファイルの親ディレクトリが pwd、writable = true
|
||||
```
|
||||
|
||||
`Pod::new()`(マニフェストなし構築)では Scope を引数で必須にする。
|
||||
|
||||
### ScopedFs の変更
|
||||
|
||||
```rust
|
||||
pub fn write(&self, path: &Path, content: &[u8]) -> Result<WriteOutcome, ToolsError> {
|
||||
if !self.inner.scope.writable() {
|
||||
return Err(ToolsError::ReadOnly);
|
||||
}
|
||||
if !self.inner.scope.contains(path) {
|
||||
return Err(ToolsError::OutOfScope(path.to_path_buf()));
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 統合
|
||||
|
||||
```rust
|
||||
// 今: scope が Some のときだけツール登録
|
||||
if let Some(scope) = scope_for_tools { ... }
|
||||
|
||||
// 後: 常にツール登録(scope は必須)
|
||||
let fs = tools::ScopedFs::new(pod.scope().clone());
|
||||
let tracker = tools::ReadTracker::new();
|
||||
worker.register_tools(tools::builtin_tools(fs, tracker));
|
||||
```
|
||||
|
||||
## 影響範囲
|
||||
|
||||
- `manifest::Scope` — `root` → `pwd`、`writable` フィールド追加
|
||||
- `manifest::ScopeConfig` — `root` → `pwd`、`writable` の serde 対応
|
||||
- `manifest::PodManifest` — [scope] 省略時のフォールバック解決
|
||||
- `Pod` — `scope: Option<Scope>` → `scope: Scope`
|
||||
- `Pod::new()`, `Pod::restore()`, `Pod::from_manifest()` — シグネチャ変更
|
||||
- `ScopedFs` — writable チェック追加
|
||||
- `Controller` — scope の Optional 分岐を削除
|
||||
- `tools::error::ToolsError` — `ReadOnly` variant 追加
|
||||
- `Scope::contains` — `root` → `pwd` のリネーム
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
# ツール出力の設計
|
||||
|
||||
## 課題
|
||||
|
||||
ツール実行結果(ファイル内容、検索結果等)はサイズが予測不能で、
|
||||
全量を LLM コンテキストに載せるとトークン消費が爆発する。
|
||||
|
||||
## 方針
|
||||
|
||||
ツール出力を **summary(常駐)** と **content(prunable)** の2フィールドに分離する。
|
||||
|
||||
- summary: 1-2行。常に history に残る。Prune 後もこれだけで「何をしたか」がわかる
|
||||
- content: 詳細な出力。一定閾値まで。Prune で消える
|
||||
|
||||
巨大な出力(大量の grep 結果、巨大ファイル等)はフレームワークの責務外。
|
||||
ツール側がファイルに書き出し、content に見取り図を置く。
|
||||
|
||||
## データ型
|
||||
|
||||
### ToolOutput
|
||||
|
||||
```rust
|
||||
/// ツール実行結果。
|
||||
///
|
||||
/// summary は常に必須。content は省略可能。
|
||||
/// Prune 時に content が除去され、summary だけが残る。
|
||||
pub struct ToolOutput {
|
||||
/// 1-2行の要約。Prune 後も history に残る。
|
||||
/// 例: "read_file: src/main.rs — 42 lines"
|
||||
/// 例: "bash: cargo test — exit 0, 3 passed"
|
||||
/// 例: "grep: TODO in src/ — 128 hits, saved to /tmp/grep_result.txt"
|
||||
pub summary: String,
|
||||
|
||||
/// 詳細な出力内容。Prune で消える。
|
||||
/// None の場合、summary のみが history に載る。
|
||||
pub content: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### Item::ToolResult
|
||||
|
||||
```rust
|
||||
Item::ToolResult {
|
||||
id: Option<ItemId>,
|
||||
call_id: CallId,
|
||||
/// 1-2行の要約。Prune 後も残る。
|
||||
summary: String,
|
||||
/// 詳細な出力。Prune で None に置換される。
|
||||
content: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
LLM への送信時は summary + content を結合して単一文字列にする。
|
||||
content が None の場合は summary のみ。
|
||||
|
||||
```rust
|
||||
impl Item {
|
||||
/// LLM に送信する出力文字列を構築。
|
||||
pub fn tool_result_text(&self) -> Option<&str> {
|
||||
match self {
|
||||
Item::ToolResult { summary, content: Some(c), .. } => {
|
||||
// 呼び出し側で結合
|
||||
None // 実際は format!("{summary}\n{c}")
|
||||
}
|
||||
Item::ToolResult { summary, content: None, .. } => Some(summary),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tool trait の変更
|
||||
|
||||
`Tool::execute()` の戻り値を `Result<ToolOutput, ToolError>` に変更する。
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Tool: Send + Sync {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError>;
|
||||
}
|
||||
```
|
||||
|
||||
ツールが独自の summary を付けたい場合は `ToolOutput` を直接構築する。
|
||||
単純なケースでは `From<String>` で自動変換できる: `Ok("result".to_string().into())`
|
||||
|
||||
### From\<String\> 変換
|
||||
|
||||
`From<String>` による自動変換:
|
||||
|
||||
```rust
|
||||
impl From<String> for ToolOutput {
|
||||
fn from(s: String) -> Self {
|
||||
if s.len() <= SUMMARY_THRESHOLD {
|
||||
// 小さい出力: summary のみ(content なし)
|
||||
ToolOutput { summary: s, content: None }
|
||||
} else {
|
||||
// summary = 先頭行 + メタ情報
|
||||
let lines = s.lines().count();
|
||||
let first_line: String = s.lines().next()
|
||||
.unwrap_or("")
|
||||
.chars().take(80)
|
||||
.collect();
|
||||
let summary = format!("{lines} lines | {first_line}…");
|
||||
ToolOutput { summary, content: Some(s) }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`SUMMARY_THRESHOLD`: summary のみで十分な小さい出力の閾値。
|
||||
具体値は調整するが、数百バイト程度を想定。
|
||||
|
||||
## Prune との関係
|
||||
|
||||
```
|
||||
ツール実行
|
||||
→ ToolOutput { summary, content }
|
||||
→ Item::ToolResult { summary, content } ← history に追加
|
||||
|
||||
─── 数ターン経過 ───
|
||||
|
||||
Prune(pre_llm_request フック)
|
||||
→ Item::ToolResult { summary, content: None } ← content を除去
|
||||
```
|
||||
|
||||
Prune の実装は `content = None` にするだけ。
|
||||
|
||||
prunable トークン数の推定:
|
||||
- `content.as_ref().map(|c| c.len() / 4).unwrap_or(0)`
|
||||
|
||||
## 巨大出力の扱い
|
||||
|
||||
フレームワークは巨大出力を特別扱いしない。
|
||||
ツール側が自分で判断して対処する。
|
||||
|
||||
```
|
||||
巨大な grep 結果 → ツールがファイルに書き出す
|
||||
→ summary: "grep: TODO in src/ — 128 hits"
|
||||
→ content: ファイルパス + ヒット数の内訳(見取り図)
|
||||
|
||||
巨大なファイル読み取り → ツールが部分読み取りを提案
|
||||
→ summary: "read_file: data.csv — 50,000 lines"
|
||||
→ content: 先頭 N 行 + 末尾 M 行
|
||||
```
|
||||
|
||||
LLM が詳細を見たい場合は、read_file / grep 等の汎用ツールで
|
||||
ファイルを直接参照する。専用の inspect ツールは不要。
|
||||
|
||||
## 削除対象(旧設計からの移行)
|
||||
|
||||
| モジュール | 理由 |
|
||||
|---|---|
|
||||
| `ToolOutput` enum(Inline/Stored) | struct に置換 |
|
||||
| `Content` enum(Text/Structured) | 不要 |
|
||||
| `auto_summarize` / `auto_summarize_text` / `auto_summarize_structured` | 不要 |
|
||||
| `ToolOutputProcessor` trait | 不要 |
|
||||
| `BlobOutputProcessor` | 不要 |
|
||||
| `BlobStore` trait / `FsBlobStore` | 不要 |
|
||||
| `inspect_tool.rs` | 不要 |
|
||||
| Worker の `output_processor` フィールド | 不要 |
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
# ToolOutput.referenced_files: ツールが参照したファイルの追跡
|
||||
|
||||
## 背景
|
||||
|
||||
Compact 実行時、「タスク続行に必要なファイル」を compact worker に提示するため
|
||||
のデフォルトリファレンス(過去に読み書きされたファイル一覧)が必要。
|
||||
builtin-tools は実装済みだが、Pod 側でファイル操作を追跡する口がない。
|
||||
|
||||
ツール名ベースのヒューリスティック検出("read" / "edit" / "write" の名前で判別)は
|
||||
脆弱でユーザー定義ツールに対応できない。各ツールが自己申告する形にする。
|
||||
|
||||
## 方針
|
||||
|
||||
`ToolOutput` に optional な `referenced_files: Vec<PathBuf>` を追加し、ツール自身が
|
||||
「この実行で触ったファイル」を申告する。Pod の `HookInterceptor::post_tool_call` で
|
||||
観察し、Pod 内の LRU 的な履歴に積む。Compact 時はその履歴から先頭 N 件を compact
|
||||
worker のデフォルトリファレンスとして渡す。
|
||||
|
||||
## 実装
|
||||
|
||||
### llm-worker: ToolOutput 拡張
|
||||
|
||||
```rust
|
||||
pub struct ToolOutput {
|
||||
pub summary: String,
|
||||
pub content: Option<String>,
|
||||
pub referenced_files: Vec<PathBuf>, // NEW. 空なら未申告
|
||||
}
|
||||
```
|
||||
|
||||
- `From<String>` 実装では空の Vec を入れる
|
||||
- 既存のツール実装は変更不要(デフォルトで空)
|
||||
- `ToolResultInfo` (`post_tool_call` Hook が受ける型)にも伝播させて Pod から見えるようにする
|
||||
|
||||
### builtin-tools: 各ツールで埋める
|
||||
|
||||
既に実装済みの以下のツールに `referenced_files` を埋める変更を入れる:
|
||||
|
||||
| ツール | 申告するファイル |
|
||||
|--------|------------------|
|
||||
| Read | 読んだファイルパス |
|
||||
| Write | 書いたファイルパス |
|
||||
| Edit | 編集したファイルパス |
|
||||
| Glob | (積極的には埋めない — マッチしたファイル全部は多すぎる) |
|
||||
| Grep | (同上) |
|
||||
| Bash | (不可能。空のまま) |
|
||||
|
||||
Glob/Grep は「発見」しかしていないので referenced とは扱わない。
|
||||
|
||||
### Pod: 追跡と取り出し
|
||||
|
||||
`Pod` 内に履歴バッファ:
|
||||
|
||||
```rust
|
||||
referenced_files: VecDeque<PathBuf>, // 最新 N 件, 重複排除
|
||||
```
|
||||
|
||||
- `HookInterceptor::post_tool_call` で `ToolResultInfo.referenced_files` を受け取り、
|
||||
既存エントリを先頭に移動 or 新規追加(LRU 的な挙動)
|
||||
- 容量上限(例: 20)を超えたら末尾を落とす
|
||||
- Compact 開始時に先頭 5 件を取り出して compact worker に渡す
|
||||
|
||||
## 設計ポイント
|
||||
|
||||
- **ツールが自己申告**: 外部から名前で判別しない。拡張性のため
|
||||
- **Vec<PathBuf> なので 0〜複数件**: 複数ファイルに触るツール (例: 大量置換) にも対応
|
||||
- **空 Vec 許容**: Bash や Glob のような「追跡不能 or 不適切」なツールはそのまま空
|
||||
- **Pod が LRU バッファを持つ**: Compact 時の抽出を O(1) に近づける。Worker 層は関知しない
|
||||
|
||||
## 依存
|
||||
|
||||
- builtin-tools が実装済み前提(チケット: [builtin-tools.md](builtin-tools.md) 完了後)
|
||||
|
||||
## ブロックする後続
|
||||
|
||||
- [compact-improvements.md](compact-improvements.md) — デフォルトリファレンスの抽出がこのフィールドに依存
|
||||
112
tickets/tracker.md
Normal file
112
tickets/tracker.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Tracker: ReadTracker のリネームと機能追加
|
||||
|
||||
## 背景
|
||||
|
||||
`tools::ReadTracker` は既に Read/Write/Edit のすべてでファイル操作を記録している
|
||||
(`record()` が各ツールから呼ばれる)。名前に反して「read 以外も追跡している」状態。
|
||||
|
||||
また compact の改善 ([compact-improvements.md](compact-improvements.md)) で
|
||||
「最近触られたファイル一覧」をデフォルトリファレンスとして compact worker に渡したい
|
||||
要求があり、既存の Tracker を拡張すれば自然に解決する。
|
||||
|
||||
## 方針
|
||||
|
||||
1. `ReadTracker` → `Tracker` にリネーム (crate 全体)
|
||||
2. 順序付き (recency) の履歴を追加
|
||||
3. `recent_files(n)` メソッドで最近 N 件を取得できるようにする
|
||||
4. Pod が Tracker を保持して compact 時に参照
|
||||
|
||||
## 実装
|
||||
|
||||
### リネーム
|
||||
|
||||
- `crates/tools/src/read_tracker.rs` → `crates/tools/src/tracker.rs`
|
||||
- `pub struct ReadTracker` → `pub struct Tracker`
|
||||
- `crates/tools/src/lib.rs` の pub 再公開 (`pub use read_tracker::ReadTracker` → `pub use tracker::Tracker`)
|
||||
- crate 内呼び出し側 (`read.rs`, `write.rs`, `edit.rs`, `scoped_fs.rs` など)
|
||||
- テスト (`tests/integration.rs`, `tests/edge_cases.rs`)
|
||||
- `crates/pod/src/controller.rs:172` の `tools::ReadTracker::new()` → `tools::Tracker::new()`
|
||||
|
||||
### 内部構造の変更
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Tracker {
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
hashes: HashMap<PathBuf, ContentHash>,
|
||||
recency: VecDeque<PathBuf>, // 先頭が最新
|
||||
}
|
||||
|
||||
const RECENCY_CAPACITY: usize = 20;
|
||||
```
|
||||
|
||||
### `record()` の挙動追加
|
||||
|
||||
既存の hash 記録に加えて:
|
||||
|
||||
```rust
|
||||
pub fn record(&self, path: &Path, bytes: &[u8]) {
|
||||
let key = canonicalize_or_owned(path);
|
||||
let hash = hash_bytes(bytes);
|
||||
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.hashes.insert(key.clone(), hash);
|
||||
|
||||
// LRU: 既存エントリを除去 → 先頭に push
|
||||
inner.recency.retain(|p| p != &key);
|
||||
inner.recency.push_front(key);
|
||||
if inner.recency.len() > RECENCY_CAPACITY {
|
||||
inner.recency.pop_back();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 新メソッド
|
||||
|
||||
```rust
|
||||
/// Return up to `n` most recently recorded file paths.
|
||||
/// Order: most recent first.
|
||||
pub fn recent_files(&self, n: usize) -> Vec<PathBuf> {
|
||||
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
inner.recency.iter().take(n).cloned().collect()
|
||||
}
|
||||
```
|
||||
|
||||
### Pod への接続
|
||||
|
||||
- `Pod` に `tracker: Option<tools::Tracker>` フィールド追加 (builtin-tools 未登録の場合は None)
|
||||
- Controller が `Tracker::new()` した時点で Pod にも `attach_tracker(tracker.clone())` で共有
|
||||
- Compact 実行時 (`Pod::compact` 内) に `self.tracker.as_ref().map(|t| t.recent_files(5))` でデフォルトリファレンスを取得
|
||||
|
||||
### ライフサイクルの整合性
|
||||
|
||||
既存のドキュメント: 「Tracker は session-scoped。Controller spawn ごとに new」
|
||||
この方針は維持。compact 後も同じ Controller spawn 内で状態が継続するので、
|
||||
compact worker が `read_file` で追加で参照したファイルも次回 compact 時に効く。
|
||||
|
||||
### テスト追加
|
||||
|
||||
- `recent_files` が recency 順で返ること
|
||||
- `RECENCY_CAPACITY` を超えた場合に古いものが落ちること
|
||||
- 既存パスを再 record したら先頭に移動すること
|
||||
- Read/Write/Edit 実行後に recent_files に現れること (integration テスト)
|
||||
|
||||
## 影響範囲
|
||||
|
||||
- `crates/tools/src/read_tracker.rs` — リネーム + Inner 構造体化 + recency フィールド + recent_files メソッド
|
||||
- `crates/tools/src/lib.rs` — pub use 修正
|
||||
- `crates/tools/src/{read,write,edit,scoped_fs}.rs` — 型名追従
|
||||
- `crates/tools/tests/*` — 型名追従 + recent_files のテスト追加
|
||||
- `crates/pod/src/pod.rs` — `tracker` フィールド + `attach_tracker` メソッド
|
||||
- `crates/pod/src/controller.rs` — `tracker.clone()` を Pod にも渡す
|
||||
|
||||
## 依存
|
||||
|
||||
- なし (単独で実装可能)
|
||||
|
||||
## ブロックする後続
|
||||
|
||||
- [compact-improvements.md](compact-improvements.md) — デフォルトリファレンスの抽出がこれに依存
|
||||
Loading…
Reference in New Issue
Block a user