Keep the Code Mode result card complete
保证 Code Mode 结果卡片内容完整
The outer `run_code` tool persisted complete rendered content, but its UI presenter ignored that content and rebuilt the card body from a logs-only `presentationMeta` projection. A result-only run appeared correct because an empty presenter body let consumers fall back to `tool/result.content`. Once the program emitted a log, the presenter supplied non-empty content, that fallback stopped, and the returned value disa
English
Problem
The outer run_code tool persisted complete rendered content, but its UI presenter ignored that content and rebuilt the card body from a logs-only presentationMeta projection. A result-only run appeared correct because an empty presenter body let consumers fall back to tool/result.content. Once the program emitted a log, the presenter supplied non-empty content, that fallback stopped, and the returned value disappeared from the completed card. A spill policy's final head/tail preview was vulnerable to the same split ownership whenever captured logs made the stale projection non-empty.
Nested Code calls never owned cards, so producing metadata for the outer call solely to reconstruct one incomplete card also obscured the intended one-card boundary.
Decision
The canonical tool registry pipeline owns the final model-facing outer content. On success, the run_code output renderer renders captured logs followed by the return value or the explicit no-output marker. Runtime failures and pre-execution policy denials are normalized into error content by ToolRegistry without invoking that renderer. A post-execute block runs after successful rendering and replaces the result with error content; other post-execute policy and spill decisions may replace content before persistence.
run_code omits presentResult. The established generic result fallback keeps the pending program title and renders the raw final tool/result.content; that durable, replayable, post-policy projection is the card's only result-content source. The host API proxy therefore omits a separate result view instead of serializing the same content in both event.data.content and view.view.content. The redundant logs-only presentationMeta projection remains removed.
Nested dispatch remains unchanged. Calls marked by exec.parent emit tool/code-dispatch events (full rendered content) but no tool/call or tool/result surface cards, so one outer run_code invocation still produces exactly one card.
Testing
Tool unit coverage drives logs-only, result-only, logs-plus-result, no-output, spilled-result, and failure outcomes through the canonical registry, then pins the durable content and absence of a result presenter. A host-mux regression uses a call-only presenter to prove the result frame carries raw content exactly once and no view. These cases prove stale metadata cannot replace final content without making the host duplicate that content.
The keyless ACP backend and TUI Code Mode snapshots execute one outer program that performs two nested bash calls, logs captured output, and returns CODE_ONE+CODE_TWO. The persisted ACP log pins the complete result; the TUI surface shows one completed outer card containing both lines and no nested cards.
Alternatives considered
Append the return value to logs metadata. Rejected because metadata would duplicate the renderer, need a second stable formatting contract for every JSON root, and still miss post-policy content replacement or spill previews.
Merge presenter metadata with result.content. Rejected because the rendered content already contains the logs; merging would duplicate them and require brittle deduplication.
Forward result.content through a generic result presenter. Rejected because the durable event already carries that content and UI consumers already have a generic raw-content fallback. The host mux serializes a tool-owned result view beside the event, so forwarding would duplicate the rendered content in one frame merely to recreate the fallback; the default worker alone admits a 64 MiB variable-payload budget before rendering.
Create one card per nested dispatch. Rejected because intermediate values are intentionally execution-local and never model-facing. Multiple cards would expose an implementation trace instead of the single Code Mode operation the model and user invoked.
Consequences
TUI and JSON-RPC/Web display the same complete content the model receives and replay persists, including post-policy spill previews, through their generic result fallback. The host API retains the pending program title without duplicating the raw result in a separate view payload. New run_code results no longer carry the optional logs metadata, but this requires no session-format bump: existing records remain valid because presentation reads their durable rendered content.
中文
问题
外层 run_code 工具会持久化完整的渲染内容,但其 UI 展示器忽略了这些内容,转而根据仅含日志的 presentationMeta 投影重新构建卡片正文。仅有结果的运行看似正确,是因为展示器正文为空时,消费方会回退到 tool/result.content。只要程序输出一条日志,展示器就会提供非空内容,回退随即停止,返回值便会从完成态卡片中消失。当已捕获的日志使陈旧投影变为非空时,输出落盘策略最终生成的头尾预览也会受到同一职责拆分的影响。
嵌套 Code 调用从不生成自己的卡片。因此,仅仅为了重建这一张不完整卡片而给外层调用生成元数据,还掩盖了每次外层调用只生成一张卡片的预期边界。
决策
规范的工具注册表流水线负责最终面向模型的外层内容。成功时,run_code 输出渲染器先渲染已捕获的日志,然后渲染返回值或显式的无输出标记。运行时失败和执行前策略拒绝由 ToolRegistry 归一化为错误内容,过程中不会调用该渲染器。Post-execute 阻断发生在成功渲染之后,并把结果替换为错误内容;其他 post-execute 策略与输出落盘决策可以在持久化之前替换内容。
run_code 不提供 presentResult。既有的通用结果回退机制会保留待完成的程序标题,并渲染原始的最终 tool/result.content;这一持久、可回放且经过 post-policy 处理的投影是卡片中结果内容的唯一来源。宿主 API 代理因此不提供单独的结果视图,而不会在 event.data.content 与 view.view.content 中重复序列化同一内容。冗余的仅含日志的 presentationMeta 投影继续保持移除状态。
嵌套分发保持不变。带有 exec.parent 标记的调用会发出 tool/code-dispatch 事件(携带完整渲染内容),但不会生成与 tool/call 或 tool/result 对应的界面卡片,因此一次外层 run_code 调用仍然只会生成一张卡片。
测试
工具单元测试通过规范注册表覆盖仅有日志、仅有结果、日志与结果并存、无输出、结果落盘和失败的结果,然后固定持久内容以及结果展示器不存在这一事实。宿主 mux 回归测试使用仅有调用的展示器,证明结果帧恰好携带一次原始内容,且不含视图。这些案例证明陈旧元数据无法替换最终内容,同时不会让宿主重复该内容。
无密钥的 ACP(Agent Client Protocol)后端快照与 TUI Code Mode 快照会执行一个外层程序:程序进行两次嵌套 bash 调用,记录 captured output,并返回 CODE_ONE+CODE_TWO。ACP 持久化日志固定完整结果;TUI 界面只显示一张完成态外层卡片,其中包含这两行内容,且没有嵌套卡片。
备选方案
**把返回值追加到日志元数据:**不予采纳。元数据会与渲染器重复,并且需要为每一种 JSON 根另行维护稳定的格式化契约;post-policy 内容替换或输出落盘预览仍然会被遗漏。
**把展示元数据与 result.content 合并:**不予采纳。渲染内容已经包含日志;合并会造成重复,还需要依赖脆弱的去重逻辑。
**通过通用结果展示器转发 result.content:**不予采纳。持久事件已经携带该内容,UI 消费方也已有通用的原始内容回退机制。宿主 mux 会在事件旁序列化工具拥有的结果视图,因此转发仅仅是为了重建该回退机制,却会在一个帧中重复渲染内容;仅默认 worker 在渲染前允许 64 MiB 的可变载荷预算。
**为每次嵌套分发创建一张卡片:**不予采纳。中间值有意只存在于执行期间,永远不面向模型。多张卡片会暴露实现轨迹,而不是模型与用户调用的单次 Code Mode 操作。
影响
TUI 与 JSON-RPC/Web 通过通用结果回退机制显示与模型接收及回放持久化相同的完整内容,其中包括 post-policy 输出落盘预览。宿主 API 保留待完成的程序标题,同时不在单独的视图负载中重复原始结果。新的 run_code 结果不再携带可选的日志元数据,但无需提升会话格式版本:现有记录仍然有效,因为展示逻辑会读取其中持久化的渲染内容。