首页
/ DeepSeek Harness 中 Code Mode 结果卡片的完整性:run_code 输出与返回值的单一来源渲染

DeepSeek Harness 中 Code Mode 结果卡片的完整性:run_code 输出与返回值的单一来源渲染

2026-09-04 16:53:37作者:贡沫苏Truman

本文基于 DeepSeek Harness 仓库中归档的 Bug 修复记录《保证 Code Mode 结果卡片内容完整》2026-07-20-code-mode-result-card-completeness.zh.md 展开。它解释了 run_code(Code Mode / PTC mode)工具在“程序打印了日志”时,返回值为何会从 UI 完成态卡片中消失,以及项目如何把最终内容的唯一来源收归到规范的工具注册表流水线。读完后,你将掌握 run_code 结果的完整渲染链路、presentResult 缺席的设计意图,以及嵌套 Code 调用“只留事件、不生成卡片”的边界约定。

背景:Code Mode 下的 run_code 与内容分工

DeepSeek Harness 的工具注册表支持三种呈现模式(native / ptc / both),由 agent-tool-presentation 插件通过 ctx.tools.presentAs() 声明:ptc 模式下模型只能看到 run_code 加一段自动生成的 SDK 声明段,程序以 await tools.name(args) 的形式调用宿主注册的全部工具。packages/core/agent-tool-presentation/src/index.ts 与模式矩阵测试 packages/core/tools/tests/ptc.spec.ts 固定了这一贡献规则。

run_code 的传输层实现在 packages/core/tools/src/ptc.ts,其输出契约是:

/** 外层 PTC 传输的规范输出。 */
type RunCodeOutput = { logs: string[]; result?: JsonValue }

即一次运行只向模型暴露两类东西:捕获的日志(print)和程序的返回值。工具的 output.render 决定了模型可见文本的拼装方式——先拼接全部日志,再渲染返回值(字符串原样、非字符串走无递归的 JSON 渲染),两者皆空时输出 (run_code completed with no output) 显式无输出标记。运行时失败(程序异常、预算耗尽、abort、runtime 崩溃)则以 CodeRunFailedError(错误码 CODE_RUN_FAILED)抛出,错误文本中附带已捕获的日志,交由注册表流水线转成 isError 结果供模型自我修正。

在此分工下,UI 展示侧(presenter)与注册表侧存在一条清晰的责任边界:注册表流水线负责最终面向模型的内容,UI 负责把这份持久内容投影成卡片。这条边界正是本次 Bug 的引爆点。

Bug 分析:为什么返回值会从完成态卡片中消失

原问题描述可以拆解为一条完整的因果链:

  1. 外层 run_code 工具持久化了完整的渲染内容(日志 + 返回值的拼接文本),它随 tool/result 事件落盘,可回放。
  2. 但 UI 展示器忽略了这份持久内容,转而根据仅含日志的 presentationMeta 投影重新构建卡片正文。
  3. 当程序只有返回值、没有日志时,展示器重建出的正文恰好为空,UI 消费方会回退去读 tool/result.content——因此这类运行“看似正确”。
  4. 一旦程序打印了任何一条日志,展示器就能提供非空正文,回退机制随即停止工作,返回值便从完成态卡片中消失
  5. 同一条职责拆分还波及输出落盘策略:当已捕获的日志使陈旧投影变为非空时,spill 落盘最终生成的头尾预览也会基于不完整内容计算。

另一个被掩盖的问题是嵌套 Code 调用:嵌套分发从不生成自己的卡片,然而为了给外层重建这一张不完整卡片而生成元数据,恰好模糊了“每次外层 run_code 调用只生成一张卡片”的预期边界。

修复决策:让规范流水线独占最终内容

渲染顺序与错误归一化

成功路径上,run_code 输出渲染器的顺序是固定的:先渲染已捕获的日志,再渲染返回值或显式的无输出标记。源码中的 render 实现正是这一契约的落点:

render: (_args, value) => {
  const rendered = value.result === undefined ? '' : renderValue(value.result)
  const parts = [value.logs.join('\n'), rendered].filter(part => part.length > 0)
  return [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : '(run_code completed with no output)' }]
},

packages/core/tools/src/ptc.ts#L325-L329。失败路径则不经过该渲染器:运行时失败和执行前(pre-execute)策略拒绝由 ToolRegistry 统一归一化为错误内容;Post-execute 阻断发生在成功渲染之后,把结果替换为错误内容;其余 post-execute 策略与输出落盘(spill)决策可以在持久化之前替换内容。也就是说,无论哪条策略介入,卡片最终读取的持久内容都已经是经过完整流水线处理的结果。

移除 presentResult:通用回退成为唯一内容来源

最关键的决策是 run_code 不提供 presentResult。源码中这一意图有明确注释:

// Deliberately no presentResult: the generic card fallback keeps this
// title and reads durable result content without duplicating a large raw
// result into the host view payload.

packages/core/tools/src/ptc.ts#L660-L662。其含义是:

  • 既有的通用结果回退机制会保留 presentCall 声明的待完成程序标题(即模型撰写的 descriptioncard: 'generic'kind: 'execute'rawInput: args.code),并直接渲染持久化的最终 tool/result.content
  • 这份持久、可回放、且经过 post-policy 处理的内容,成为卡片中结果内容的唯一来源
  • 宿主 API 代理因此不再提供单独的结果视图,不会在同一帧中于 event.data.contentview.view.content 里重复序列化同一份内容;
  • 冗余的“仅含日志”的 presentationMeta 投影保持移除状态,陈旧元数据从此无法替换最终内容。

