Ver Fonte

fix: keep Code Mode result cards complete

Tianyi Cui há 2 meses atrás
pai
commit
7c2fb0a6fe

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write
+2026-07-20-code-mode-result-card-completeness.md: 65aad02713498f5dbeff5ab3886da558326a0003
+2026-07-20-code-mode-result-card-completeness.zh.md: 082e209544d4ea43f75bb979fc3aa490b426952e

+ 37 - 0
.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.md

@@ -0,0 +1,37 @@
+# Agent Note: Keep the Code Mode result card complete
+
+Status: implemented
+
+English | [中文](2026-07-20-code-mode-result-card-completeness.zh.md)
+
+## Problem
+
+The outer `run_code` tool persisted complete rendered content, but its editor 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 ACP and TUI 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. Failure text and a spill policy's final head/tail preview were vulnerable to the same split ownership.
+
+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 `run_code` output renderer remains the single owner of model-facing outer content. It renders captured logs followed by the return value, the explicit no-output marker, or the failure content produced by the canonical tool pipeline. Post-execute policy and spill may replace that content before it is persisted.
+
+`run_code.presentResult` now forwards the final `result.content` into one generic result card. It deliberately omits the title so the pending card retains the program text. The existing logs metadata remains in `tool/result` for transcript compatibility, but the presenter no longer treats it as a second content source: `tool/result.content` is the durable, replayable, post-policy projection.
+
+Nested dispatch remains unchanged. Calls marked by `exec.parent` emit bounded `tool/code-dispatch` diagnostics but no `tool/call` or `tool/result` surface cards, so one outer `run_code` invocation still produces exactly one card.
+
+## Testing
+
+Presenter unit coverage pins logs-only, result-only, logs-plus-result, no-output, failure, and spilled-result content. Every case proves stale metadata cannot replace the final content.
+
+The keyless ACP and TUI Code Mode snapshots execute one outer program that performs two nested bash calls, logs `captured output`, and returns `CODE_ONE+CODE_TWO`. Both surfaces show 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.
+
+**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
+
+ACP and TUI now display the same complete content the model receives and replay persists, including post-policy spill previews. The change adds or removes no event fields and requires no session-format bump. Existing and future replay records remain valid because the presenter ignores logs metadata when choosing card content and reads their durable rendered content.

+ 37 - 0
.agents/notes/implemented/bug-fix/2026-07-20-code-mode-result-card-completeness.zh.md

@@ -0,0 +1,37 @@
+# Agent Note: 保证 Code Mode 结果卡片内容完整
+
+Status: implemented
+
+[English](2026-07-20-code-mode-result-card-completeness.md) | 中文
+
+## 问题
+
+外层 `run_code` 工具会持久化完整的渲染内容,但编辑器的卡片展示逻辑忽略了这些内容,转而根据仅含日志的 `presentationMeta` 投影重新构建卡片正文。仅有结果的运行看似正确,是因为展示逻辑未提供正文时,ACP 和 TUI 会回退到 `tool/result.content`。只要程序输出一条日志,展示逻辑就会提供非空内容,回退随即停止,返回值便会从完成态卡片中消失。失败文本以及输出落盘策略最终生成的头尾预览,也会受到同一职责拆分的影响。
+
+嵌套 Code 调用从不生成自己的卡片。因此,仅仅为了重建这一张不完整卡片而给外层调用生成元数据,还掩盖了每次外层调用只生成一张卡片的预期边界。
+
+## 决策
+
+`run_code` 输出渲染器继续作为面向模型的外层内容的唯一所有者。它先渲染已捕获的日志,然后渲染返回值、显式的无输出标记,或规范工具流水线生成的失败内容。Post-execute 策略与输出落盘机制可以在内容持久化之前替换它。
+
+`run_code.presentResult` 会把最终的 `result.content` 转交给一张通用结果卡片。它有意省略标题,使待完成卡片保留程序文本。现有日志元数据仍保留在 `tool/result` 中,以维持 transcript(文本记录)兼容性;但展示逻辑不再把它视为第二个内容来源:`tool/result.content` 才是持久、可回放且经过 post-policy 处理的投影。
+
+嵌套分发保持不变。带有 `exec.parent` 标记的调用会发出有界的 `tool/code-dispatch` 诊断,但不会生成与 `tool/call` 或 `tool/result` 对应的界面卡片,因此一次外层 `run_code` 调用仍然只会生成一张卡片。
+
+## 测试
+
+展示逻辑的单元测试覆盖仅有日志、仅有结果、日志与结果并存、无输出、失败和结果落盘六种情况。每个用例都证明,陈旧元数据无法替换最终内容。
+
+无密钥的 ACP 与 TUI Code Mode 快照会执行一个外层程序:程序进行两次嵌套 bash 调用,记录 `captured output`,并返回 `CODE_ONE+CODE_TWO`。两个界面都只显示一张完成态外层卡片,其中包含这两行内容,且没有嵌套卡片。
+
+## 备选方案
+
+**把返回值追加到日志元数据:**不予采纳。元数据会与渲染器重复,并且需要为每一种 JSON 根另行维护稳定的格式化契约;post-policy 内容替换或输出落盘预览仍然会被遗漏。
+
+**把展示元数据与 `result.content` 合并:**不予采纳。渲染内容已经包含日志;合并会造成重复,还需要依赖脆弱的去重逻辑。
+
+**为每次嵌套分发创建一张卡片:**不予采纳。中间值有意只存在于执行期间,永远不面向模型。多张卡片会暴露实现轨迹,而不是模型与用户调用的单次 Code Mode 操作。
+
+## 影响
+
+ACP 和 TUI 会显示模型接收、回放持久化的同一份完整内容,其中包括 post-policy 输出落盘预览。该变更不增加或删除任何事件字段,也不需要提升会话格式版本。现有及未来的回放记录都保持有效,因为展示逻辑在选择卡片内容时会忽略日志元数据,转而读取记录中的持久化渲染内容。

