From 3c510860fa6e52592f90786dbf2992f5d203d334 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 19 Apr 2026 12:13:03 +0900 Subject: [PATCH] =?UTF-8?q?compact-improvements=20=E3=83=81=E3=82=B1?= =?UTF-8?q?=E3=83=83=E3=83=88=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 - tickets/compact-improvements.md | 418 -------------------------------- 2 files changed, 419 deletions(-) delete mode 100644 tickets/compact-improvements.md diff --git a/TODO.md b/TODO.md index 155a9a8b..f7d30343 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,6 @@ - [ ] 引数なし tool 呼び出しで `arguments = "null"` が記録される不具合 → [tickets/tool-call-empty-args-null.md](tickets/tool-call-empty-args-null.md) - [ ] ツール設計 - [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md) -- [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) - [ ] Pod オーケストレーション diff --git a/tickets/compact-improvements.md b/tickets/compact-improvements.md deleted file mode 100644 index 5be9a8c6..00000000 --- a/tickets/compact-improvements.md +++ /dev/null @@ -1,418 +0,0 @@ -# Compact の改善 - -## 背景 - -`Pod::compact()` とその周辺機構は実装済み。 -要約品質、保護単位、compact 後のコンテキスト構築に改善が必要。 - -## 前提(完了済み) - -以下は完了済み。git 履歴参照。 - -- **usage-history** — session-store に `UsageRecord` を積む基盤(`101679d usageデータの永続化実装`) -- **token-counter** — Usage 履歴ベースのトークン会計。`Pod::total_tokens()` / `Pod::split_for_retained(n)` として公開済み(`a89c448 token-counter実装`) -- **tracker** — `tools::Tracker` に `recent_files(n)` 実装済み(`6f2362e ToolsのTracker実装`) - ---- - -## 要件 - -### R1: 一貫した振る舞い -- システムプロンプトは不変 -- compact 前後でユーザーが違和感を覚えない -- 「何を知っていて何を忘れたか」が自然であること - -### R2: 直近の記憶の確実性 -- 直近 N トークン分の会話をそのまま保持(Prune 済み = summary only の状態で計測) -- **トークン数ベース** で保護量を決める(ターン単位ではない) - - 自走エージェントは1ターン内で多数のリクエストを回す - - ターン単位だと保護量がターン長に依存してしまう - -### R3: Auto-Read + リファレンス -- compact 後の最初のターンで、タスク遂行に必要なファイルが既に読まれている -- 2段階: **Read**(全文/範囲をコンテキストに注入)と **Reference**(「読んだことがある」とだけ伝える) -- compact worker が「続行に必要なファイル」を判断して指定する - -### R4: マルチタスク対応 -- セッション中に一貫した課題に取り組んでいないものとする -- **完了タスク**: 簡潔に。注意点・発覚した事実だけ -- **進行中タスク**: サマリ + 現状 + 次のステップを十分に - ---- - -## 用語の定義(重要 — 混乱防止のため明記) - -- **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 に根拠がない - -これらをまとめて修正する。値入れ替え + リネーム + マニフェストで両閾値を個別指定。 - -### 正しい方針 - -| チェックポイント | 変数名 (コード) | マニフェスト | 役割 | -|----------------|---------------|------------|------| -| `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 -``` - -想定: `compact_threshold < compact_request_threshold`。逆転していてもエラーにはしないが、 -warn を出す。両方 None なら compact 無効(今まで通り)。片方だけ None なら... - -**片方だけ指定されたときの挙動**: -- `compact_threshold` のみ設定 → `compact_request_threshold` は無効 (リクエスト間チェック無し) -- `compact_request_threshold` のみ設定 → `compact_threshold` は無効 (post_run チェック無し) -- 両方設定 → 両方有効 - -→ `CompactState` 内部では `Option` 2 本持ち。`exceeds_*` メソッドは `Option` が `None` なら常に `false`。 - -### 占有量ソースの統合(重要) - -現在 `CompactState::last_input_tokens: AtomicU64` が `on_usage` callback から -更新され、閾値判定に使われている。これは session-store の `UsageRecord` -履歴(usage-history で導入済み)と**情報源が二重化**している状態。 - -本チケットで両者を統合する。**`Pod::total_tokens()`(token-counter で導入済み)を -単一の情報源とし、`last_input_tokens` 経路を撤去する**: - -- `CompactState` から `last_input_tokens: AtomicU64` フィールドを削除 -- `CompactState::update_input_tokens` メソッドを削除 -- `Pod::ensure_interceptor_installed` の on_usage callback から - `state_for_usage.update_input_tokens(tokens)` の行を削除 - (`tracker_for_usage.record_usage(event)` だけが残る) -- 閾値判定 (`exceeds_request` / `exceeds_post_run`) は `Pod::total_tokens()` の - 戻り値を見る形に変える -- これにより「実測値の単一履歴 → `Pod::total_tokens()` → 閾値判定」と一直線になる - -Anthropic のキャッシュヒット時に占有量を取りこぼす旧バグも、このパスを -廃止することで自動的に解消する(`UsageRecord.input_total_tokens` は -scheme 層で占有量に正規化済み)。 - -### 影響箇所 - -- **`crates/manifest/src/lib.rs`** - - `CompactionConfig` に `compact_request_threshold: Option` フィールドを追加 - - デフォルトは `None` - - テスト更新 (両閾値が読めること) - -- **`crates/pod/src/compact_state.rs`** - - `last_input_tokens: AtomicU64` フィールドを **削除**(情報源を usage_history に一本化) - - `update_input_tokens` / `last_input_tokens` メソッドも削除 - - `turn_threshold` フィールドを `request_threshold: Option` にリネーム + `Option` 化 - - `post_run_threshold: u64` → `Option` に変更 - - コンストラクタシグネチャ変更: - ```rust - // Before - pub fn new(turn_threshold: u64, retained_turns: usize) -> Self - // After - pub fn new( - post_run_threshold: Option, - request_threshold: Option, - retained_turns: usize, - ) -> Self - ``` - - `exceeds_turn()` → `exceeds_request()` にリネーム。閾値超過判定は - 呼び出し側で現在の占有量を渡す形に変える(CompactState は閾値しか持たない): - ```rust - pub(crate) fn exceeds_request(&self, current_tokens: u64) -> bool { - self.request_threshold - .map(|t| current_tokens > t) - .unwrap_or(false) - } - ``` - 呼び出し元 (`compact_interceptor.rs` / `controller.rs`) は `Pod::total_tokens()` - から現在の占有量を取って渡す - - `exceeds_post_run()` も同様に Option 対応 - - `turn_threshold()` getter → `request_threshold()`、戻り値は `Option` - - ドックコメントを「proactive = post_run」「safety net = request」で書き直し - - テスト: 両方設定/片方だけ/両方 None の 3 ケース - -- **`crates/pod/src/pod.rs`** (上記の compact_state 変更に伴って) - - `ensure_interceptor_installed` の on_usage callback から - `state_for_usage.update_input_tokens(tokens)` の行を削除。 - `tracker_for_usage.record_usage(event)` だけが残る - -- **`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 で導出」の記述を削除、個別指定である旨に修正 - ---- - -## compact 後の history 構造 - -全て system message(`Item::Message { role: System }`)として注入。 - -``` -[system prompt] ← 不変 (R1) -[system: 構造化要約] ← R4: compact worker の出力 -[system: auto-read ファイル群] ← R3: read_required の結果 -[system: リファレンス一覧] ← R3: reference の結果 -[直近 N トークン分の生の会話] ← R2: pruned 状態で保持 -``` - -system message で統一する理由: -- LLM に「システムから提供された前提情報」として認識させる -- fake ユーザーメッセージや fake ToolCall を作らない -- 要約もファイルも同じ role で自然に並ぶ - -### auto-read の system message 例 - -``` -[Auto-read file: src/main.rs:42-142] -fn main() { - let config = Config::load(); - ... -} -``` - -### リファレンスの system message 例 - -``` -[Referenced files — read before compaction, contents not included] -- src/config.rs (read during task setup) -- tests/integration_test.rs (read during test implementation) -Use read_file to access current contents if needed. -``` - ---- - -## R2: トークンベースの保護 - -現状の `retained_turns` を `retained_tokens` に変更。 - -``` -history (全て pruned 済み = summary only): - [...古い部分...] [...直近 N トークン分...] - ↓ ↓ - 要約対象 そのまま新 history に載せる -``` - -- Prune 済みの history に対して `Pod::split_for_retained(N)`(token-counter で - 導入済み)で cut 位置を求める -- 計算は session-store の `UsageRecord` 履歴 (実測値) を逆算ソースに使う -- ターン境界は無視。アイテム単位で切る - -```toml -[compaction] -compact_threshold = 80000 -retained_tokens = 8000 # ← retained_turns から変更 -``` - ---- - -## R3: Auto-Read + リファレンス - -### デフォルトリファレンスの抽出 - -`tools::Tracker`(実装済み)が Read/Write/Edit で触られたファイルを LRU で -保持している。Compact 時は `self.tracker.recent_files(5)` で先頭 5 件を -compact worker のデフォルトリファレンスとして渡す。 - -### compact worker のツール - -``` -read_file(path, offset?, limit?) — ファイルを読んで判断するため -mark_read_required(path, offset?, limit?) — auto-read 対象(内容をコンテキストに載せる) -add_reference(path) — リファレンス追加(内容は載せない) -write_summary(text) — 構造化要約を出力/上書き(上書き可) -``` - -`write_summary` は**上書き可**。マルチターンで「下書き → 追加 read → 書き直し」の順序が自然に動く。 -最終的に直近の呼び出しが採用される。ガードは「一度も呼ばれていない」時のみ。 - -### フロー - -1. Pod が `Tracker::recent_files(5)` で最近触られたファイルを抽出(デフォルトリファレンス) -2. compact worker のプロンプトに含める: - - ``` - 以下のファイルがリファレンスとして指定されています。 - 全て読んで、タスク続行に必要なものを mark_read_required で指定してください。 - リファレンスを追加したい場合は add_reference で追加できます。 - ``` - -3. compact worker が read_file で全ファイルを読み、判断: - - 必要なファイル → `mark_read_required(path, offset?, limit?)` - - 不要だがコンテキストとして有用 → リファレンスのまま残す - - 追加のリファレンス → `add_reference(path)` -4. `write_summary` で構造化要約を出力(最後のが採用される) -5. ターン終了時に summary が一度も書かれていない or read_required が空(かつファイル操作履歴がある場合)→ 追加プロンプトで促す - -### Auto-Read の Budget 管理 - -compact worker が `mark_read_required` を無制限に呼ぶとコンテキストが膨張する。 -共有 budget で制御: - -```toml -[compaction] -auto_read_budget = 8000 # 合計トークン上限 -``` - -- `mark_read_required` のツール結果で残量を返す: - `"Marked. Budget: 4200/8000 tokens remaining"` -- 50% 以下になったら次のツール結果に system reminder を append: - `"Budget half consumed. Consider calling write_summary soon."` -- 100% 超過で Err: - `"Error: auto-read budget exhausted (8000 tokens). Remove an existing mark or use add_reference instead."` -- compact worker が判断して自分で調整できる(Err は即中断ではない) - -### compact worker の暴走抑止 - -Turn/request 数ではなく、compact worker の累計入力トークンで上限を設ける: - -```toml -[compaction] -compact_worker_max_input_tokens = 50000 -``` - -超えたら compact worker を強制終了。`CompactState::record_compact_failure()` 経由で -サーキットブレーカーの自然な経路に乗る。 - ---- - -## R4: 要約の内容と品質 - -### 出力方法 - -compact worker が `write_summary(text)` ツールで出力する(上書き可)。 -最後のテキスト出力ではなくツールにする理由: -- マルチターンで read_file → 判断 → 要約の順序が自由 -- 要約を書いた後にさらにリファレンスを追加できる -- 「要約を書いていない」のガードが mark_read_required と同じパターンで検出可能 - -### 含めるべき内容 - -コードスニペットは auto-read に任せる。要約に求めるのは: -1. **何を、なぜやったか** — 意思決定の記録。具体的な型名・関数名で言及 -2. **ユーザーの指示・フィードバックの原文** — ニュアンス保持。重要なもののみ -3. **発生した問題と解決策** — 同じ轍を踏まない -4. **今どこにいて次に何をするか** — compact 前後の一貫性 (R1) - -含めないもの: -- コードの全文(auto-read が担う) -- 変更の diff(git がある) -- 中間のやりとりの詳細(最終結論だけ) - -### フォーマット(5セクション、1000-2000 トークン目安) - -``` -## Completed Tasks -### (タスク名) -- 完了した作業(具体的な型名・ファイル名で) -- 注意点 / 発覚した事実 - -### (タスク名) -- ... - -## Active Task -### (タスク名) -- 目標 -- 現状(何が済んで何が未着手か) -- 次のステップ - -## Key Decisions -- (判断内容) — (理由) -- ... - -## User Directives -- 「(ユーザー発言の原文)」 — 重要な指示・フィードバックのみ -- ... - -## Current Work -(直前に何をしていたか。2-3行) -``` - -各セクションの目安量: -- Completed Tasks: 各タスク 2-3 行 × タスク数 -- Active Task: 5-10 行 -- Key Decisions: 各 1-2 行 -- User Directives: 重要な発言のみ原文引用 -- Current Work: 2-3 行 - -### 要約の入力 - -pruned history から: -- ToolResult は summary のみ(content 除去) -- ToolCall は名前のみ(arguments 除去) -- Reasoning は除去 - ---- - -## 挙動の未決定事項 - -### Yield のタイミング精度 - -現状 `pre_llm_request`(リクエストの合間)でのみチェック。 -1 turn 内でツール呼び出しが多く途中でコンテキストが膨らむケースは次のリクエストまで待つ。 - -検討: `post_tool_call` でもチェックする? - -### 閾値の推奨値 - -- `compact_threshold` (post_run, proactive): モデルのコンテキスト上限の 70-80% あたりが目安 -- `compact_request_threshold` (request, safety net): `compact_threshold` より少し上、85-95% あたり - -両方 manifest で個別指定する(導出はしない)。要調整の余地あり。 - -### Prune と Compact の相互作用 - -Prune はリクエストコンテキストのみ操作。閾値判定は usage_history の最新 -測定値(前回の LLM レスポンス時点の占有量)を見るので、Prune の効果は -次回 LLM call まで反映されない。保守的(compact しすぎる方向)で実害は小さい。 - -### compact 中のクライアント通知 - -Protocol チケットの `CompactStart`/`CompactDone` で対応。 - -### 復元時の挙動 - -`Outcome::Yielded` で記録されたセッションは `last_run_interrupted = true` で復元。 -compact 後の新セッションが存在する場合、どちらを restore するかは呼び出し側の責任。 -`compacted_from` で辿れる。 - ---- - -## 実装順序 - -前提(usage-history / token-counter / tracker)は完了済み。 - -1. **閾値の修正 + リネーム + 個別指定化 + 占有量ソース統合** — manifest に `compact_request_threshold` 追加、`compact_state.rs` の 2 閾値を `Option` 化、`turn_threshold` → `request_threshold` リネーム、`exceeds_turn()` → `exceeds_request()`。`last_input_tokens` 撤去、閾値判定は `Pod::total_tokens()` 経由に切替。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ファイル抽出 (`Tracker::recent_files` から)、compact worker による選定、system message での注入 -6. **Auto-Read Budget** — `mark_read_required` のトークン会計、残量通知、超過エラー -7. **compact worker の累計入力トークン制限** — `compact_worker_max_input_tokens` -8. **要約フォーマット** — タスク分類の要約プロンプト調整 -9. **ガード** — write_summary 未呼び出し or mark_read_required 空時の追加プロンプト