deepseek-harness/.agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.zh.md

3.8 KiB
Raw Blame History

Agent Note: 基于既有 seam 的最小 read_image 工具

Status: implemented

English | 中文

问题

多模态附件工作为用户上传建立了完整的持久路径,但模型无法查看磁盘图片。read 按约定拒绝二进制内容,因此被问到截图或渲染图表的 agent 要么失败,要么使用有损的变通方法。PR #598 的独立尝试把工具与循环级路由作用域、按路由控制 schema 可见性和新的会话日志概念放在一起。这些能力不是发布一条带图片且已记录的工具结果所必需的。

决定

两个图片读取操作都放在 dsh-tool-fs,通过现有扩展点发布普通的持久工具结果。

  • read_image 读取文件系统路径。 扩展名选择声明的 PNG/JPEG/WebP/GIF 媒体类型,附件存储的魔数与像素校验保持权威。字节沿 ctx.fs.stat → 有界 ctx.fs.readBytes → ctx.attachments.saveImage → fs/observed 流动。工具结果包含元数据和一个 ImageBlock。
  • FileSystem.readBytes(target, signal, maxBytes) 是新的必备提供方原语:字节上限放在 seam 上,任何后端都无法无界缓冲文件;stat 大小先短路,随后的流最多多读一个字节以防 stat 之后的增长(FS_TOO_LARGE)。
  • 注册随组合条件挂载,执行按路由门禁。 工具只在 ctx.inject(['attachments'], …) 作用域内注册。执行时在 I/O 之前通过 ctx.llm.resolveModelInfo 解析调用路由,并要求 inputModalities 包含 image;能力未知即拒绝。纯文本路由仍可使用此前的持久图片,因为共享 LLM 运行时会在请求组装时把图片投影为占位符。
  • PTC mode 以带外方式转发图像:嵌套分派返回规范值(仅限本次执行,不含图像块),并延迟提交一条携带信封和图像的 user 角色上下文消息,图片仍会到达下一次请求。
  • llm-replay 模型可以声明 inputModalities,因此 keyless ACP 快照可以覆盖支持图片的结果和纯文本拒绝。

考虑过的替代方案

  • PR #598 的路由作用域设计使用 request-ready 扩展点、按路由控制 schema 可见性、可逆投影和三个持久概念。共享 LLM 请求投影现在可以处理纯文本路由,无需把工具注册或会话格式放进 agent-loop。
  • 用 agent.inject() 代替带图像的工具结果——把图像绕过工具结果,作为单独注入的用户消息。拒绝:图像就是工具的结果;拆开只会多一条无收益的日志消息,而工具结果路径本就端到端可用。
  • 用魔数嗅探代替扩展名声明——嗅探重复了附件存储已拥有的检测(基于 sharp,权威)。扩展名只是声明;不匹配时按改名修复提示失败关闭,而不是被静默接受,这也让模型对文件名与内容的对应保持诚实。这一拒绝覆盖带扩展名的路径;无扩展名图片路径将其收窄,什么也没声明的路径按文件签名识别。
  • 无条件注册、缺存储时执行报错——拒绝;没有附件存储的部署永远无法满足该工具,其 schema 会是常态谎言。相反,路由门禁是逐调用状态,正确的位置就是执行边界。

后果

  • 工具在纯文本路由上拒绝执行,而会话历史中已经存在的图片会由请求期占位符表示。
  • 重复的图片结果会累积请求成本,直到请求投影或压缩将其移除;内容寻址只去重持久字节。
  • 工具结果卡片现在经由浏览器的 tool.call.images 槽位渲染图像本身(见 tool-card image results 笔记);未组合附件呈现插件的 UI 显示结果的信封文本。