首页
/ DeepSeek Harness PTC 模式 run_code 结果卡片单源渲染:让持久化内容成为卡片的唯一内容来源

DeepSeek Harness PTC 模式 run_code 结果卡片单源渲染:让持久化内容成为卡片的唯一内容来源

2026-09-04 14:40:27作者:田桥桑Industrious

DeepSeek Harness 的 PTC(Program-as-Tool-Calling)模式下,run_code 工具让模型用一段 TypeScript/Python 程序编排多个子工具调用,其 UI 结果卡片必须完整呈现程序日志与返回值。本文基于该仓库的一条已归档 Agent Note(2026-07-20-code-mode-result-card-completeness.md),讲清这个"结果卡片完整性"问题的成因、最终决策(由工具注册表管线独占最终内容、run_code 有意省略 presentResult)、对应的测试与快照证据,以及四条被否决的替代方案。读完你能理解 DeepSeek Harness 工具呈现层"单一事实来源"的设计契约,并掌握在工具插件中正确实现结果展示的方式。

背景:PTC 模式下的 run_code 与结果卡片

packages/core/tools 中,工具注册表通过 mode 配置决定模型看到什么:native(完整工具 schema)、ptc(只有 run_code 加生成的 SDK)或 both。PTC 模式下,模型提交一段 run_code 程序,程序内以 await tools.name(args) 的形式发起嵌套子调用;每次子调用会重入完整的工具执行管线,并记录与外层调用的关联日志。

关键契约是:中间绑定值是执行期本地的(execution-local),只有外层 run_code 的结果进入模型历史,也只有一张卡片对应一次外层调用。这个"一张卡片"的边界正是本次问题修复所保护的。

问题:卡片内容的"分裂所有权"

原实现中,run_codeoutput.render 会把捕获的日志与返回值渲染成完整内容并持久化,但它的 UI 呈现器(presenter)忽略了这份完整内容,转而从一份只含日志(logs-only)的 presentationMeta 投影重建卡片正文。由此出现一个隐蔽的故障模式:

  • 只有返回值、没有日志时:呈现器产出的正文为空,消费方回退到 tool/result.content(持久化的完整渲染内容),卡片看起来正确;
  • 程序一旦 print 出日志:呈现器拿到非空内容,回退停止,卡片只显示日志,返回值从完成态卡片上消失
  • 溢写策略(spill policy)产生的头/尾预览(head/tail preview)同样暴露在这个分裂所有权之下——只要捕获日志让旧投影变非空,预览也会被丢弃。

此外,嵌套的 Code 调用从不拥有卡片,因此为了外层调用专门生成元数据去重建一张残缺卡片,还会模糊"一次外层 run_code 调用恰好一张卡片"的预期边界。

决策:规范管线独占最终内容,run_code 不声明 presentResult

修复的核心是重新划清所有权边界,包含四个相互咬合的决策:

1. 工具注册表管线拥有模型可见的最终外层内容

packages/core/tools/src/ptc.ts#L325-L329 中,run_code 的输出渲染器定义了唯一的模型可见文本契约:成功时依次拼接捕获日志、返回值(或显式的无输出标记);两者皆空时输出 (run_code completed with no output)

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)' }]
},

其中 renderValueptc.ts#L257-L260)对字符串原样输出,对非字符串 JSON 根使用两空格缩进的迭代式 JSON 呈现器(renderJsonValue,缩进总量封顶 10 字符、深层子树紧凑化,保证格式化输出规模与规范 JSON 线性相关)。

失败路径不走这个渲染器:运行期失败与执行前的策略拒绝由 ToolRegistry 直接归一化为错误内容。例如 ptc.ts#L640-L643 中,运行失败抛出 CodeRunFailedError,最终呈现为 Error: code run failed (<kind>): <message> 并条件性附加 Captured output: 与捕获日志。而 post-execute 阶段的 block 决策在成功渲染之后执行,可把结果替换为错误内容;其他 post-execute 策略决策与溢写决策则可能在持久化之前替换内容——这些替换都发生在渲染器下游,因此天然被纳入"最终内容"。

2. run_code 有意省略 presentResult,走通用结果回退

packages/core/tools/src/presentation.ts#L133-L138 定义了结果视图词汇表(ToolResultView)的通用规则:"省略 presentResult 方法时,保留 pending 态标题并渲染原始结果内容"。run_code 正是主动利用这条通用回退:

// The model-authored description is the call's always-visible UI label
// (the bash `description` precedent); the program itself rides rawInput.
presentCall: args => ({
  card: 'generic',
  title: args.description,
  kind: 'execute',
  rawInput: args.code,
}),
// 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.

ptc.ts#L652-L663。由此,那张持久化、可重放、策略后(post-policy)的 tool/result.content 成为卡片的唯一结果内容来源:模型看到什么,UI 就回放什么,包括溢写策略替换后的头/尾预览。

这也解释了 Host 侧的配套决策:Host API 代理因此再序列化的独立结果视图,避免同一份内容同时出现在 event.data.contentview.view.content 两处;原来那份冗余的 logs-only presentationMeta 投影则被彻底移除。

3. 嵌套派发边界保持不变

嵌套派发逻辑见 ptc.ts#L467-L603:每个绑定调用分配形如 ${callId}:code:${n} 的子调用 ID,并携带 parent 标记(ptc.ts#L473-L482)。带 exec.parent 标记的调用只会追加 tool/code-dispatch 事件——其中 content 字段是子调用的完整渲染内容ptc.ts#L506-L524 中经 shapeDispatchLog 瀑布处理后追加)——但不产生 tool/call / tool/result 事件,即不产生卡片。子调用的并发由每运行的调度器池约束(配置项 maxParallelSubCalls,默认 10,见 packages/core/tools/README.md),exclusive 分类的调用会独占屏障直到 commit(含 post-execute)完成。

