主动摘要探测
开场解释
这个 Stage 在解决一个很实际的问题:别等上下文快爆了才补救,而是提前看一眼,要不要现在就做摘要。 它是一次“预检”。位置上,它在真正问 LLM(大语言模型)之前,先判断当前对话离上下文上限还有多少余量。若空间已经很紧,它就先走一遍摘要旁路,把后续要交接的摘要材料先准备好;若还够用,就什么都不改,直接进入下一步。它的价值不是推进对话本身,而是提前控风险,避免后面因为上下文太长而被动收缩。
主流程
- 先看要不要进这个 Stage。
只有两个条件都满足才会跑:
- 有
original_instruction(最初的用户任务,还保留着) - 开了
self._enable_summarize(允许摘要)
少一个都不做,立刻落到 4.3。 这说明它不是每轮必跑,而是一个有条件的“容量探针”。
- 有
- 如果要跑,就先做一次容量判断。
_check_proactive_summarization()(检查“现在是否该主动摘要”)会看:- 当前上下文上限
context_limit - 已经用了多少 token(给 LLM 的文本切分单位)
本质上是在问:“剩余空间还安全吗?” 这里它自己就能做两种结论:
- 不用摘要:余量还够,返回空结果
- 现在就摘要:余量低于阈值,立刻走完整摘要旁路
- 当前上下文上限
- 如果真的触发摘要,就先把“交接材料”暂存起来。
摘要旁路会产出两类东西:
handoff_prompt:后面要写进轨迹的“交接提示词”subagent_refs:摘要里提到的 subagent(被主 Agent 调用的小 Agent)引用
这个 Stage 只把它们放进 pending 寄存器里暂存,还不会立刻改动主轨迹。 这么设计是为了让本轮后面的 4.5 / 4.6 按统一位置去落盘、记账、切分历史。 也就是说,它现在只是“先备好材料”,不是“当场改历史”。
- 不管有没有摘要,这一轮通常都会继续往前走。
- 没触发摘要:直接去 4.3 问 LLM
- 触发了摘要:把交接材料暂存后,继续本轮后续 Stage;真正写入轨迹要等 4.5 / 4.6
所以它的成功效果很明确:要么什么都不变,要么只准备待提交的摘要交接物;它不直接改当前对话状态。
状态流动
- 读:
reg-pending-handoff-prompt— 本 Stage 不读取它;这个寄存器的意义在于后续 4.6 会消费这里暂存的摘要交接提示。这里说明它只是“待提交槽位”。 - 写:
reg-pending-handoff-prompt— 只有当主动摘要检查判定“现在就摘要”,且摘要旁路返回了handoff_prompt时才写入;目的是把本轮中途产出的交接提示先暂存,留给后续 Stage 统一记入轨迹。 - 写:
reg-pending-subagent-refs— 只有当主动摘要真的发生,且摘要旁路返回了subagent_refs时才写入;目的是把相关 subagent 引用一起暂存,供后续轨迹记录使用。 - 清: 无 — 这个 Stage 不清寄存器;如果门没过,或检查后判定“不需要摘要”,pending 寄存器在这里保持原样不变。
分支结果要点:
- 门条件不满足:本 Stage 不做检查,立即落到 4.3。
- 门条件满足并运行检查:
_check_proactive_summarization()会自己决定“无需摘要”或“立刻摘要”。- 无需摘要:不写任何 pending 数据,继续到 4.3。
- 立刻摘要:把交接材料写入 pending 寄存器,但此时仍只是暂存,还没有提交进主轨迹。
与前后 Stage 的衔接
4.1 刚完成本轮入口判断,把循环带到这里;这个 Stage 接着做的是“在真正发问前,先看上下文还撑不撑得住”。它产出的不是给 4.3 直接消费的答案内容,而是给 4.5 / 4.6 之后统一落轨迹 的待提交摘要材料;若什么都没产出,4.3 就像平常一样继续问 LLM。
Terminus2._run_agent_loopterminus_2.py:1283–1294 ↗
主循环中的主动摘要探测段
stage 上下文: 该片段位于 stage-4.2,承担“是否提前触发摘要交接”的门控职责。只有
original_instruction存在且self._enable_summarize为真时才进入;它本身不落轨迹,而是把_check_proactive_summarization(...)的结果暂存到 pending 状态,交由后续 stage-4.5 / 4.6 统一记入 trajectory。由于这是本 stage 当前唯一已翻译单元,它也构成该 stage 与 side-S1 摘要支流的直接连接点。
这段代码在干什么
这段代码在 agent loop 中可选地执行一次“主动摘要”探测:以当前 chat、初始用户指令 original_instruction 与 self._session 为输入,调用 _check_proactive_summarization(...)。若该调用返回非空结果,则将返回的 prompt 与 subagent_refs 分别写入 self._pending_handoff_prompt 和 self._pending_subagent_refs。真正的产出不是立即修改轨迹,而是为当前迭代后续阶段延迟提供 handoff 用户消息与摘要子代理引用。
接口 · 参数 / IO
(self, original_instruction, chat) -> None
- 参数:
self:?— Terminus2 实例,提供摘要开关、会话对象与 pending 状态寄存;original_instruction:?— 原始用户任务指令;作为主动摘要探测的前置条件与输入之一;chat:?— 当前主对话对象;传入_check_proactive_summarization(...)进行上下文容量判断 - 读状态:
self._enable_summarize,self._session - 返回: 无显式返回;真正产出是按条件写入 pending handoff / subagent 引用,供本次迭代后续 stage 消费。
- 副作用: 调用
self._check_proactive_summarization(chat, original_instruction, self._session); 写入self._pending_subagent_refs; 写入self._pending_handoff_prompt
执行流
- 先同时检查
original_instruction与self._enable_summarize;只有两者都满足时,才进入主动摘要探测分支。 - 以当前对话
chat、原始指令original_instruction和会话self._session调用await self._check_proactive_summarization(...),得到proactive_summary_result。 - 对返回值做真值判断;只有结果非空时,才将其解包为
prompt, subagent_refs。 - 把
subagent_refs暂存到self._pending_subagent_refs,用于稍后补写一条 system 级轨迹步骤。 - 把
prompt暂存到self._pending_handoff_prompt,用于稍后作为用户侧 handoff 消息进入当前迭代的轨迹处理。
源码
if original_instruction and self._enable_summarize:
proactive_summary_result = await self._check_proactive_summarization(
chat,
original_instruction,
self._session,
)
if proactive_summary_result:
prompt, subagent_refs = proactive_summary_result
# Store subagent_refs to add a system step later
self._pending_subagent_refs = subagent_refs
# Also store the handoff prompt to add as a user step
self._pending_handoff_prompt = prompt
Non-obvious 设计决策
- 该段以双条件门控(
original_instruction与self._enable_summarize)限制主动摘要探测范围,避免在无原始任务语义或功能开关关闭时进入额外的摘要判定路径。 - 代码对
_check_proactive_summarization(...)的结果采用显式“仅在非空时写入 pending 状态”的策略,而不是无条件覆盖;这保留了“未触发摘要”这一状态的空值语义,避免后续 stage 将空摘要误当成一次真实 handoff。 - 这里不直接把结果写入 trajectory,而是先写入
self._pending_*;这种延迟提交把“是否触发摘要”与“如何在轨迹中落盘”分离,使 stage-4.5 / 4.6 能用统一路径处理主动与回退两类摘要触发来源。
上下游关系
- 调用方: Terminus2._run_agent_loop
- 核心被调用: Terminus2._check_proactive_summarization
- 配置/状态来源: self._enable_summarize; self._session; original_instruction; chat
- 结果去向: self._pending_handoff_prompt; self._pending_subagent_refs; stage-4.5 对 subagent refs 的轨迹记录; stage-4.6 对 handoff prompt 的轨迹记录或历史切分处理
reg-pending-subagent-refs— 摘要触发后暂存子代理引用reg-pending-handoff-prompt— 摘要触发后暂存交接提示
Terminus2._check_proactive_summarizationterminus_2.py:956–980 ↗
按上下文余量判定是否触发主动摘要
stage 上下文: 该函数是 stage-4.2 中实际执行“主动摘要探测”的判定点。它不直接修改 Terminus2 的挂起寄存器,而是根据当前 chat 的估算 token 余量决定是否调用
_summarize(...),并把结果作为返回值交给上层。与同 stage 已翻译的_run_agent_loop相比,前者负责接收并保存结果,本函数只负责判定、调用与容错。
这段代码在干什么
函数读取模型上下文上限与当前 chat 的总 token 估算值,计算 free_tokens,并与 self._proactive_summarization_threshold 比较。若余量低于阈值,则记录一条 debug 日志并异步调用 _summarize(chat, original_instruction, session);成功时返回 (summary_prompt, subagent_trajectory_refs),失败时记录 error 日志后返回 None。若余量未低于阈值,也直接返回 None。
接口 · 参数 / IO
(self, chat: Chat, original_instruction: str, session: TmuxSession) -> tuple[str, list[SubagentTrajectoryRef] | None] | None
- 参数:
chat:Chat— 当前对话历史;用于估算总 token,并在需要时传给_summarize(...);original_instruction:str— 原始指令文本;仅在触发摘要时转交给_summarize(...);session:TmuxSession— tmux 会话对象;仅在触发摘要时转交给_summarize(...) - 读状态:
self._llm,self._proactive_summarization_threshold,self.logger - 返回: 当
free_tokens < self._proactive_summarization_threshold且_summarize(...)成功时,返回(summary_prompt, subagent_trajectory_refs);其余情况返回None。 - 副作用: 调用
self._llm.get_model_context_limit()读取模型上下文上限; 调用self._count_total_tokens(chat)估算当前对话 token 总量; 在阈值触发时写入一条 debug 日志; 在阈值触发时异步调用self._summarize(chat, original_instruction, session); 若摘要过程抛出异常,写入一条 error 日志
执行流
- 先通过
self._llm.get_model_context_limit()取得context_limit,再调用self._count_total_tokens(chat)得到current_tokens,据此计算free_tokens = context_limit - current_tokens。 - 将
free_tokens与self._proactive_summarization_threshold比较;只有在余量低于阈值时,才进入主动摘要分支。 - 进入摘要分支后,先用
self.logger.debug(...)记录一条包含free_tokens的调试日志,说明即将执行主动摘要。 - 随后在
try中等待self._summarize(chat, original_instruction, session)完成;若成功,解包得到summary_prompt与subagent_trajectory_refs,并立即以二元组形式返回。 - 如果
_summarize(...)抛出任意异常,则由except Exception as e捕获,并通过self.logger.error(...)记录错误信息。 - 当未触发阈值,或触发后摘要失败时,函数都走到结尾并返回
None。
源码
async def _check_proactive_summarization(
self, chat: Chat, original_instruction: str, session: TmuxSession
) -> tuple[str, list[SubagentTrajectoryRef] | None] | None:
"""Check if we should proactively summarize due to token usage.
Returns:
tuple: (summary_prompt, subagent_trajectory_ref) if summarization occurred, None otherwise
"""
context_limit = self._llm.get_model_context_limit()
current_tokens = self._count_total_tokens(chat)
free_tokens = context_limit - current_tokens
if free_tokens < self._proactive_summarization_threshold:
self.logger.debug(
f"Proactively summarizing. Free tokens: approximately {free_tokens}"
)
try:
summary_prompt, subagent_trajectory_refs = await self._summarize(
chat, original_instruction, session
)
return (summary_prompt, subagent_trajectory_refs)
except Exception as e:
self.logger.error(f"Error in proactively summarizing: {e}")
return None
Non-obvious 设计决策
- 触发条件使用“剩余上下文余量”
free_tokens < self._proactive_summarization_threshold,而不是等待真正超限;这是代码中显式采用的前置判定策略,使摘要在接近容量边界时就可发生。 - 摘要调用被包裹在
try/except Exception中,且异常只记录日志、不再向外抛出;这表明该函数将主动摘要视为可失败的辅助路径,而不是必须成功的硬性步骤。 - 返回值设计为“摘要结果二元组或
None”的可空协议,而不是在函数内写入某个self._pending_*字段;因此本函数的职责被限定为判定与产出结果,不在本地完成持久化。
上下游关系
- 调用方: Terminus2._run_agent_loop
- 核心被调用: self._llm.get_model_context_limit; self._count_total_tokens; self._summarize; self.logger.debug; self.logger.error
- 配置/状态来源: self._proactive_summarization_threshold; self._llm
- 结果去向: 返回给上层调用者的
(summary_prompt, subagent_trajectory_refs); 返回给上层调用者的None - 同类 sibling: Terminus2._run_agent_loop:兄弟函数负责接收本函数返回值并进一步处理;本函数自身只做阈值判定、日志记录与
_summarize(...)调用。