+ 1 - 1
.agents/notes/implemented/feature/2026-06-15-code-mode.md

@@ -48,7 +48,7 @@ Under `'code'` and `'both'` the registry owns `run_code` as a reserved presentat
 
 **Concurrency is serialized.** Each run owns a dispatch queue, so even `Promise.all` executes tool calls in submission order. Settlement abandons queued calls that have not started. Parallelism requires per-tool concurrency-safety metadata.
 
-**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` → a `generic` card, `kind: 'execute'`, title = the program text, `rawInput` = the same program text; `presentResult` → a `generic` card whose content is the captured output (from `meta`). The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not.
+**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` → a `generic` card, `kind: 'execute'`, title = the program text, `rawInput` = the same program text; `presentResult` → a `generic` card whose content is the final durable `tool/result.content`, including captured logs plus the returned value, failure, or post-policy spill preview. The existing logs metadata remains replayable but is not a second content source. The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. See the [result-card completeness note](../bug-fix/2026-07-20-code-mode-result-card-completeness.md).
 
 ### Observability: `tool/code-dispatch`
 

+ 5 - 5
examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl

@@ -89,7 +89,7 @@
 {"type":"assistant/chunk","seq":87,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" echo"}}}
 {"type":"assistant/chunk","seq":88,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\""}}}
 {"type":"assistant/chunk","seq":89,"time":1783611772684,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" });\\n"}}}
-{"type":"assistant/chunk","seq":90,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"return"}}}
+{"type":"assistant/chunk","seq":90,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"console.log(\\\"captured output\\\");\\nreturn"}}}
 {"type":"assistant/chunk","seq":91,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" out"}}}
 {"type":"assistant/chunk","seq":92,"time":1783611772713,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"1"}}}
 {"type":"assistant/chunk","seq":93,"time":1783611772714,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":".stdout.text.trim"}}}
@@ -105,14 +105,14 @@
 {"type":"assistant/chunk","seq":103,"time":1783611772772,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\""}}}
 {"type":"assistant/chunk","seq":104,"time":1783611772773,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"}"}}}
 {"type":"assistant/chunk","seq":105,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."}}}}
-{"type":"assistant/chunk","seq":106,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}}}
+{"type":"assistant/chunk","seq":106,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nconsole.log(\\\"captured output\\\");\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}}}
 {"type":"assistant/chunk","seq":107,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}}}}
 {"type":"assistant/chunk","seq":108,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":109,"time":1783611772840,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."},{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108],"surfaceOp":"append"}
