# コンテキスト圧縮 (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, // Prune 対象。None に置換するだけ } ``` `Tool::execute()` は `Result` を返す。 `From` で自動変換可能(小さい出力は 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` で、片方だけの設定も可能 (そちら側のチェックだけが有効になる)。 ### 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 / index(User / 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 が担う) - 変更の diff(git がある) - 中間のやりとりの詳細(最終結論だけ) ### 要約フォーマット(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 機構を壊さない