チケット更新

This commit is contained in:
Keisuke Hirata 2026-04-13 04:10:19 +09:00
parent 3d0d5ffe85
commit 5bc4a6d6d6
8 changed files with 327 additions and 355 deletions

View File

@ -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
View 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 コマンド制御

View File

@ -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(&params.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 操作を可能にする

View File

@ -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
View 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` のリネーム

View File

@ -1,160 +0,0 @@
# ツール出力の設計
## 課題
ツール実行結果(ファイル内容、検索結果等)はサイズが予測不能で、
全量を LLM コンテキストに載せるとトークン消費が爆発する。
## 方針
ツール出力を **summary常駐****contentprunable** の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 に追加
─── 数ターン経過 ───
Prunepre_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` enumInline/Stored | struct に置換 |
| `Content` enumText/Structured | 不要 |
| `auto_summarize` / `auto_summarize_text` / `auto_summarize_structured` | 不要 |
| `ToolOutputProcessor` trait | 不要 |
| `BlobOutputProcessor` | 不要 |
| `BlobStore` trait / `FsBlobStore` | 不要 |
| `inspect_tool.rs` | 不要 |
| Worker の `output_processor` フィールド | 不要 |

View File

@ -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
View 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) — デフォルトリファレンスの抽出がこれに依存