-{"type":"tool/call","seq":110,"time":1783611772840,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}
+{"type":"assistant/message","seq":109,"time":1783611772840,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."},{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nconsole.log(\\\"captured output\\\");\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108],"surfaceOp":"append"}
+{"type":"tool/call","seq":110,"time":1783611772840,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nconsole.log(\\\"captured output\\\");\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}
 {"type":"tool/code-dispatch","seq":111,"time":1783611772933,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"First echo"},"isError":false,"resultSummary":"CODE_ONE\n"}}
 {"type":"tool/code-dispatch","seq":112,"time":1783611772936,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Second echo"},"isError":false,"resultSummary":"CODE_TWO\n"}}
-{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"CODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[110],"surfaceOp":"append"}
+{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":["captured output"]}},"sourceEventSeqs":[110],"surfaceOp":"append"}
 {"type":"step/end","seq":114,"time":1783611772938,"data":{"turn":1,"step":1}}
 {"type":"step/start","seq":115,"time":1783611772938,"data":{"turn":1,"step":2}}
 {"type":"assistant/chunk","seq":116,"time":1783611773376,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}

+ 2 - 2
examples/acp-agent/tests/snapshots/code-mode-turn/stdout.expected.jsonl

@@ -30,8 +30,8 @@
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" write"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" this"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","title":"const out1 = await tools.bash({ command: \"echo CODE_ONE\", description: \"First echo\" });\nconst out2 = await tools.bash({ command: \"echo CODE_TWO\", description: \"Second echo\" });\nreturn out1.stdout.text.trim() + \"+\" + out2.stdout.text.trim();","kind":"execute","status":"in_progress","rawInput":"const out1 = await tools.bash({ command: \"echo CODE_ONE\", description: \"First echo\" });\nconst out2 = await tools.bash({ command: \"echo CODE_TWO\", description: \"Second echo\" });\nreturn out1.stdout.text.trim() + \"+\" + out2.stdout.text.trim();"}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","status":"completed","content":[{"type":"content","content":{"type":"text","text":"CODE_ONE+CODE_TWO"}}]}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","title":"const out1 = await tools.bash({ command: \"echo CODE_ONE\", description: \"First echo\" });\nconst out2 = await tools.bash({ command: \"echo CODE_TWO\", description: \"Second echo\" });\nconsole.log(\"captured output\");\nreturn out1.stdout.text.trim() + \"+\" + out2.stdout.text.trim();","kind":"execute","status":"in_progress","rawInput":"const out1 = await tools.bash({ command: \"echo CODE_ONE\", description: \"First echo\" });\nconst out2 = await tools.bash({ command: \"echo CODE_TWO\", description: \"Second echo\" });\nconsole.log(\"captured output\");\nreturn out1.stdout.text.trim() + \"+\" + out2.stdout.text.trim();"}}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","status":"completed","content":[{"type":"content","content":{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}}]}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" output"}}}}
 {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" is"}}}}

+ 5 - 5
examples/tui-agent/tests/snapshots/code-mode/session.jsonl

@@ -89,7 +89,7 @@
 {"type":"assistant/chunk","seq":87,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" echo"}}}
 {"type":"assistant/chunk","seq":88,"time":1783611772655,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\\\""}}}
 {"type":"assistant/chunk","seq":89,"time":1783611772684,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" });\\n"}}}
-{"type":"assistant/chunk","seq":90,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"return"}}}
+{"type":"assistant/chunk","seq":90,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"console.log(\\\"captured output\\\");\\nreturn"}}}
 {"type":"assistant/chunk","seq":91,"time":1783611772685,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":" out"}}}
 {"type":"assistant/chunk","seq":92,"time":1783611772713,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"1"}}}
 {"type":"assistant/chunk","seq":93,"time":1783611772714,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":".stdout.text.trim"}}}
@@ -105,14 +105,14 @@
 {"type":"assistant/chunk","seq":103,"time":1783611772772,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"\""}}}
 {"type":"assistant/chunk","seq":104,"time":1783611772773,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","argumentsDelta":"}"}}}
 {"type":"assistant/chunk","seq":105,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."}}}}
-{"type":"assistant/chunk","seq":106,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}}}
+{"type":"assistant/chunk","seq":106,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nconsole.log(\\\"captured output\\\");\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}}}
 {"type":"assistant/chunk","seq":107,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}}}}
 {"type":"assistant/chunk","seq":108,"time":1783611772836,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":109,"time":1783611772840,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."},{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108],"surfaceOp":"append"}
