yoi/docs/compaction.md

293 lines
14 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.

# コンテキスト圧縮 (Compaction)
長時間実行エージェントのコンテキストウィンドウ管理。
Prune段階的な出力除去と Compact履歴の要約置換の2層で対処する。
---
## 全体像
```
[各リクエストの合間]
LLM リクエスト
PruneHook (pre_llm_request)
→ 古い ToolResult の content を除去summary は残す)
→ リクエストコンテキストのみ操作、history 本体は不変
CompactInterceptor (pre_llm_request) ← safety net
→ input_tokens > request_threshold なら Yield
→ Worker が WorkerResult::Yielded で正常終了
Pod::handle_worker_result
→ turn 結果を現在 segment log に記録
→ compact() で同じ session_id 内の新 segment へ rotate → resume()
[ターンの合間 — 次の Pod::run 冒頭]
Pod::try_pre_run_compact ← proactive
→ input_tokens > post_run_threshold なら compact() (best-effort)
```
---
## Prune
古い ToolResult の `content``None` に置換し、`summary` だけ残す。
### 設計判断
- **条件付き実行**: 推定トークン節約量が `min_savings` を超えた場合のみ。KV キャッシュの無駄な無効化を避ける
- **リクエストコンテキストのみ操作**: history 本体は変更しない。Prune 状態を Pod が保持し、LLM リクエスト構築時に反映する
- **保護境界**: 直近 `prune_protected_tokens` 相当の suffix は残す。turn 数ではなく usage history 由来の token estimate で境界を引くため、単発の長い tool loop でも古い `ToolResult.content` が候補になる
- **冪等**: `content: None` のアイテムはスキップ
### ToolOutput の構造
```rust
pub struct ToolOutput {
pub summary: String, // 常に残る。「何をしたか」の最低限の情報
pub content: Option<String>, // Prune 対象。None に置換するだけ
}
```
`Tool::execute()``Result<ToolOutput, ToolError>` を返す。
`From<String>` で自動変換可能(小さい出力は summary のみ、大きい出力は summary + content
巨大な出力はフレームワークの責務外。ツール側がファイルに退避し、content に見取り図を置く。
---
## Compact
### 用語
- **run = turn**: 同じ概念。1 ユーザープロンプト → 完了までの単位
- **リクエスト**: 1 turn 内で投げる個別の LLM 呼び出し。ツール使用で 1 turn に複数リクエストが発生する
- **リクエストの合間**: 1 turn 内、次の LLM リクエストを投げる前の地点
- **ターンの合間**: turn が完了して次の turn を待つ状態
### トリガー2段階の閾値
1. **ターンの合間 (次 turn 冒頭)**: `try_pre_run_compact()``input_tokens > post_run_threshold` → best-effort
2. **リクエストの合間 (CompactInterceptor)**: `pre_llm_request``input_tokens > request_threshold``PreRequestAction::Yield`
**ターンの合間が proactive (小さい閾値)**:
turn が完了した地点はタスクの自然な区切り。ここで先を見越して早めに compact する。
マニフェストの `threshold` が対応。
**リクエストの合間は safety net (大きい閾値)**:
turn 内部でリクエストの合間にチェックするのは「暴走的に膨張した場合のみ止める」用途。
マニフェストの `request_threshold` が対応。通常は発動しない。
**両閾値は manifest で個別指定する**。過去の設計では 9/8 倍で自動導出していたが、
比率に根拠がなかったため廃止。両方が `Option<u64>` で、片方だけの設定も可能
(そちら側のチェックだけが有効になる)。
### Yield の仕組み
`PreRequestAction::Yield` は Worker のターンループを正常終了させる汎用プリミティブ。
Worker は Compact を知らない。「Interceptor が yield と言ったので抜ける」だけ。
Pod が Yielded を検知して compact → resume する。
```
PreRequestAction::Yield → WorkerResult::Yielded → Pod が compact → resume
PreRequestAction::Cancel → WorkerError::Aborted → エラー
```
### 安全機構
- **サーキットブレーカー**: 3回連続 compact 失敗で無効化 (`CompactState::disabled`)
- **Thrash 検出**: compact 直後に再び閾値超過 → `PodError::CompactThrash`
- **Yield 前後の永続化**: Worker が `Yielded` で抜けた turn は現在 segment log に記録してから compact する。compact が失敗しても元 segment の状態は残る
### セッション管理
compact は「新 SessionId を作る」操作ではなく、同じ `session_id` 配下で active `segment_id` を切り替える操作。
```
session_id = abc-123
segment old:
SegmentStart { origin: fresh/fork/... }
[...entries...]
RunCompleted { result: Yielded }
segment new:
SegmentStart { compacted_from: SegmentOrigin { segment_id: old, at_turn_index: N } }
[system prompt]
[system: 構造化要約]
[system: retained tail / auto-read / references]
...以後の turn を append
```
`SegmentStart.compacted_from` / `forked_from` が lineage を追跡する。Pod 名からの resume は `pod-store` metadata の active pointer が現在の `(session_id, segment_id)` を指し、conversation/history の replay は `session-store` の segment log から行う。
---
## compact 後の history 構造
全て system message`Item::Message { role: System }`)として注入。
```
[system prompt] ← 不変
[system: 構造化要約] ← compact worker の出力
[system: auto-read ファイル群] ← read_required の結果
[system: リファレンス一覧] ← reference の結果
[直近 N トークン分の生の会話] ← pruned 状態で保持
```
### 直近の保護: トークンベース
ターン単位ではなくトークン数ベースで直近の会話を保護する。
ターン単位の問題: 自走エージェントは1ターン内で多数のリクエストを回す。
1ターンが膨大に長くなり、保護量がターン長に依存してしまう。
トークン数ベースなら、ターンの長さに関わらず一定量の直近コンテキストが保持される。
```toml
[compaction]
threshold = 80000 # ターンの合間 (proactive)
request_threshold = 90000 # リクエストの合間 (safety net)
prune_protected_tokens = 8000 # prune から保護する末尾 token budget
retained_tokens = 8000 # compact 後に生のまま残す末尾 token budget
overview_target_tokens = 8000 # compact worker 初期 overview の通常目標
overview_warning_tokens = 16000 # 超えたら警告・trace、compact は続行
overview_deadline_tokens = 40000 # 超えたら粗い overview へ fallback
worker_context_max_tokens = 50000 # compact worker session 全体の hard limit
finish_warning_remaining_tokens = 8000 # 残りが少ないため write_summary へ進める勧告
final_reserve_tokens = 4000 # 最終 summary/closing turn 用 reserve
worker_max_turns = 20 # compact worker 自身の tool loop 上限
summary_target_tokens = 1500 # write_summary の目標サイズ
summary_max_tokens = 3000 # write_summary の hard validation
auto_read_budget_tokens = 8000 # compact 後に注入する file content 合計上限
result_context_max_tokens = 24000 # 新 session 初期 context の dry-run validation
```
`compact_*` prefix の旧 key は互換 alias として読み取るが、`[compaction]` 内の新規 key は prefix なしを正とする。
初期 overview の target/warning は効率のための目安で、通常は hard error にしない。deadline 超過時も、可能なら deterministic に粗い overview へ fallback して compact の完走を優先する。
### Auto-Read とリファレンス
2段階のファイル参照:
- **Read** (`mark_read_required`): 全文/範囲指定でコンテキストに注入
- **Reference** (`add_reference`): 「読んだことがある」とだけ伝える。必要なら再読要求
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.
```
auto-read も通常の history 内 system message なので、将来の Prune/Compact 対象になる。
---
## compact worker
要約生成とファイル選定を行う使い捨て Worker。Pod は compact 対象 prefix を全文投入せず、User / Assistant / System を優先した bounded overview と tool index を初期 input として渡す。Tool call arguments、tool result full content、reasoning body は初期 input には載せない。
初期 overview は `overview_target_tokens` を目標にする。`overview_warning_tokens` を超えた場合は警告・trace を記録して続行し、`overview_deadline_tokens` を超えた場合は粗い deterministic overview へ fallback する。Compact の目的は完走なので、初期 input が少し大きいだけでは hard error にしない。
### ツール
```
read_file(path, offset?, limit?) — ファイルを読んで判断する
mark_read_required(path, offset?, limit?) — auto-read 対象として指定
add_reference(path) — リファレンスとして追加
write_summary(text) — 構造化要約を出力/上書き(最後の呼び出しが採用)
```
### フロー
1. Pod が `tools::Tracker::recent_files(5)` で最近触られたファイルを抽出(デフォルトリファレンス)
2. compact worker にプロンプトとして渡す:
- bounded overview / indexUser / Assistant / System 優先)
- デフォルトリファレンスの一覧
- TaskStore snapshot
3. compact worker が自律的に:
- search_session_log / read_session_items で bounded overview から漏れた compact 対象履歴を必要範囲だけ探索
- read_file で各ファイルを読み、必要性を判断
- mark_read_required / add_reference で指定
- write_summary で構造化要約を出力(呼び直し可)
4. CompactWorkerInterceptor が worker session 全体の context occupancy を監視する:
- `finish_warning_remaining_tokens` 到達時に「探索を切り上げて write_summary へ進め」と Worker history に永続化される warning を挿入し、人間向け warning も出す
- `final_reserve_tokens` を割った後は `write_summary` 以外の探索 tool call に synthetic error を返し、最終 summary の余白を守る
- `worker_context_max_tokens` 超過は最後の hard stop
5. ターン終了時に write_summary 未呼び出し or read_required 空(かつファイル操作履歴がある場合)→ 追加プロンプトで促す
6. `summary_max_tokens``result_context_max_tokens` で compact 結果を検証してから新 segment を作り、Pod metadata の active pointer を更新する
### 構造化要約の要件
**目的**: auto-read でコードは別途載せるので、要約は意思決定・コンテキスト・方向性の記録に特化する。
**含めるべき内容:**
1. 何を、なぜやったか — 意思決定の記録。具体的な型名・関数名で言及
2. ユーザーの指示・フィードバックの原文 — ニュアンス保持。重要なもののみ
3. 発生した問題と解決策 — 同じ轍を踏まない
4. 今どこにいて次に何をするか — compact 前後の一貫性
**含めないもの:**
- コードの全文auto-read が担う)
- 変更の diffgit がある)
- 中間のやりとりの詳細(最終結論だけ)
### 要約フォーマット5セクション、1000-2000 トークン目安)
```
## Completed Tasks
### (タスク名)
- 完了した作業(具体的な型名・ファイル名で)
- 注意点 / 発覚した事実
## Active Task
### (タスク名)
- 目標
- 現状(何が済んで何が未着手か)
- 次のステップ
## Key Decisions
- (判断内容) — (理由)
## User Directives
- 「(ユーザー発言の原文)」 — 重要な指示・フィードバックのみ
## Current Work
直前に何をしていたか。2-3行
```
セッション中に複数タスクが切り替わっている前提で設計。
完了タスクは簡潔に(注意点・事実のみ)、進行中タスクは十分な詳細で。
---
## 設計の背景
### Claude Code からの知見
Claude Code の compaction 実装(`docs/ref/claude-code-compaction.md`)を参考に、以下を取り入れた:
- **条件付き Prune** (`min_savings`): KV キャッシュ無効化コストとのトレードオフ。`clear_at_least` パターン
- **サーキットブレーカー**: 連続失敗の無限ループ防止
- **2段階のファイル参照** (Read vs Referenced): Claude Code は compact 後に「Referenced file」内容省略と「Read」内容保持を区別する。サイズと必要性で判断
- **要約の具体性**: 型名・関数名・ユーザー発言原文を保持。抽象的な要約は役に立たない
### 設計原則との対応
- Prune は Worker 層の拡張。新しい trait は不要設計原則3
- Compact は Pod 層の制御。Worker は Yield を知るが Compact は知らない
- CompactInterceptor は Decorator パターンで HookInterceptor をラップ。既存の Hook 機構を壊さない