yoi/tickets/compact-improvements.md

315 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Compact の改善
## 背景
`Pod::compact()` とその周辺機構は実装済み。
要約品質、保護単位、compact 後のコンテキスト構築に改善が必要。
## 前提チケット
- [token-counter.md](token-counter.md) — LlmClient に Tokenizer 導入。retained_tokens / auto-read budget がこれに依存
- [tool-output-referenced-files.md](tool-output-referenced-files.md) — ToolOutput にファイル追跡フィールド追加。デフォルトリファレンスがこれに依存
---
## 要件
### R1: 一貫した振る舞い
- システムプロンプトは不変
- compact 前後でユーザーが違和感を覚えない
- 「何を知っていて何を忘れたか」が自然であること
### R2: 直近の記憶の確実性
- 直近 N トークン分の会話をそのまま保持Prune 済み = summary only の状態で計測)
- **トークン数ベース** で保護量を決める(ターン単位ではない)
- 自走エージェントは1ターン内で多数のリクエストを回す
- ターン単位だと保護量がターン長に依存してしまう
### R3: Auto-Read + リファレンス
- compact 後の最初のターンで、タスク遂行に必要なファイルが既に読まれている
- 2段階: **Read**(全文/範囲をコンテキストに注入)と **Reference**(「読んだことがある」とだけ伝える)
- compact worker が「続行に必要なファイル」を判断して指定する
### R4: マルチタスク対応
- セッション中に一貫した課題に取り組んでいないものとする
- **完了タスク**: 簡潔に。注意点・発覚した事実だけ
- **進行中タスク**: サマリ + 現状 + 次のステップを十分に
---
## 閾値の修正(重要)
現状の実装は閾値の大小関係が意図と逆になっている。修正する。
### 正しい方針
- **post-run (タスク区切り) = 早めの閾値**: タスクの区切りで先を見越して compact
- **mid-turn (pre_llm_request) = 遅めの閾値**: ターン中は最終防衛ラインとして、遅くなっても止まらないよう
```
manifest.compact_threshold → post_run_threshold (基本ライン, 早め)
turn_threshold = post_run_threshold * 9 / 8 (safety net, 遅め)
```
### 影響箇所
- `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` — 「ターン間は早めの閾値」の記述を逆に修正
---
## 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 に対して `LlmClient::tokenizer()` でトークン数を推定
- 末尾から逆順に数えて N トークン分の位置で切る
- ターン境界は無視。アイテム単位で切る
```toml
[compaction]
compact_threshold = 80000
retained_tokens = 8000 # ← retained_turns から変更
```
token-counter チケットが前提。
---
## R3: Auto-Read + リファレンス
### デフォルトリファレンスの抽出
Pod は `ToolOutput.referenced_files``HookInterceptor::post_tool_call` で観察し、
LRU 的な履歴バッファに積む(→ tool-output-referenced-files チケット)。
Compact 時は先頭 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 が referenced_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 は即中断ではない)
token-counter チケットが前提budget の計測に `estimate_text` が要る)。
### 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 が担う)
- 変更の diffgit がある)
- 中間のやりとりの詳細(最終結論だけ)
### フォーマット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ターン内でツール呼び出しが多く途中でコンテキストが膨らむケースは次のリクエストまで待つ。
検討: `post_tool_call` でもチェックする?
### 閾値の比率
- `post_run_threshold` = マニフェストの `compact_threshold`
- `turn_threshold` = `post_run_threshold * 9 / 8`(≈ 112.5%
9/8 の根拠はない(安全マージン)。要調整。
### Prune と Compact の相互作用
Prune はリクエストコンテキストのみ操作、`last_input_tokens` は前回の LLM レスポンスの値。
Prune の効果は閾値判断に反映されない。保守的compact しすぎる方向)で実害は小さい。
### compact 中のクライアント通知
Protocol チケットの `CompactStart`/`CompactDone` で対応。
### 復元時の挙動
`Outcome::Yielded` で記録されたセッションは `last_run_interrupted = true` で復元。
compact 後の新セッションが存在する場合、どちらを restore するかは呼び出し側の責任。
`compacted_from` で辿れる。
---
## 実装順序
0. **[前提] token-counter** — LlmClient に Tokenizer
0. **[前提] tool-output-referenced-files** — ToolOutput + Pod の LRU バッファ
1. **閾値逆転の修正**`compact_state.rs` のフィールド入れ替え、テスト修正、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 での注入
6. **Auto-Read Budget**`mark_read_required` のトークン会計、残量通知、超過エラー
7. **compact worker の累計入力トークン制限**`compact_worker_max_input_tokens`
8. **要約フォーマット** — タスク分類の要約プロンプト調整
9. **ガード** — write_summary 未呼び出し or mark_read_required 空時の追加プロンプト