-{"type":"tool/call","seq":110,"time":1783611772840,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}
+{"type":"assistant/message","seq":109,"time":1783611772840,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants a single run_code program that calls bash twice, then returns the two outputs joined with a plus sign. Let me write this."},{"type":"tool-call","id":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nconsole.log(\\\"captured output\\\");\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3009,"outputTokens":133,"cacheReadTokens":0,"reasoningTokens":29}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108],"surfaceOp":"append"}
+{"type":"tool/call","seq":110,"time":1783611772840,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","name":"run_code","arguments":"{\"code\": \"const out1 = await tools.bash({ command: \\\"echo CODE_ONE\\\", description: \\\"First echo\\\" });\\nconst out2 = await tools.bash({ command: \\\"echo CODE_TWO\\\", description: \\\"Second echo\\\" });\\nconsole.log(\\\"captured output\\\");\\nreturn out1.stdout.text.trim() + \\\"+\\\" + out2.stdout.text.trim();\"}"}}
 {"type":"tool/code-dispatch","seq":111,"time":1783611772933,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"First echo"},"isError":false,"resultSummary":"CODE_ONE\n"}}
 {"type":"tool/code-dispatch","seq":112,"time":1783611772936,"data":{"parentCallId":"call_00_DRxnM6R1TThfDwcudW0f2050","subCallId":"call_00_DRxnM6R1TThfDwcudW0f2050:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Second echo"},"isError":false,"resultSummary":"CODE_TWO\n"}}