结果是一次外层 run_code 调用恰好产生一张卡片;中间值有意保持执行期本地,模型与用户看到的就是那一次 Code Mode 操作本身。

验证:单元测试、host-mux 回归与端到端快照

工具单测:驱动六种结局并钉死内容

packages/core/tools/tests/ptc.spec.ts#L1330-L1347 用参数化用例把六种结局逐一驱动过规范注册表,并在每个用例中断言 presentResult 不存在:

it.each([
  ['logs only', { logs: ['printed'] }, 'printed'],
  ['result only', { logs: [], value: 'returned' }, 'returned'],
  ['logs plus result', { logs: ['printed'], value: 'returned' }, 'printed\nreturned'],
  ['no output', { logs: [] }, '(run_code completed with no output)'],
] as [string, CodeRunResult, string][])('keeps %s in durable content without a result presenter', async (_name, output, text) => {
  ...
  expect(result.content).toEqual([{ type: 'text', text }])
  expect('presentResult' in tool).toBe(false)
})

另外两个用例分别钉住策略后内容与失败内容:

  • 溢写预览ptc.spec.ts#L1349-L1363):注册一个 tools/post-execute 监听器把结果替换为 HEAD\n\n(Omitted 100 bytes. Full formatted result stored at: ...)\n\nTAIL 形式的预览,断言该预览进入持久内容且工具仍无 presentResult
  • 失败结局ptc.spec.ts#L1365-L1381):运行以 output-limit 失败且带捕获日志,断言持久内容为 Error: code run failed (output-limit): ...\nCaptured output:\ncaptured before failure

这些用例的共同目标是证明:旧元数据无法再顶替最终内容,同时 host 也不会因此复制该内容。host-mux 回归则用一个 call-only 呈现器证明:结果帧恰好携带一次原始内容、且没有伴随的 view 载荷。

端到端快照:外层一张卡片,嵌套零卡片

keyless ACP 后端与 TUI Code Mode 的快照执行同一个外层程序:两次嵌套 bash 调用、printcaptured output、返回 CODE_ONE+CODE_TWO。会话级快照 snapshots/session/ptc-turn/session.jsonl 中持久化了完整结果;从源码结构看,ACP 日志钉住的是完整渲染文本,TUI 表面显示一张完成态外层卡片(含日志行与返回值两行),且没有任何嵌套卡片。

被否决的替代方案及原因

原 Agent Note 明确记录了四条被否决的路径,值得逐条对照:

方案 否决原因
把返回值追加进 logs 元数据 元数据将复制渲染器逻辑,需要为每种 JSON 根维护第二套稳定格式化契约,且仍会漏掉 post-policy 内容替换与溢写预览这两种下游替换
把 presenter 元数据与 result.content 合并 渲染内容本身已包含日志,合并会造成日志重复,并引入脆弱的去重逻辑
经通用结果 presenter 转发 result.content 持久化事件本身已携带该内容,且 UI 消费方已有通用原始内容回退。Host mux 会在事件旁序列化一份工具自有的结果视图,转发等于仅为重建回退而在一帧内重复一次渲染内容;相比之下,默认 worker 在渲染前就允许 64 MiB 的变量载荷预算,转发并无收益
为每个嵌套派发创建一张卡片 中间值是有意执行期本地、从不模型可见的;多卡片会暴露实现轨迹,而非模型与用户实际发起的那一次 Code Mode 操作

从源码结构看,这些否决理由与最终实现一一对应:output.render 是唯一格式化点(方案一、二会引入第二套契约);presentResult 缺席即方案三的取舍;tool/code-dispatch 只记日志不建卡片即方案四的落实。

影响与兼容性

  • 显示一致性:TUI 与 JSON-RPC/Web 显示的内容与模型收到、回放持久化的内容完全一致,包括 post-policy 溢写预览,全部经由通用结果回退送达;
  • 载荷不膨胀:Host API 保留 pending 程序标题,但不再把原始结果复制到独立 view 载荷中;
  • 无会话格式升级:新 run_code 结果不再携带可选的 logs 元数据,但由于呈现层读取的是持久化渲染内容,既有会话记录依然有效,无需会话格式版本升级(session-format bump);
  • 适用前提:非 native 模式(ptc/both)要求已组装的 ctx.codeRuntime 且其语言有注册的 SDK 渲染器;maxParallelSubCalls(默认 10)约束嵌套调用的重叠上限,1 恢复严格串行派发。

延伸阅读

  • tools 子系统文档工具执行管线tools/pre-execute → guards → tools/executetools/post-executefinalizeContenttools/result 的完整管线类型与可视化;
  • @deepseek-ai/dsh-tools 包文档:PTC 模式、defineTool DSL 与 Model Experience 一节对模型可见结果的权威描述;
  • 呈现意图词汇表ToolCallView / ToolResultView 各卡片变体(generic、terminal、diff、search、read、web)的定义与回退规则;
  • run_code 传输实现:语言风味解析、绑定构建、每运行调度器与 quiescence 排空逻辑的完整源码;
  • PTC 工具测试:本文引用的内容完整性、失败归一化与事件日志用例;
  • 会话快照:外层一张卡片、嵌套零卡片的端到端持久化证据。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
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
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384