十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepSeek Harness 轮次结束原因通知:让 TUI 中每一次 stop 都有用户可见的解释

DeepSeek Harness 轮次结束原因通知:让 TUI 中每一次 stop 都有用户可见的解释 DeepSeek Harness 轮次结束原因通知让 TUI 中每一次 stop 都有用户可见的解释【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness本篇技术笔记基于仓库中归档的 Agent Note2026-07-24-tui-turn-end-stop-reason-notices.md深入解析 DeepSeek Harness 中轮次turn结束必须有可见原因这一产品约定的设计与实现从可合并扩展的TurnEndReasonMap类型契约到turn/end事件在 agent-loop 中的产生路径再到 TUI 转录区通知的渲染策略与备选方案权衡。读完本文你将掌握轮次结束原因体系的全部 kind、其产生与持久化机制以及如何确保插件新增的结束原因在界面上按名呈现。问题背景为什么每一个 stop 都必须被解释DeepSeek Harness 的 TUI终端界面会在转录区transcript渲染轮次结束通知。修复前TUI 已经为error、aborted、max-tokens、rejected、interrupted这几种轮次结束渲染通知但存在两个漏洞disposed轮次结束没有通知当 agent 被释放dispose导致正在运行的轮次中断时界面没有任何提示插件新增的TurnEndReasonMap分支没有通知TurnEndReasonMap是一个可合并扩展merge-extensible的接口任何插件都可以通过模块声明合并为其追加新的结束原因而 TUI 的默认分支当时保持静默。结果就是当这类轮次结束时——无论是实时运行还是从持久化日志回放replay——agent 停止工作但用户看不到任何原因违背了每一次停止都要向用户解释的产品期望。需要说明的是该笔记为 2026-08-04 归档的决策记录笔记中提到的 TUI 实现路径packages/ui/tui/src/index.ts在当前仓库中已不在原位置但其核心机制——TurnEndReasonMap契约与turn/end事件——仍然完整存在于会话类型定义中并继续驱动各前端界面的展示逻辑。轮次结束原因体系TurnEndReasonMap类型契约轮次结束原因的权威定义位于 packages/core/session/src/types.ts。它是一个可合并扩展的求和类型merge-extensible sum type/** * Why a turn ended. Merge-extensible sum type. */ export interface TurnEndReasonMap { completed: { kind: completed } /** A cancellation request interrupted the live turn. */ aborted: { kind: aborted; reason: TurnEndCancelCause } blocked: { kind: blocked } /** * The turn failed. error is always a structured failure: the LlmError * facts verbatim, or { message: errorChain(error), code: UNKNOWN } * flattened from any other error. */ error: { kind: error; error: LlmFailure } /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */ max-tokens: { kind: max-tokens } /** * A persistence backend closed a crash-orphaned turn on reload. The loop never * emits this marker, and the events recorded before the crash remain intact. */ interrupted: { kind: interrupted } } /** The union over {link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */ export type TurnEndReason TurnEndReasonMap[keyof TurnEndReasonMap]当前仓库内置六种 kind各具明确语义kind语义说明completed正常完成轮次自然结束无需额外提示aborted被取消携带reason: TurnEndCancelCauseuser/parent/hook/disposed/legacy见 types.tsblocked被拦截进入 step 前被pre-step拒绝error结构化失败总是携带LlmError事实或{ message, code: UNKNOWN }扁平化结构max-tokens触发输出 token 上限粘性标记即使后续 step 正常完成也不降级interrupted崩溃孤儿轮次由持久化后端在重载时补写agent-loop 自身从不发出TurnEndReason通过TurnEndReasonMap[keyof TurnEndReasonMap]导出为联合类型。这种接口 索引访问的写法使得插件可以通过 TypeScript 的声明合并declaration merging向TurnEndReasonMap追加新分支TurnEndReason联合类型会自动扩展——这正是笔记中所说插件新增的 kind的来源。同样的合并策略也用于SessionEventMap见 types.ts其中定义了轮次关闭事件/** * Closes turn turn with the {link TurnEndReason} that ended it. ... */ turn/end: { turn: number; reason: TurnEndReason }原因从何而来agent-loop 中turn/end的产生路径turn/end事件由 agent-loop 的轮次驱动逻辑在 packages/core/agent-loop/src/agent.ts 中产生。核心循环要点如下每次轮次开始先追加turn/start随后在while (true)中反复执行preStep→step/start→ 模型调用 →step/end各分支分别决定turnEnds的取值preStep拒绝turnEnds { kind: blocked }L275没有模型调用的空轮次turnEnds { kind: completed }L282单步结束后stepEnd记为轮次结果其中max-tokens是粘性的一旦某一步触顶后续正常完成的 step 不得把轮次结果降级L292-L297捕获到取消信号turnEnds { kind: aborted, reason: signal.reason }L311任何其他失败结构化为errorLlmError保留事实其余扁平化为UNKNOWN码见 L316-L321最后在finally中无论何种路径都追加turn/endL326this.session.append(turn/end, { turn, reason: turnEnds! })保证每个轮次都有且只有一个结束事件。而disposed取消原因的产生点在 packages/core/agent-loop/src/index.tsagent 的生命周期dispose会执行abort.abort(...)并调用machine.cancel({ kind: disposed })随后等待whenIdle()静默退出。也就是说disposed轮次结束正是agent 被释放时正在运行的轮次被中断这一事实在事件流中的落点。修复方案turn/end分支的全覆盖策略笔记记录的决策Decision是在 TUI 的turn/end分支中按原因的判别属性discriminantswitch 并覆盖每一个 kind具体规则如下completed保持静默已落定的助手消息assistant message及其携带的Completed计时头部timing header本身已经呈现了正常结束这一结果追加通知只会产生冗余disposed追加Turn stopped: the agent was disposed.明确告诉用户本次轮次被截断合并可扩展的默认分支追加Turn ended: kind.对 TUI 未知的插件新增结果按 kind 名称点名说明 agent 为何停止其余所有既有 kinderror、aborted、max-tokens、interrupted等保留原有通知文案不变。这样任何非completed的turn/end都会在转录区产生一条用户可见的原因通知——包括按名呈现的未知插件结果。其背后的工程哲学是未知恰恰是用户最需要被解释的情况因为此时界面上没有任何其他途径可以获知 agent 停止的原因。这一设计思路在当前仓库的同类实现中也能看到印证Web 客户端的max-tokens轮次结束通知测试 max-tokens-notice.expected.e2e.ts 明确验证在被截断的回答之后渲染本地化截断通知而不是无声结束并专门区分了通知行与错误行防止回归把通知路由成错误样式——可见轮次结束必须有可见通知是该产品在多个前端的共同约定。备选方案权衡为什么不做全量通知与静默 disposed笔记记录了三个被否决的备选方案其权衡过程对理解设计意图很有价值方案一为completed也追加通知。否决理由这是噪音。每一次普通响应都会多出一行冗余文本而助手消息加其冻结的计时头部已经标记了完成。也就是说完成的结果载体是消息本身不是通知。方案二因为agent/disposed事件也会追加Agent id was disposed.就抑制实时场景下的disposed轮次结束通知。否决理由两条通知陈述的是不同事实——前者是这个轮次被截断了后者是这个 agent 不存在了更重要的是轮次结束通知是持久化日志回放后唯一存活的提示——回放时实时的agent/disposed事件不会再触发只有转录区里记录下来的轮次结束通知能够向用户解释停止原因。agent/disposed事件的实时分发实现在 packages/core/agent/src/index.tsagent 注册时发agent/created、释放时发agent/disposed其语义在 packages/core/agent/README.md 中有明确说明。方案三默认分支保持静默即修复前行为。否决理由对 TUI 未知的合并扩展 kind用户没有任何其他途径获知 agent 为何停止——这正是按名呈现默认分支存在的意义。修复影响与验证修复后的行为边界Consequences如下轮次不会在无原因的情况下结束所有非completed的turn/end都会追加转录通知包括按名呈现的未知插件 kind实时释放与日志回放的差异运行中释放 agent 会同时出现两条通知轮次结束通知 agent/disposed而回放持久化日志时只显示轮次结束通知这一条快照回归保护errors-and-help快照将disposed与未知 kind 的通知与既有的失败、中断通知一同固定防止后续改动破坏该行为。延伸阅读结束原因在其他子系统中的消费TurnEndReason并不只被 TUI 消费它在仓库多个子系统中承担语义角色可作为理解该体系的旁证崩溃修复packages/core/session/src/repair.ts 中的interruptedTurnClosers在重载崩溃孤儿日志时为未关闭的轮次补写turn/endreason 为{ kind: interrupted }——这正是loop 从不发出 interrupted这一注释的实现对应全文检索packages/session-query/session-query/src/extraction.ts 的turnEndText将error提取为错误消息、aborted/max-tokens/interrupted提取为对应关键词completed与未知 kind 不产生语义文本遥测分类packages/session/session-telemetry/src/coordinator.ts 将turn/end事件按 reason 分类为error或info事件流示例在apps/cli的预期会话日志如 session.expected.jsonl中可以直观看到type:turn/end,data:{turn:1,reason:{kind:completed}}以及携带error结构的失败结束事件是理解序列化格式的最佳参考。综上DeepSeek Harness 用可合并扩展的原因枚举 事件流落盘 前端按名兜底通知三层机制实现了每一次停止都有解释这一对用户最基本的承诺。对于需要扩展轮次结束语义的插件开发者向TurnEndReasonMap合并新分支之后务必同时为消费方界面通知、检索提取、遥测分类补充对应文案与语义处理避免新原因重新掉进无声结束的缺口。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表