自动压缩、重试与用量统计怎样协作
从 message_end 落盘、post-run 决策、自动压缩回写和统计聚合出发,解释重试历史、活动 Context 与累计用量为什么必须分别计算。
第 12 章已经区分 provider 内部 retry、assistant turn retry 和 overflow recovery。本章站到 Coding Agent 控制面的角度,补上它们与持久化、队列和用量统计的结算关系。关键不是“失败后再试几次”,而是哪些事实留在历史,哪些对象只服务下一次 Context。
flowchart TD
accTitle: 一次 assistant 终态后的控制面结算
accDescr: assistant 终态先进入 session 历史;post-run 再选择普通重试或压缩,活动 Context 可以移除失败消息,但累计用量始终从完整 entry 历史聚合
ME["assistant message_end"] --> PE["appendMessage 到 session"]
PE --> ER{"普通瞬时错误?"}
ER -->|是| RT["退避,移出活动 Context,continue"]
ER -->|否| CP{"overflow / threshold?"}
CP -->|是| CO["生成并追加 compaction"]
CO --> RB["buildSessionContext 重建活动消息"]
RB --> CT{"需要重试或仍有队列?"}
CT -->|是| CN["continue"]
CT -->|否| ST["agent_settled"]
PE -. "完整 entries" .-> TOT["累计 tokens / cost"]
RB -. "当前 messages" .-> CUR["Context usage"]
失败消息先成为历史,再离开活动 Context
_handleAgentEvent() 在 assistant message_end 时先把消息追加到 SessionManager,并保存为 _lastAssistantMessage。底层 Agent 返回以后,_handlePostAgentRun() 才处理策略:普通可重试错误优先进入 _prepareRetry();预算耗尽时发出失败事件;其余消息再检查 compaction。任何一条策略返回 true,外层都会调用 agent.continue()。
_prepareRetry() 读取 settings 中的 enabled、maxRetries 与 baseDelayMs,递增 session 内的 attempt,发出可见倒计时事件,再从 Agent.state.messages 移除末尾 assistant error。它没有删除 SessionManager entry。退避使用独立 AbortController;用户取消只会结束等待并发出 auto_retry_end(success: false),已经写下的失败尝试仍可审计。
一次成功 assistant message 会立即把 _retryAttempt 归零并发出成功事件,避免同一 prompt 内多次 LLM 调用累计到下一次独立失败。compaction 与 branch summary 的模型调用复用同一份 retry settings 和回调形状,但通用 retry helper 在各自调用内管理尝试;它们不共享 _retryAttempt 这个 Agent turn 计数器。
Overflow 和 threshold 走两条压缩后续
_checkCompaction() 只检查当前 model 的 assistant message,并跳过最新 compaction 之前的旧 usage。context overflow 时,成功 stop 说明答案已完整生成,只压缩,不再次请求;错误 overflow 会把失败消息从活动 Context 移走,compact 后最多 continue 一次。threshold 触发只压缩,默认等用户下一条 prompt;如果压缩期间已经有 steering、follow-up 或 custom message 排队,控制面仍会 continue 交付它们。
压缩生成 summary 后,控制面先追加 compaction entry,再调用 buildSessionContext(),用 summary、保留区与压缩之后的新 entry 替换 Agent.state.messages。完成事件包含 tokensBefore、估算的 tokensAfter、usage 和 willRetry。若确实需要恢复失败 turn,方法返回 true;否则只在 Agent 队列非空时继续。
这也划清了 compaction 的作用域:它改变下一次送给模型的上下文,不删 JSONL 里的旧消息,不抹掉产生 summary 的额外模型用量,也不把旧分支重写成一条线。
累计账单与当前 Context 不能用同一个数字
getSessionStats() 遍历 sessionManager.getEntries() 的全部历史。它累计 assistant usage,也把 tool result、branch summary 和 compaction 自身的 usage 加进去;因此被 compaction 从活动 Context 裁掉的消息仍计入 tokens 与 cost。这些数字回答“这个 session 一共消耗了多少”。
getContextUsage() 回答另一个问题。它以当前 model 的 contextWindow 为分母,用活动 messages 的最近有效 assistant usage 加后续估算求 tokens。最新 compaction 刚结束、还没有新的 assistant usage 时,保留消息里的 usage 仍反映压缩前 Context,不能拿来显示当前占用;函数返回 { tokens: null, percent: null },直到下一次模型响应建立新测量点。未知不是零。
usage breakdown 还会把可归因的 assistant usage 按 provider/model 分组,把 tool result 和两类 summary 放进 Tools/summaries,避免把扩展或压缩成本硬塞给当前聊天模型。
下面两组 faux 测试都不需要真实模型,但需要先安装固定 tag 的依赖:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
cd "$repo"
npx vitest --run packages/coding-agent/test/suite/agent-session-retry-events.test.ts
npx vitest --run packages/coding-agent/test/agent-session-stats.test.ts
# 当前无依赖 checkout 可执行的静态复核
git show v0.83.0:packages/coding-agent/src/core/agent-session.ts |
nl -ba | sed -n '1061,1103p;1953,2042p;2672,2738p;3107,3207p'
本轮验证到源码引用和静态结算顺序为止,两条 npm 测试没有运行。真实 provider 的计费字段也未与账单后台逐项对账:Pi 聚合的是 provider 返回并写入消息的 usage,最终费用仍以供应商记录为准。
第五部到这里把 Coding Agent 的控制面闭合起来:输入经 prompt 管线进入 Agent,终态进入 session 树,runtime replacement 管会话切换,设置变更同时留下当前状态和恢复线索,post-run 决定恢复策略。下一部沿工具 registry 往下走,看默认工具、自定义扩展和执行拦截怎样进入这套控制面。