Tool-produced images reach the model but are never visible to the user #4706
Replies: 5 comments
|
你的诊断我可以给一份完全独立的、来自另一条代码路径的印证——我们撞上的是同一堵墙,而且我们的"解法"恰好证明了你要的那个声明机制是真缺的。 我们也做了这件事,代价是一张硬编码的逐包白名单我维护 pi2dsh(把 Pi 生态插件翻译成 DSH 原生插件的兼容层)。里面有一个能力是"插件生成图片 → 存成 DSH 原生 attachment → 在 Web 里内联渲染出来",端到端跑通、有 example。 但它是怎么"通"的——我们源码里就这一段: const KNOWN_IMAGE_TOOLS_BY_PACKAGE: ReadonlyMap<string, ReadonlySet<string>> = new Map([
['@crazygit/pi-codex-image-gen', new Set(['codex_generate_image'])],
])
function isKnownImageTool(packageName: string, toolName: string): boolean {
return KNOWN_IMAGE_TOOLS_BY_PACKAGE.get(packageName)?.has(toolName) === true
}一个包名、一个工具名,写死。 而且我们在自己的项目守则里把它标成了唯一一条成文的破例,原话大意是:
也就是说:我们不是解决了这个问题,我们是给一个具体的包开了后门,并且把这件事记为技术债。 这已经是同一个缺失声明的第三处特例了把你找到的和我们的放一起:
三处特例,零个声明。 我觉得这是你这份提案最有力的论据,比"我写了个插件但图片看不见"强得多——它说明:
建议把这三行放进原帖——"内部特判 + 外部白名单 + 标准路径无支持"这个组合,比任何单方证据都更能说明该补的是声明而不是补丁。 顺带说一个我们的观察,可能对设计有用我们那张白名单的判据是"逐包核证过输出契约"——也就是我们人工验证过那个工具确实返回符合预期形状的 image block,才敢让它的输出走图片卡。 这背后有个真实顾虑:如果任何工具都能声明"我输出图片",那渲染层就要处理任意插件返回的任意形状(坏的 attachment ref、指向不存在对象的引用、超大图、非图片却声称是图片)。所以如果上游要加这个声明,它大概率需要配一个"宿主侧验证"环节——声明只是意图,实际渲染前仍应校验 attachment 确实存在且是合法图片。 这一点值得你在提案里主动写上("声明 + 宿主校验"而不是"声明即渲染")——评审对"让插件决定 UI 渲染什么"的第一反应通常是警惕,你先把这层答了,阻力会小很多。 关于你的两个期望,我的看法
前者更值得推。 后者(让 (另外你提到 边界与利益相关我们不修 DSH 自家组件—— 利益相关:我维护 pi2dsh,它就是那张白名单的所有者,所以我是这个声明机制的直接受益者——上游一旦有了声明,我们那张白名单就能删掉。这一点先说在前面。这条不推销:你要的是 DSH 开一个契约,装我们的东西对此毫无帮助(我们和你卡在同一堵墙前,只是我们选择了给一个包开后门)。 |
|
Correction to my own report — the capability already exists, and I missed it. I should have searched before posting. #2995 covers adjacent ground, and @weijiafu14's answer there already points at the mechanism I claimed was missing: a tool registers its Web result card at mount time, and the completed tool row renders the pixels inline through the session-authorized attachment endpoint. Working reference: https://github.com/weijiafu14/pi2dsh/tree/main/examples/codex-image-gen So the "root cause" section above is wrong. I returned What I think still stands, as a much smaller docs point:
One line in the tool-authoring cookbook pointing from image content blocks to the result-card path would have saved this entire thread. Happy for this to be closed as covered by #2995. |
|
The correction identifies the key boundary: output.render is model-facing content, while Web dispatches the durable tool-call node through the keyed tool.call.toolview slot. A result-card integration therefore needs two deliberate paths: save and reference the attachment for the model, then register a client row under the exact controlled wire tool name and retrieve bytes through the current Session-authorized attachment path. Context injection is not a display API and changes transcript semantics. Two safety details are worth keeping in the example: derive replay UI from the frozen completed result rather than provider state, and abort/revoke attachment loads on Session change or component disposal. Missing or unauthorized images should retain readable fallback text. We turned the corrected route, package split, lifecycle, failure matrix, and fourteen acceptance gates into a source-pinned guide: https://sandbaseai.github.io/deepseek-harness-handbook/tool-image-result-card.html Disclosure: I contribute to this independent community handbook. |
|
Retracting my previous comment — I over-corrected. The original report stands. I read "already working" in #2995 and assumed it meant a stock capability I had missed. I should have checked what it actually required before retracting. Having now traced it properly:
The decisive evidence: So a tool that returns What The ask, restated precisely: either let tool result cards render Apologies for the noise of correcting my own correction. |
|
Reproduced and fixed locally. Your trace is right up to the last step, where the behaviour is slightly different from "shows nothing" — and the difference is what makes the fix small. The image is not dropped, it is rendered as JSON
That reframing matters because it means nothing is being filtered out on the way in. Only the presentation is wrong. The renderer is already in the props
/** Render a historical image group through the attachment slot. */
renderMessageImages: RenderMessageImagesSo the node view already receives the attachment-slot renderer and simply never used it. Threading it down to the body is the entire fix; no slot contract changes, no new plumbing. Two details worth keeping when it lands:
This also fixes code-mode's own imagesWorth noting because you found that special case and it looks like precedent for a workaround. TestAsserting what you measured rather than an internal: expand the disclosure, then two |
Uh oh!
There was an error while loading. Please reload this page.
A tool can return an
imagecontent block and the model receives it — but the human never sees it. The image is delivered, described, and stored, yet the Chat view shows nothing.What I did
I wrote a Cordis plugin registering a
generate_imagetool, loaded from a user preset under$DSH_HOME/.agent-presets/. It calls an image model, stores the result throughattachments.saveImages(), and returnsfrom
output.render— the same shapepackages/fs/tool-fs/src/read-image.tsuses.What happens
The image is generated, normalized, and stored under
$DSH_HOME/attachments/v1/objects/. A vision-capable conversation model receives it and describes its contents accurately (details that were never in the prompt), so the block reaches the model correctly.But in the Chat view,
document.querySelectorAll('img').lengthstays0.Where it seems to break
MessageItem.tsxrenders images only for user message nodes —contentParts()collectstype: 'image'blocks there. Content a plugin injects is projected as acontextnode (ContextMessageNodeView→ContextInjectionRow) because of itssource: { kind: 'plugin' }, and collapses into a one-line row that cannot be expanded. Bothexec.deferContext()andagent.inject()land on that path.Notably,
packages/core/tools/src/code-mode.ts:564already special-cases exactly this: when a tool result contains an image block, it re-injects the content as a user message. So the capability is half-present — Code Mode handles it, the Standard-mode path has no equivalent, and even an injected message renders collapsed.What I would hope for
A supported way for a tool-produced image to be visible to the user, not only to the model — either by rendering image blocks inside tool result cards, or by letting plugin-injected content with images render expanded rather than collapsed.
Happy to test any change against the plugin above.
Environment
dsh 0.1.1-rc.2 · Windows 11 · Node 24.13.1 · web profile
All reactions