← Starchild Wiki

Context Compaction上下文压缩系统

Three trigger levels, newest-first retention, and the guards that keep a summary from eating the conversation三级触发、最近优先保留,以及防止摘要吞掉对话的守卫层

core/compaction.py · core/context_engine.py · core/compaction_validation.py

01The problem问题

A long tool loop grows the prompt on every turn. Without intervention the agent bills 500K–800K-token prompts before it hits any wall.长工具循环每一轮都在撑大 prompt。不干预的话,Agent 会在撞墙之前反复以 50–80 万 token 的 prompt 计费。

Two constraints have to hold at once. Capability ceiling: never exceed the model's context window. Working-set ceiling: never carry more context than the task actually needs, regardless of how big the window is. A 1M-context model that respects only the first constraint is an expensive way to be wrong.两个约束必须同时成立。能力上限:不能超过模型上下文窗口。工作集上限:不管窗口多大,都不该携带超出任务所需的上下文。只守第一条的 1M 窗口模型,只是把错误变得更贵。

So the effective limit is min(compact.max_tokens, window × ratio) — 80K by default. Small-window models compact by ratio; large-window models compact at the absolute working-set limit.因此有效阈值是 min(compact.max_tokens, window × ratio),默认 80K。小窗口模型按比例压缩,大窗口模型按绝对工作集上限压缩。

02Three trigger levels三级触发

Compaction is not one event. It is a ladder — the cheapest intervention fires first.压缩不是单一事件,而是一个阶梯——最便宜的干预先触发。

0.50 hygiene 0.78 entry / mid-loop 0.92 hard 0 context window no action prune tool_results (no LLM) summarize force

Fig 1.图 1. Escalating interventions as a fraction of the model's context window.按上下文窗口占比递进的干预层级。

Level层级Ratio比例Cost代价What it does做什么
hygiene0.50free免费No-LLM pass. Prunes old tool_result bodies outside the last 10 messages. Runs once before the tool loop so tool-heavy growth is caught early.无 LLM 清理。裁剪最近 10 条之外的旧 tool_result 正文。在工具循环前跑一次,提前吃掉工具输出的膨胀。
entry / mid-loop0.781 summarizer call1 次摘要调用Real compaction: keep the recent tail verbatim, summarize everything older. Entry = before the turn; mid-loop = inside a long tool loop.真正的压缩:逐字保留近期尾部,把更早的内容摘要。entry 在回合前触发,mid-loop 在长工具循环内触发。
hard limit0.92forced强制Last line of defence — compaction happens whether or not the soft trigger already ran this turn.最后防线——无论本回合软触发是否已跑过,都强制压缩。

All three are overridable in workspace/config/agent.yaml under compact.*. The summarizer runs on a cheap dedicated model (anthropic/claude-haiku-4.5), never on the conversation model.三者都可在 workspace/config/agent.yamlcompact.* 下覆盖。摘要器跑在专用的廉价模型(anthropic/claude-haiku-4.5)上,绝不占用对话模型。

03Newest-first retention budget最近优先的保留预算

The old design kept a fixed count of recent messages. The current one keeps a fixed token budget, walking newest → oldest.旧设计保留固定条数的近期消息,现设计保留固定token 预算,从新到旧回溯。

Why the change: message count is a terrible proxy for size. Ten screenshots and ten one-liners are both "10 messages" but differ by two orders of magnitude in tokens. The budget (compact.retention_budget_tokens, default 64K, clamped to 80% of max_tokens) makes retention size-aware.为什么要改:消息条数完全不能代表体积。十张截图和十句短话都是「10 条」,token 差两个数量级。预算制(compact.retention_budget_tokens,默认 64K,且被夹到 max_tokens 的 80%)让保留策略对体积敏感。

# walk newest → oldest until budget exhausted
keep verbatim   msg[n], msg[n-1], … while budget remains
keep truncated  first over-budget message  # never dropped outright
summarize       everything older than the boundary