presentCall / presentResult 这套 provider 中立的展示意图词表定义在 packages/core/tools/src/presentation.tsgeneric 卡片默认形态即带标题、rawInput 与内容块的普通工具行。

嵌套分发:有事件、无卡片

嵌套 Code 调用的分发链路保持不变,边界更加明确:

  • 程序内每次 tools.name(args) 调用都会发出 tool/code-dispatch-starttool/code-dispatch 两类会话事件,携带完整渲染内容,但不生成tool/call / tool/result 对应的界面卡片;
  • 一次外层 run_code 调用仍然只生成一张卡片;嵌套值属于“执行期间存在的中间值”,永远不面向模型。

源码证据有两处。其一是事件追加现场:packages/core/tools/src/ptc.ts#L513-L524 在子调用 settle 时以 subCallId(形如 <outerCallId>:code:n)、parentCallId 与参数快照写入 tool/code-dispatch 事件;其二是事件类型注册表 packages/core/session/src/known-event-types.ts#L63-L64tool/code-dispatchtool/code-dispatch-start 列为一级会话事件。客户端侧,ui-tool 包按 root 调用树渲染,Code Dispatch 子调用保留 parentCallId 并以扁平 generic 形态呈现,见 packages/client/ui-tool/README.zh.md 的“内置视图”一节;快照语料 snapshots/session/ptc-turn/session.jsonl 也完整保留了这一类事件的落盘形态。

验证:三层测试固定不变式

修复按“单元—宿主—端到端”三层固化:

  1. 工具单元测试packages/core/tools/tests/ptc.spec.ts):通过规范注册表覆盖仅有日志、仅有结果、日志与结果并存、无输出、结果落盘与失败结果等情形,并固定“持久内容完整”与“结果展示器不存在”两个事实。例如“仅返回整理后输出、每次分发记录一个事件”的用例断言 result.content 为日志加返回值的拼接文本('saw echo:one\necho:two'),且 result.metaundefinedpackages/core/tools/tests/ptc.spec.ts#L823-L841)。
  2. 宿主 mux 回归测试packages/api/session-controller/tests/event-script.client.ts):使用仅有调用的展示器,证明结果帧恰好携带一次原始内容且不含视图——即陈旧元数据无法替换最终内容,宿主也不重复该内容。
  3. 端到端快照:无密钥的 ACP 后端快照与 TUI Code Mode 快照会执行一个外层程序——两次嵌套 bash 调用、记录 captured output、并返回 CODE_ONE+CODE_TWO。ACP 持久化日志固定了完整结果;TUI 界面只显示一张完成态外层卡片,内含这两行内容且没有嵌套卡片。会话级语料可参见 snapshots/session/ptc-turn/session.jsonl

被否决的备选方案

原决策记录完整列出了四条被否决的路线,其否决理由同样值得保留为设计依据:

备选方案 否决理由
把返回值追加到日志元数据 元数据会与渲染器重复;需要为每种 JSON 根另行维护稳定的格式化契约;post-policy 内容替换或输出落盘预览仍会被遗漏
把展示元数据与 result.content 合并 渲染内容已包含日志,合并会造成重复,且需依赖脆弱的去重逻辑
通过通用结果展示器转发 result.content 持久事件已携带该内容,UI 消费方已有通用的原始内容回退;转发只是为重建回退机制,却会在一个帧中重复渲染内容;仅默认 worker 在渲染前允许 64 MiB 的可变载荷预算
为每次嵌套分发创建一张卡片 中间值有意只存在于执行期间、永不面向模型;多张卡片会暴露实现轨迹,而非模型与用户调用的单次 Code Mode 操作

影响与兼容性

  • 一致性:TUI 与 JSON-RPC/Web 三种界面通过通用结果回退机制显示与“模型接收 + 回放持久化”完全相同的完整内容,包括 post-policy 输出落盘后的头尾预览(spill 预览的构建逻辑见 packages/spill/spill-policy/src/index.ts)。
  • 宿主 API:保留待完成程序标题,但不再在单独的视图负载中重复原始结果,消除了双份序列化。
  • 格式兼容:新的 run_code 结果不再携带可选的日志元数据,但无需提升会话格式版本——现有记录仍然有效,因为展示逻辑读取的是其中持久化的渲染内容。换言之,旧会话回放在新展示逻辑下自然获得完整卡片,无需迁移。

小结

这个 Bug 的根因不是渲染算法错误,而是“谁拥有最终内容”的职责漂移:UI 展示器试图基于过时的 presentationMeta 重建卡片正文,绕开了注册表流水线的持久输出。修复的核心动作是删减而非增加——移除 presentResult、移除日志元数据投影,让持久化的 tool/result.content 经通用回退成为卡片内容的唯一来源,同时以嵌套分发的 tool/code-dispatch 事件维持“一张外层卡片”的边界。这套“内容单一来源 + 回退投影”的模式,对任何需要在模型侧、持久化与 UI 三处呈现同一工具结果的系统都有参考价值。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384