-{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"CODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[110],"surfaceOp":"append"}
+{"type":"tool/result","seq":113,"time":1783611772937,"data":{"turn":1,"step":1,"callId":"call_00_DRxnM6R1TThfDwcudW0f2050","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false,"meta":{"logs":["captured output"]}},"sourceEventSeqs":[110],"surfaceOp":"append"}
 {"type":"step/end","seq":114,"time":1783611772938,"data":{"turn":1,"step":1}}
 {"type":"step/start","seq":115,"time":1783611772938,"data":{"turn":1,"step":2}}
 {"type":"assistant/chunk","seq":116,"time":1783611773376,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}

+ 16 - 14
examples/tui-agent/tests/snapshots/code-mode/terminal.expected.txt

@@ -1,7 +1,7 @@
 terminal 100x36 buffer=normal length=36 base=0 viewport=0
 lifecycle started=1 stopped=0 progress=inactive
 title "DSH TUI snapshot"
-cursor hidden column=1 viewportRow=29 bufferRow=29
+cursor hidden column=1 viewportRow=30 bufferRow=30
 buffer
 0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
   style 0-99 fg=bright-blue
@@ -54,26 +54,28 @@ buffer
 19| "▌ const o                                                                                           "
   style 0-0 fg=green
   style 2-8 bold
-20| "▌ CODE_ONE+CODE_TWO                                                                                 "
+20| "▌ captured output                                                                                   "
   style 0-0 fg=green
-21| "▌ "
+21| "▌ CODE_ONE+CODE_TWO                                                                                 "
   style 0-0 fg=green
-22| <blank>
-23| " Reasoning                                                                                          "
+22| "▌ "
+  style 0-0 fg=green
+23| <blank>
+24| " Reasoning                                                                                          "
   style 1-9 fg=bright-black italic
-24| " The output is exactly what the user asked for: CODE_ONE+CODE_TWO                                   "
+25| " The output is exactly what the user asked for: CODE_ONE+CODE_TWO                                   "
   style 1-64 fg=bright-black italic
-25| <blank>
-26| " Assistant                                                                                          "
+26| <blank>
+27| " Assistant                                                                                          "
   style 1-9 fg=bright-magenta bold
-27| " CODE_ONE+CODE_TWO                                                                                  "
-28| "────────────────────────────────────────────────────────────────────────────────────────────────────"
+28| " CODE_ONE+CODE_TWO                                                                                  "
+29| "────────────────────────────────────────────────────────────────────────────────────────────────────"
   style 0-99 dim
-29| "                                                                                                    "
+30| "                                                                                                    "
   style 1-1 inverse
-30| "────────────────────────────────────────────────────────────────────────────────────────────────────"
+31| "────────────────────────────────────────────────────────────────────────────────────────────────────"
   style 0-99 dim
-31| "/workspace/project  ↑3.1k ↓158                 idle  reasoning:on  tools:compact"
+32| "/workspace/project  ↑3.1k ↓158                 idle  reasoning:on  tools:compact"
   style 0-49 dim
   style 67-99 dim
-32-35| <blank>
+33-35| <blank>

+ 3 - 23
packages/core/tools/src/code-mode.ts

@@ -107,22 +107,9 @@ function renderValue(value: JsonValue): string {
   return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
 }
 
-/** The run_code result's `meta` payload (JSON-serializable; `presentResult` narrows it back). */
-interface RunCodeMeta {
-  logs: CodeRunResult['logs']
-}
-
 /** Canonical value returned by the outer Code Mode transport. */
 type RunCodeOutput = { logs: string[]; result?: JsonValue }
 
-/** Soft-narrow a result `meta` back to {@link RunCodeMeta} (replay may carry older shapes; presentation must not throw). */
-function asRunCodeMeta(meta: unknown): RunCodeMeta | undefined {
-  if (typeof meta !== 'object' || meta === null) return undefined
-  const m = meta as Record<string, unknown>
-  if (!Array.isArray(m.logs) || !m.logs.every(log => typeof log === 'string')) return undefined
-  return m as unknown as RunCodeMeta
-}
-
 /**
  * Build the `run_code` {@link ToolDefinition}: one required `code` parameter,
  * executed through the dispatch bridge described above. The
@@ -293,15 +280,8 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
     }),
     // Title omitted on the result: an update replaces only the fields it
     // carries, so the pending card's program title persists through
-    // completion; the captured output rides as body content.
-    presentResult: (_args, result) => {
-      const meta = asRunCodeMeta(result.meta)
-      if (!meta) return undefined
-      const output = meta.logs.join('\n')
-      return {
-        card: 'generic',
-        ...output.length > 0 ? { content: [{ type: 'text' as const, text: output }] } : {},
-      }
-    },
+    // completion. The durable final content already includes logs plus the
+    // return value, failure, or post-policy spill preview.
+    presentResult: (_args, result) => ({ card: 'generic', content: result.content }),
   })
 }

+ 21 - 17
packages/core/tools/tests/code-mode.spec.ts

@@ -663,7 +663,7 @@ describe('the run_code dispatch bridge', () => {
     expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
   })
 
-  it('presents the PROGRAM as the execute-card title on both call and result (the one slot execute cards always show)', async () => {
+  it('presents the program as the execute-card title', async () => {
     const { ctx } = await setup({ mode: 'code' })
     const tool = ctx.tools.get(RUN_CODE_NAME)!
     // The program IS the title, mirroring how command tools title their cards
@@ -676,24 +676,28 @@ describe('the run_code dispatch bridge', () => {
       kind: 'execute',
       rawInput: 'return 1',
     })
-    const view = tool.presentResult?.({ code: 'return 1' }, {
-      content: [{ type: 'text', text: 'model-facing' }],
-      isError: false,
-      meta: { logs: ['printed'] },
-    })
+  })
+
+  it.each([
+    ['logs only', 'printed', false],
+    ['result only', 'returned', false],
+    ['logs plus result', 'printed\nreturned', false],
+    ['no output', '(run_code completed with no output)', false],
+    ['failure', 'Error: code run failed (output-limit): outer output exceeded 8 bytes', true],
+    ['spilled result', 'HEAD\n\n(Omitted 100 bytes. Full formatted result stored at: /tmp/run-code.txt.)\n\nTAIL', false],
+  ] as const)('presents %s from the final post-policy content', async (_name, text, isError) => {
+    const { ctx } = await setup({ mode: 'code' })
+    const tool = ctx.tools.get(RUN_CODE_NAME)!
     // The result omits the title — an update replaces only provided fields,
     // so the pending card's program title persists through completion.
-    expect(view).toEqual({
-      card: 'generic',
-      content: [{ type: 'text', text: 'printed' }],
-    })
-    // No captured output → no content either; everything pending persists.
-    expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [] } }))
-      .toEqual({ card: 'generic' })
-    // Replay with an unrecognizable meta falls back to the generic rendering.
-    expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [{ text: 'legacy' }], dispatches: 1 } })).toBeUndefined()
-    expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { other: true } })).toBeUndefined()
-    expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false })).toBeUndefined()
+    const content = [{ type: 'text' as const, text }]
+    expect(tool.presentResult?.({ code: 'return 1' }, {
+      content,
+      isError,
+      // Stale or unrelated metadata must not replace the authoritative
+      // post-policy content used by the card.
+      meta: { logs: ['stale logs-only projection'] },
+    })).toEqual({ card: 'generic', content })
   })
 
   it('renders non-text sub-result blocks as placeholders and truncates long event summaries', async () => {