# invariants
- boundary never splits a tool_use / tool_result pair (moves back)
- messages containing images or tool blocks are never truncated
- images cost a flat 1,100 tokens each, not their byte size
- post-compaction sweep strips orphan tool_results

The truncate-don't-drop rule matters: the message straddling the boundary usually holds the user's actual instruction. Dropping it loses the task; truncating it keeps the intent at a fraction of the cost.「截断而非丢弃」很关键:跨越边界的那条消息往往正是用户的实际指令。丢掉它就丢了任务,截断则以极小代价保住意图。

The flat image cost is a similar piece of realism. A base64 screenshot is enormous as text but roughly constant as tokens, so charging it by byte size would evict a whole conversation to make room for one picture.图片按固定 token 计价同理。base64 截图作为文本极其庞大,但作为 token 大致恒定;按字节计价会为了一张图把整段对话赶出去。

04Guards: when the summary itself is the bug守卫层:当摘要本身是 bug

Compaction replaces real history with generated text. If that text is junk, the failure is silent and the conversation is already gone.压缩用生成文本替换真实历史。如果这段文本是垃圾,失败是静默的,而对话已经没了。

Failure故障Observed实际观测Guard守卫
Placeholder占位符External writers set "Generating…" into the summary field while the real one is still in flight.外部写入方在真实摘要生成中时,把 "Generating…" 写进摘要字段。Regex whitelist of placeholder shapes, matched against the whole stripped string — a summary that merely mentions "loading" is still valid.对整串(strip 后)做占位符正则匹配——仅在正文中提到「loading」的摘要仍然有效。
Marker echo标记回显A model returned an 11-token output that was only the END-OF-SUMMARY marker — silently replacing ~23K tokens of history with nothing.某模型只输出了 11 token —— 全是 END-OF-SUMMARY 标记本身,静默地把约 23K token 的历史替换成了空。Normalize first: strip the echoed marker and any wrapping code fence, then length-check. Marker-only collapses to empty and is rejected.先归一化:剥掉回显的标记与包裹的代码围栏,然后再做长度检查。只有标记的输出会塌缩为空并被拒绝。
Too short过短Any output below the section-scaffold floor is not a summary.低于分节骨架下限的输出不可能是摘要。50-char minimum — comfortably above every plausible placeholder, far below a real summary.50 字符下限——远高于任何合理占位符,远低于真实摘要。
Orphan pairs孤儿配对A tool_result whose tool_use was summarized away — most providers hard-error on this.对应 tool_use 已被摘要掉的 tool_result——多数供应商会直接报错。Post-compaction pairing sweep, in message order, strips unmatched results.压缩后按消息顺序做配对清扫,剥掉未配对的结果块。

Every rejection falls back to full message replay rather than poisoning the context. Losing the cost saving is recoverable; feeding the model noise labelled "[Previous conversation summary]" is not.任一拒绝都回退到完整消息重放,而不是污染上下文。省不下钱可以接受;把噪声贴上「[Previous conversation summary]」标签喂给模型不可接受。

05Design principles设计原则

Principle原则Why理由
Cheapest intervention first最便宜的干预优先Pruning stale tool output costs nothing and defers the summarizer call entirely on tool-heavy turns.裁剪陈旧工具输出零成本,且能在工具密集的回合彻底推迟摘要调用。
Two ceilings, not one两个上限而非一个Window ratio alone lets 1M-context models burn money legally.只看窗口比例,会让 1M 上下文模型「合法地」烧钱。
Budget over count预算优于条数Message count is uncorrelated with token cost.消息条数与 token 成本不相关。
Truncate, don't drop截断而非丢弃The boundary message usually carries the live instruction.边界消息通常承载着当前指令。
Fail to replay, not to noise失败回退到重放而非噪声A bad summary is worse than no summary, and it fails silently.坏摘要比没有摘要更糟,而且它静默失败。