10 KiB
コンテキスト圧縮: Prune + Compact
背景
長時間実行エージェントにとって、コンテキストウィンドウの管理はコア要件。 現状の Worker は history をそのまま保持し、オーバーフロー時の対策がない。
Claude Code の3層構造(MicroCompaction / AutoCompact / Full Compact)を参考に、 Insomnia では2層(条件付き Prune + Compact)で対処する。
参考: docs/ref/claude-code-compaction.md
前提: ToolOutput の再設計
Prune の設計は ToolOutput の構造に依存する。 現行の Inline/Stored enum を summary + content の2フィールド構造に改める。
詳細: tool-output-design.md — 実装済み
構造
pub struct ToolOutput {
pub summary: String, // 1-2行。常に残る
pub content: Option<String>, // 詳細。Prune で消える
}
Item::ToolResult {
call_id: CallId,
summary: String,
content: Option<String>,
}
Prune との関係
- summary: Prune 後も残る。「何をしたか」の最低限の情報
- content: Prune 対象。
Noneに置換するだけ - 巨大出力はツール側がファイルに退避し、content に見取り図を置く
削除対象
ToolOutput 再設計に伴い、以下を削除:
ToolOutputenum(Inline/Stored)→ struct に置換Contentenum,auto_summarize,ToolOutputProcessortraitBlobStoretrait,FsBlobStore,BlobOutputProcessorinspect_tool.rs(汎用の read_file/grep で代替)- Worker の
output_processorフィールド
Phase 1: 条件付き Prune
概要
Claude Code の clear_at_least パターンに倣い、削れるトークン量が閾値を超える場合にのみ Prune を実行する。キャッシュを無駄に壊さない。
キャッシュの制約
全主要プロバイダ(Anthropic / OpenAI / Gemini)で KV キャッシュはプレフィクスベース。 プレフィクス中のアイテムを変更すると、変更地点以降が全て再計算になる。
キャッシュ済み: [A, B, C, D, E]
Prune: [A', B, C, D, E] ← A の content を消した
再計算: [A', B, C, D, E] ← A' 以降すべて
Prune で得られるトークン節約 vs キャッシュ再計算コスト。
min_savings 閾値で「削る価値がある場合だけ」実行する。
コード配置
| 場所 | 内容 |
|---|---|
crates/llm-worker/src/prune.rs |
Prune アルゴリズム(集計 + 置換) |
crates/pod/src/prune_hook.rs |
PruneHook(Hook<PreLlmRequest> 実装) |
アルゴリズム
pub struct PruneConfig {
/// Prune 対象外とする直近ターン数
pub protected_turns: usize,
/// この推定トークン数以上削れる場合にのみ Prune を実行
pub min_savings: usize,
}
pub fn prune(items: &mut Vec<Item>, config: &PruneConfig) -> bool {
// 1. ターン境界の特定(UserMessage 出現位置)
let turn_starts = find_turn_starts(items);
if turn_starts.len() <= config.protected_turns {
return false;
}
let boundary = turn_starts[turn_starts.len() - config.protected_turns];
// 2. Prune 可能なトークン数を集計
let mut total_savings: usize = 0;
let mut prunable: Vec<usize> = Vec::new();
for (i, item) in items[..boundary].iter().enumerate() {
if let Item::ToolResult { content: Some(c), .. } = item {
total_savings += c.len() / 4; // 粗い推定
prunable.push(i);
}
}
// 3. 閾値チェック
if total_savings < config.min_savings {
return false;
}
// 4. Prune: content を None にするだけ
for &i in &prunable {
if let Item::ToolResult { content, .. } = &mut items[i] {
*content = None;
}
}
true
}
PruneHook
pub struct PruneHook {
config: PruneConfig,
}
#[async_trait]
impl Hook<PreLlmRequest> for PruneHook {
async fn call(&self, context: &mut Vec<Item>) -> PreRequestAction {
prune(context, &self.config);
PreRequestAction::Continue
}
}
特性
- 条件付き: 集計して閾値を超えた場合のみ実行
- 冪等:
content: Noneのアイテムはスキップ - 非破壊: history 本体は変更しない。Prune 状態(どこまで刈ったか)を Pod が保持し、LLM リクエスト構築時に反映する
- 単純: Prune =
content = None。blob 参照の解析やサマリ生成は不要
Phase 2: Compact
概要
history 全体を要約で置き換える。 別の Worker(要約専用・ツールなし)で要約を生成する。
トリガー
Controller が input_tokens を追跡し、run 完了後に閾値と比較。
let last_input_tokens = Arc::new(AtomicU64::new(0));
{
let tracker = last_input_tokens.clone();
worker.on_usage(move |event| {
if let Some(tokens) = event.input_tokens {
tracker.store(tokens, Ordering::Relaxed);
}
});
}
サーキットブレーカー
const MAX_COMPACT_FAILURES: usize = 3;
// 3回連続失敗で compaction を無効化
Compaction フロー
Compact は fork と同じ構造。旧セッションを保全し、新しい SessionId で圧縮後のセッションを開始する。
Run 完了 → input_tokens > threshold
↓
Controller: history を要約プロンプトに変換
↓
Controller: 要約用 Worker 生成(ツールなし、temperature=0)
↓
要約 Worker: 構造化要約を生成
↓
Controller: [要約 Item, 直近 N ターン] で新 history を構築
↓
Controller: 新 SessionId で新セッションを作成(SessionStart に compacted_from を記録)
↓
旧セッション JSONL はそのまま保全(append-only 原則を維持)
旧セッション (abc-123):
[entry0] → [entry1] → ... → [entryN] ← そのまま残る
新セッション (def-456):
[SessionStart { history: [要約 + 直近N], compacted_from: (abc-123, entryN.hash) }] → ...
SessionStart の出自フィールド
LogEntry::SessionStart {
ts: u64,
system_prompt: Option<String>,
config: RequestConfig,
history: Vec<Item>,
/// fork 由来の場合、元セッションと分岐点
forked_from: Option<(SessionId, EntryHash)>,
/// compact 由来の場合、元セッションと圧縮時点
compacted_from: Option<(SessionId, EntryHash)>,
}
- 通常の新規セッション: 両方
None - fork:
forked_from = Some(...) - compact:
compacted_from = Some(...) - EntryHash で元セッションのどの時点からの操作かを追跡可能
要約フォーマット
## Original Task
(元のユーザー指示)
## Completed Work
- (完了した作業。ファイルパス・関数名等の具体情報)
## Key Discoveries
- (判明した事実・制約・エラー)
## Current State
- (変更されたファイル、残タスク)
エラーハンドリング
- 要約 Worker エラー → 警告ログ、スキップ、consecutive_failures++
- 3回連続失敗 → セッション残りで compaction 無効化
- Thrash loop(compaction 直後に再び閾値超過)→ エラーで停止
設定
マニフェスト
[compaction]
# Prune: 直近何ターンを保護するか(デフォルト: 3)
prune_protected_turns = 3
# Prune: この推定トークン数以上削れる場合にのみ実行(デフォルト: 4096)
prune_min_savings = 4096
# Compact: input_tokens がこの値を超えたら要約を実行(省略 = 無効)
compact_threshold = 80000
# Compact: 圧縮後に保持するターン数(デフォルト: 2)
compact_retained_turns = 2
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionConfig {
#[serde(default = "default_prune_protected_turns")]
pub prune_protected_turns: usize, // default: 3
#[serde(default = "default_prune_min_savings")]
pub prune_min_savings: usize, // default: 4096
pub compact_threshold: Option<u64>,
#[serde(default = "default_compact_retained_turns")]
pub compact_retained_turns: usize, // default: 2
}
デフォルト動作
[compaction]省略: Prune も Compact も無効[compaction]あり・compact_threshold省略: Prune のみ有効
設計判断
| 判断 | 理由 |
|---|---|
| ToolOutput を summary + content に | Prune が content = None で済む。blob/inspect の複雑さが消える |
| BlobStore / inspect を削除 | 巨大出力はツール側の責務。フレームワークは summary/content を受け取るだけ |
Prune は条件付き(min_savings) |
KV キャッシュ無効化コスト vs 節約量。Claude Code の clear_at_least に倣う |
| Prune は request context を操作 | history 本体を保全。session log の完全性を維持 |
| Compact は run 間で実行 | 要約は LLM 呼び出しを伴う。ターンループ内では Prune が対処 |
| サーキットブレーカー | 連続失敗の無限ループ防止。Claude Code の知見 |
| 新しい trait は不要 | 設計原則3: Hook + Controller 制御 + set_history() で完結 |
実装順序
- ToolOutput 再設計 — enum → struct(summary + content)。Item::ToolResult の変更。単体テスト
- 旧モジュール削除 — BlobStore, BlobOutputProcessor, inspect_tool, ToolOutputProcessor, Content, auto_summarize。Worker から output_processor 除去
prune.rs— 条件付き Prune アルゴリズム。単体テストPruneHook— Pod に Hook 実装CompactionConfig— manifest にセクション追加LogEntry::Compacted— session_log に variant 追加compact()関数 — Controller に compaction ロジック + サーキットブレーカー- Protocol —
CompactionStart/CompactionDoneイベント追加
ステップ 1-2 は ToolOutput 移行として独立実行可能。 ステップ 3-4(Prune)と 5-6(Compact 準備)は並行可能。