コンテキスト圧縮と MoA
職務
二つの役:① 履歴メッセージが provider のコンテキスト上限に近づいたとき、古いメッセージを要約/刈り込んで予算 (budget) を空ける(context_compressor)。② 任意で Mixture-of-Agents パスにより複数モデルの出力を集約する(moa_loop)。どちらも主ループ内から呼ばれ、「長い対話を走らせ続ける」「回答を安定させる」ための支えである。
主要ファイル
context_compressor モジュール— 圧縮総入口(約 3700 行)予算推定:190-242—_estimate_msg_budget_tokens/_serialized_length_for_budgetメッセージ浄化:136-155—_fresh_compaction_message_copy/_strip_persistence_markersツール呼び出し (tool dispatch) とパス言及抽出:337-360—_extract_tool_call_name_and_args/_collect_path_mentions内容刈り込み:472-515—_append_text_to_content/_strip_image_parts_from_partsmoa_loop— Mixture-of-Agents 集約IterationBudget— 圧縮 (compression) refund と予算連動
データフロー
- 主ループは毎ラウンド圧縮要否を評価(
_should_run_preflight_estimate:213参照)。 - 閾値到達後、context_compressor を呼ぶ:
- 各メッセージの予算占有を推定(
agent/context_compressor.py:419) - 古いメッセージを浄化コピー(
agent/context_compressor.py:136)、永続化マーカーを剥離(agent/context_compressor.py:155) - ツール呼び出し名/引数とパス言及を保持し、要約で重要操作を失わない(
agent/context_compressor.py:337) - 画像パートと超長内容を刈り込む(
agent/context_compressor.py:472)
- 各メッセージの予算占有を推定(
- 圧縮が生んだ「新空間」は
iteration_budget.refund()(agent/iteration_budget.py) で一部還付され、主ループが数ラウンド多く走れる。 - MoA が有効なら、
moa_loopがこのラウンドで複数 provider を並行呼び出して集約し、主回答の増強または仲裁とする。
予算推定は圧縮の意思決定の基礎だ。以下は agent/context_compressor.py:419-437 の核心。
def _estimate_msg_budget_tokens(msg: dict) -> int:
"""Counts the full ``tool_call`` envelope, not just ``function.arguments``
— counting only the arguments undercounted parallel-tool turns by 2-15x
(#28053). Also counts provider replay fields (``codex_reasoning_items``)."""
content_len = _content_length_for_budget(msg.get("content") or "")
tokens = content_len // _CHARS_PER_TOKEN + 10 # +10 for role/key overhead
for tc in msg.get("tool_calls") or []:
if isinstance(tc, dict):
tokens += len(str(tc)) // _CHARS_PER_TOKEN
for key in _REPLAY_BUDGET_KEYS:
tokens += _serialized_length_for_budget(msg.get(key)) // _CHARS_PER_TOKEN
return tokensかつて踏んだ二つの罠がコメントに書かれている。① function.arguments 文字列だけ数えると並行ツール呼び出しのケースで 2-15 倍少なく数える。② codex_reasoning_items のような replay フィールドを数えないと、「小さく見えて実は大きい」assistant メッセージが誤って保護され、圧縮後も上限に張り付き、その後も繰り返し圧縮される(#55572)。
浄化と永続化マーカー剥離は agent/context_compressor.py:136-160 にある。
def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]:
"""Shallow .copy() propagates ``_db_persisted``; new child session then
skips every row (#57491). Strip at every copy site."""
fresh = msg.copy()
fresh.pop(_DB_PERSISTED_MARKER, None)
return fresh
def _strip_persistence_markers(messages: List[Dict[str, Any]]) -> None:
"""Terminal sweep — invariant holds regardless of intermediate copy sites."""
for msg in messages:
if isinstance(msg, dict):
msg.pop(_DB_PERSISTED_MARKER, None)「二重の防線」だ。_fresh_compaction_message_copy が各 copy サイトでその場で剥離し、_strip_persistence_markers が compress() 末尾でもう一度完全な sweep を行う。前者は意図が明確、後者は構造的な安全網。画像の刈り込み(agent/context_compressor.py:497-515 の _strip_image_parts_from_parts)もロジックは似ている——image/image_url/input_image パートをテキストプレースホルダーに置き換え、画像がないときは None を返して呼び出し側にスキップさせ、各メッセージで無意味な新リストを生成しない。
設計動機
なぜ正確な token 計数ではなく「推定」を使うのか? provider の tokenizer は非公開(reasoning モデルの隠しフィールドは特にそう)で、tiktoken は遅い上に不正確だ。モジュールは char-based 推定 + 10 文字のオーバーヘッドという固定式を採用する。「圧縮するか」「tail をどう保護するか」の意思決定において、一貫して保守側に倒れていれば十分だ。正確である必要はなく、preflight の推定と同じ物差しを使うことだけが求められる(turn_context の _should_run_preflight_estimate を参照)。さもないと「preflight は圧縮不要、圧縮内部は圧縮できない」というデッドロックが起きる。
なぜツール呼び出し名とパス言与を保持するのか? LLM が古いメッセージを要約するとき、「write_file(/tmp/foo.py) を呼んだ」という关键操作を飲み込みがちだ。次ラウンドでモデルが「前にファイルについて議論した」と読んでも、どこに書いたか分からなければ、重複作成や誤読を起こす。_extract_tool_call_name_and_args / _collect_path_mentions が hard な情報を summary の横に貼り付ける。代償は数行の余分な token、見返りは後続ツール呼び出しが記憶喪失しないことだ。
refund を iteration_budget に返す理由——予算を直接増やさないのは、max_iterations を有名無実化しないためだ。圧縮一回ごとに数ラウンド増やしていたら、長対話は無限に走れてしまう。refund は圧縮自身が節約した一回二回分のツール呼び出しイテレーション (tool dispatch iteration) だけを返す。「上限はあくまで上限」だ。
境界と失敗
- 圧縮が前に進まない:
_compression_made_progressは token 減少が 5% を超えたときだけ進展とみなす。あるラウンドが 2% しか節約しなければ「進展なし」となり、多段空転を防ぐ。このとき主ループは auto-reset で新会話に切り替える。 - 永続化マーカーの残留:圧縮産物が
_db_persistedを持っていると、新しい child session のstate.dbがそれをスキップし、compacted transcript が DB 上で完全に失われる(#57491)。terminal sweep がcompress()末尾で強制的にもう一度掃して安全網とする。 - summary が新指示と誤読される:弱いモデルは summary 内の「## Active Task」を新規ユーザーリクエストとして扱ってしまう(
#11475、#14521)。_SUMMARY_END_MARKERで明示的な区切り線を入れ、alternation 境界シナリオでは_MERGED_PRIOR_CONTEXT_HEADER/_MERGED_SUMMARY_DELIMITERで二重に包んで曖昧さを排除する。 - 履歴 prefix の復活:
_HISTORICAL_SUMMARY_PREFIXESが旧バージョンの summary prefix を保存する。resume で新 lineage に入るとき、旧 prefix も一緒に剥離し、stale directive が body に埋まって返答を乗っ取るのを防ぐ。
まとめ
圧縮器は長対話の生命線。核心のトレードオフは「何を捨て、何を残すか」——ツール呼び出しとパス言及を優先保持する。失うと後続ラウンドが「記憶喪失」するからだ。MoA は直交する拡張で、オンオフ可能。どちらも主ループ構造を変えず、適切なタイミングで切入するだけ。