Skip to content

✨ feat(sidecar): 防护性终止独立 repeat_guard reason 与恢复上下文 - #466

Merged
CavinHuang merged 3 commits into
mainfrom
fix/392-repeat-guard-recovery
Aug 23, 2026
Merged

✨ feat(sidecar): 防护性终止独立 repeat_guard reason 与恢复上下文#466
CavinHuang merged 3 commits into
mainfrom
fix/392-repeat-guard-recovery

Conversation

@CavinHuang

@CavinHuang CavinHuang commented Aug 22, 2026

Copy link
Copy Markdown
Owner

背景

repeat guard 硬停经 SDK 的 error_completion_guard subtype 收场(errorCode: 'repeated_tool_call')。此前 sidecar run-loop 把它归入泛化 status: 'errored' → 对外 failed,导致:

  • 下条消息无任何 runtime-recovery-state(hasTurnLimitedMarker 只认 error_max_turns
  • 与 turn_limited 不同,进度持久化与恢复链路全部缺席

状态机改动:带标记的 turn_limited(非新枚举)

选了**「带标记的 turn_limited」**而非新增状态枚举:新增一个状态会波及所有状态消费者(LumeRunStatus、fromAgentRuntimeRunResult、事件总线、web 适配层),而 repeat guard 硬停的下游语义(进度保留、可恢复、markCompleted)与 turn_limit 完全一致,差异只在恢复文案。

  • run-looperror_completion_guard + errorCode === 'repeated_tool_call'{ status: 'turn_limited', terminationReason: 'repeat_guard' }。宿主自有 completionGuard stop 保持 errored 不变——两者共用 subtype 且都带结构化 errorCode,靠取值区分(repeat guard 为 repeated_tool_call,宿主 stop 为 verification_inconclusive / verification_failed_after_repair 等其他取值),不做英文文案匹配。
  • AgentRuntimeRunResult 增加可选 terminationReason?: 'repeat_guard'fromAgentRuntimeRunResult 无需改动(turn_limited → completed 路径天然覆盖带标记结果,测试钉住)。
  • run-observer.recordTurnLimited 落盘 item 的 payload 携带 terminationReason 标记;item name 保持 turn_limited,hasTurnLimitedMarker 判定语义零变化。
  • onComplete reason 扩为 max_turns | repeat_guard 并透传(im-message-router / rpc 测试 mock 同步类型)。

恢复上下文与文案

  • agent-service 新增 isRepeatGuardLimitedItem,按落盘标记把恢复上下文分流到新的 buildRepeatGuardRecoveryContext
    • <runtime-recovery-state reason="repeat_guard">
    • 文案:「上次运行因重复执行相同操作被保护机制停止,建议调整指令以避免重复操作。」风格对齐既有 reason="turn_limit" 恢复文案。
  • web 端检查结论:🐛 fix(sdk): repeat guard 加固——交替击穿/同批副作用/误伤面收敛 #390 的固定提示(completion_guard → run.failed + code repeated_tool_call 专属文案)与 turn_limit 提示本就分事件、分文案呈现,无需改动。

测试

  • run-loop.test:repeat guard 硬停映射为带标记 turn_limited;宿主 completionGuard stop 不误伤(保持 errored)
  • run-result.test(新增):带标记结果落 completed 而非 failed
  • agent-service.test:repeat_guard 恢复上下文注入与文案断言(不含 turn_limit 标记)

验证

Fixes #392

🤖 Generated with Claude Code

repeat guard 硬停(SDK error_completion_guard + errorCode repeated_tool_call)
此前被 run-loop 归入泛化 errored → 对外 failed,下条消息拿不到任何
runtime-recovery-state。

- run-loop 将其映射为带标记的 turn_limited(terminationReason: repeat_guard),
  不新增状态枚举,其余状态消费者零波及;宿主自有 completionGuard stop
  (无 errorCode)保持 errored 不变
- recordTurnLimited 落盘 item 携带 terminationReason 标记
- hasTurnLimitedMarker 语义不变;新增 isRepeatGuardLimitedItem 区分,
  恢复上下文按标记分流:<runtime-recovery-state reason="repeat_guard">
  文案说明因重复操作被保护机制停止并建议调整指令(风格对齐 turn_limit)
- onComplete reason 扩为 max_turns | repeat_guard 并透传
- web 端 #390 固定提示与 turn_limit 提示本就分事件/分文案呈现,无需改动

Fixes #392
@CavinHuang

Copy link
Copy Markdown
Owner Author

Review

结论:Approve(可合,无阻塞项)。核心映射、传播链、恢复分流逐条实证正确;以下三条 P3 均不阻塞。

发现

[P3] PR 描述对区分机制的表述与实现不符

描述称「宿主自有 completionGuard stop(无 errorCode)保持 errored」。实际现网唯一的宿主 stop 来源 coding-run-tracker.ts 两处均携带 errorCode(verification_inconclusive / verification_failed_after_repair);getTodoCompletionBlocker 只产 string 型 continue 反馈,不产 stop。代码用全等比较 errorCode === 'repeated_tool_call',行为完全正确,但真实区分机制是「errorCode 取值不同」而非「有无 errorCode」,建议修正描述,避免误导后续维护者对判据的心智模型。

[P3] 第二个 commit(f2e9ac11b)与本主题无关且 PR body 零提及

「Retry-After 解析收敛至 SDK 导出」(packages/sdk/src/index.ts 三符号导出 + pi-ai-provider.ts 去重 -6 行):已核实三个符号在 main 的 utils/retry.ts 均有现成实现,纯补导出,功能安全。但这是独立的传输层去重重构,混入 #392 且描述只字未提——reviewer 对着 +204/-12 找不出对应主题。建议后续此类顺手修单独成 PR,或至少在 body 里交代一句。

[P3] web 对两类 guard stop 仍不可区分(既有局限,非本 PR 引入)

lifecycle-projector 的 run.end detail 只含 stopReason/isError/numTurns/usage/cost,不透传 errorCode/errors[]。web adapter 对一切 completion_guard 渲染同一条「本轮检测到重复执行相同操作」文案——宿主 verification 类 stop 在 web 上也被这样标注(main 上已如此)。本 PR 服务端恰好建好了精确区分能力,follow-up 可考虑把 errorCode 加进 RunEndDetail 让 web 按类分文案。

另注一处测试缺口(不计级):生产端(run-loop)与消费端(agent-service)分别钉死,但缺 items.jsonl 落盘→读回的序列化往返用例;两端各自覆盖下实际风险很低。

已核实

  1. 识别条件可靠性repeated_tool_call 全仓唯一产出点 = sdk engine.ts repeatGuardPreCheck 硬停路径,经 result 消息结构化 errorCode 字段透出(types.ts 有 errorCode?: string 类型声明),拼写逐字符一致;宿主 stop 携带不同取值,全等比较无误判可能;run-loop 新分支插在 error_max_turns 之后、泛化 errored 之前,位置正确。

  2. 传播链逐跳完整:run-loop 返回 {status:'turn_limited', terminationReason:'repeat_guard'} → lume-runner {...result} 展开保留(satisfies AgentRuntimeRunResult)→ recordTurnLimited(errorMessage, terminationReason)(全仓唯一调用方)→ run-observer payload 条件展开 → items.jsonl 纯 JSON.stringify/parse 往返(readJsonlFile 无 schema 剥离,LumeSystemEventItem.payload 为 unknown 宽类型)→ store.get 读回 → isRepeatGuardLimitedItem 判 payload。旧记录无该字段 → undefined → 走 turn_limit 文案,向后兼容正确(历史 repeat-guard 收场本就落 errored 而非 turn_limited item,不存在会被误分流的旧数据)。

  3. hasTurnLimitedMarker 零变化属实:diff 仅新增并行函数,原判定逻辑未动一行。main 上它匹配两种形态(name=turn_limited 的 system_event / payload.subtype=error_max_turns 的 result item),新函数 isRepeatGuardLimitedItem 是其真子集。两 marker 并存时 some(isRepeatGuardLimitedItem) 确定性胜出;单 run 实际只落一个 turn_limited item(recordTurnLimited 单调用方、每 run 至多一次),混合仅理论场景。

  4. reason 扩展涟漪面为零:全仓没有任何运行时消费方读取 onComplete 的 payload.reason——rpc agent-handlers 与 im-message-router 的 onComplete 均为忽略 payload 的空实现,agent-notification-service 仅透传给同样忽略的下游。本次是纯类型层管道扩展 + 两处测试 mock 同步,不存在穷举遗漏导致 fallthrough 的可能。

  5. web 零改动声明成立:web 的 run 终态文案来自 SDK 总线 lifecycle-projector(stopReason = 原始 result subtype error_completion_guard),sidecar run-loop 的映射发生在另一条独立消费路径上,不影响总线事件。web adapter 的 includes('completion_guard') 分支继续命中 🐛 fix(sdk): repeat guard 加固——交替击穿/同批副作用/误伤面收敛 #390 专属文案「本轮检测到重复执行相同操作,已由保护机制停止;当前进度已保存」,不会退回 turn_limit 文案;附带效果是该文案里的「进度已保存」在新语义下从虚变实(此前 errored 路径恢复链路缺席)。

  6. 测试质量:新增 5 用例(run-loop 2 + run-result 2 + agent-service 1)全部经公开入口断言行为结果(映射终态对象、注入 prompt 的标签与文案、不含 turn_limit 标记的反向断言),非实现细节;agent-service 用例走完整 sendAgentMessage + 真实文件 store,可信度高。

  7. 其他:CI 4/4 全绿;commit 划分主题清晰(除上述 P3 所指的第二 commit);commit 与 PR 文案无竞品表述、符合 emoji 前缀规范;无其他 scope creep。

@CavinHuang

Copy link
Copy Markdown
Owner Author

已剔除与 #392 无关的 retry 导出 commit(该改动归口 #468/#422),并按 review 修正描述中关于宿主 stop errorCode 的表述。

TaTaLiao and others added 2 commits August 23, 2026 09:06
与 PR 描述对齐:宿主 completionGuard stop 同样带结构化 errorCode(verification_inconclusive / verification_failed_after_repair 等),与 repeat guard 的区分机制是取值不同而非有无字段。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CavinHuang
CavinHuang merged commit ce46bae into main Aug 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agent-service 为防护性终止(repeated_tool_call)提供独立 reason 与恢复上下文

2 participants