293 lines
14 KiB
Markdown
293 lines
14 KiB
Markdown
# コンテキスト圧縮 (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 / 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 機構を壊さない
|