瀏覽代碼

fix(tool-fs): accept extension-less attachment paths in read_image

creatixchu 2 周之前
父節點
當前提交
7222e17dc0
共有 62 個文件被更改,包括 564 次插入69 次删除
  1. 6 0
      .agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.i18n.yaml
  2. 27 0
      .agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.md
  3. 27 0
      .agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.zh.md
  4. 2 2
      docs/subsystems/attachment.i18n.yaml
  5. 1 1
      docs/subsystems/attachment.md
  6. 1 1
      docs/subsystems/attachment.zh.md
  7. 2 2
      docs/tool-catalog.i18n.yaml
  8. 1 1
      docs/tool-catalog.md
  9. 1 1
      docs/tool-catalog.zh.md
  10. 22 0
      packages/attachment/attachment-local/tests/store.spec.ts
  11. 2 2
      packages/attachment/attachment/README.i18n.yaml
  12. 2 1
      packages/attachment/attachment/README.md
  13. 2 1
      packages/attachment/attachment/README.zh.md
  14. 1 0
      packages/attachment/attachment/src/index.ts
  15. 39 0
      packages/attachment/attachment/src/sniff.ts
  16. 二進制
      packages/attachment/attachment/tests/sniff.spec.ts
  17. 2 2
      packages/fs/tool-fs/README.i18n.yaml
  18. 1 2
      packages/fs/tool-fs/README.md
  19. 1 2
      packages/fs/tool-fs/README.zh.md
  20. 40 12
      packages/fs/tool-fs/src/read-image.ts
  21. 186 4
      packages/fs/tool-fs/tests/read-image.spec.ts
  22. 1 1
      snapshots/sdk/bash-tool/tool-schemas.expected.json
  23. 1 1
      snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json
  24. 1 1
      snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json
  25. 1 1
      snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json
  26. 1 1
      snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json
  27. 1 1
      snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json
  28. 1 1
      snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json
  29. 1 1
      snapshots/sdk/subagent-report/tool-schemas.1.expected.json
  30. 1 1
      snapshots/sdk/text-turn/tool-schemas.expected.json
  31. 2 2
      snapshots/session/agent-instructions/tool-schemas.expected.json
  32. 1 1
      snapshots/session/both-mode-turn/system-prompt.expected.md
  33. 1 1
      snapshots/session/both-mode-turn/tool-schemas.expected.json
  34. 2 2
      snapshots/session/compaction-recovery/tool-schemas.expected.json
  35. 1 1
      snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md
  36. 1 1
      snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json
  37. 1 1
      snapshots/session/fs-glob-sampling/tool-schemas.expected.json
  38. 1 1
      snapshots/session/lsp-definition/tool-schemas.expected.json
  39. 1 1
      snapshots/session/product-subagent-both/tool-schemas.expected.json
  40. 1 1
      snapshots/session/product-subagent-codex/tool-schemas.expected.json
  41. 1 1
      snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json
  42. 29 0
      snapshots/session/ptc-read-image-attachment-path/replay.override.json
  43. 48 0
      snapshots/session/ptc-read-image-attachment-path/session.jsonl
  44. 10 0
      snapshots/session/ptc-read-image-attachment-path/snapshot.yml
  45. 二進制
      snapshots/session/ptc-read-image-attachment-path/workspace/red.png
  46. 1 1
      snapshots/session/ptc-read-image/system-prompt.expected.md
  47. 1 1
      snapshots/session/ptc-turn/system-prompt.expected.md
  48. 1 1
      snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json
  49. 1 1
      snapshots/session/ralph-loop/tool-schemas.1.expected.json
  50. 1 1
      snapshots/session/ralph-loop/tool-schemas.2.expected.json
  51. 29 0
      snapshots/session/read-image-attachment-path/replay.override.json
  52. 38 0
      snapshots/session/read-image-attachment-path/session.jsonl
  53. 9 0
      snapshots/session/read-image-attachment-path/snapshot.yml
  54. 二進制
      snapshots/session/read-image-attachment-path/workspace/red.png
  55. 1 1
      snapshots/session/session-query-spill/tool-schemas.expected.json
  56. 1 1
      snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json
  57. 1 1
      snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json
  58. 1 1
      snapshots/session/text-turn/tool-schemas.expected.json
  59. 1 1
      snapshots/session/web-fetch/tool-schemas.expected.json
  60. 1 1
      snapshots/web/cordis-tool-round/tool-schemas.expected.json
  61. 1 1
      snapshots/web/fresh-round-trip/tool-schemas.expected.json
  62. 1 1
      snapshots/web/ptc-round/system-prompt.expected.md

+ 6 - 0
.agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.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 .agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.md
+2026-08-28-read-image-extensionless-attachment-paths.md: be6acb2ca336a976bcbf0c50efda0b933fb06df6
+2026-08-28-read-image-extensionless-attachment-paths.zh.md: a4e1e677a46dd72661944e09101994358c829877

+ 27 - 0
.agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.md

@@ -0,0 +1,27 @@
+# Agent Note: read_image accepts extension-less attachment object paths
+
+Status: implemented
+
+English | [中文](2026-08-28-read-image-extensionless-attachment-paths.zh.md)
+
+## Problem
+
+Model-visible image descriptors name the normalized attachment's local read-only path, and normalized objects are content-addressed files without an image extension. `read_image` mapped `file_path` to a media type by extension alone and refused everything else, so passing the descriptor's own path back produced `read_image only accepts PNG/JPEG/WebP/GIF paths` for an image the store had already validated and persisted. The model's only workarounds were copying the object to a renamed file or re-uploading it.
+
+## Decision
+
+The extension stays the declared media type when it names one of the four supported formats, and every existing gate and diagnostic on that branch is unchanged. A path without an extension is no longer refused up front: the tool reads the bytes under the existing byte cap and identifies the container with `sniffImageMediaType`, a pure file-signature helper exported by the attachment Service Definition package. The sniffed type then passes through the same deployment media-type policy and `saveImage` admission, so the store's full decode remains the authority; a path whose extension names a non-image format is still refused before any I/O.
+
+Reading an object path re-saves its bytes rather than short-circuiting through a reverse path lookup. Normalization passes an already-normalized image through byte-identically, so the re-save deduplicates to the same content-addressed reference; a store test pins that idempotency for a re-encoded object. No reference proof is required at the tool: object paths are unguessable content digests, the published objects are already readable through the mounted filesystem (Bash included), and the session-reference gate continues to protect the remote client RPC, which is the boundary where an attachment id alone grants bytes.
+
+Two diagnostics are sharpened alongside: `INVALID_IMAGE` admission failures now name the offending path instead of surfacing the store's bare message, and an extension-less admission mismatch blames the file signature rather than a nonexistent extension.
+
+## Consequences
+
+The model reads a descriptor's normalized attachment path directly, in native and PTC modes, without copying, renaming, or re-uploading; recorded scenarios `read-image-attachment-path` and `ptc-read-image-attachment-path` pin both flows end to end, including deduplication to the stored reference. Ordinary extension-less image files become readable through the same content identification, while a wrong-extension path keeps its fast pre-I/O refusal and repair message. Any future consumer accepting extension-less image paths can reuse `sniffImageMediaType` instead of a second signature table.
+
+## Alternatives considered
+
+**Name objects with an extension on disk.** This changes the storage layout, dedup commit path, corrupt checks, export archive, and every committed object-path expectation, only to satisfy the tool's extension heuristic, and the heuristic itself — extension as media-type authority — stays wrong. The store already treats decoded content as the authority.
+
+**Instruct the model to copy the object before reading.** Prompt guidance does not stop direct calls, so the misleading refusal survives; the copy adds a filesystem write and a turn for a pure read; and the re-read commits the identical bytes to the same reference anyway, so the copy changes nothing but cost.

+ 27 - 0
.agents/notes/implemented/feature/2026-08-28-read-image-extensionless-attachment-paths.zh.md

@@ -0,0 +1,27 @@
+# Agent Note: read_image 接受无扩展名的附件对象路径
+
+Status: implemented
+
+[English](2026-08-28-read-image-extensionless-attachment-paths.md) | 中文
+
+## 问题
+
+模型可见的图片描述会给出规范化附件的本地只读路径,而规范化对象是不带图片扩展名的内容寻址文件。`read_image` 仅凭扩展名把 `file_path` 映射到媒体类型并拒绝其余一切,于是把描述里的路径原样传回会得到 `read_image only accepts PNG/JPEG/WebP/GIF paths`,尽管该图片已经通过存储校验并持久保存。模型只能把对象复制成改名文件或要求重新上传。
+
+## 决定
+
+扩展名命名四种受支持格式之一时仍作为声明的媒体类型,该分支的所有既有检查和诊断保持不变。无扩展名的路径不再被提前拒绝:工具在既有字节上限内读取字节,用 `sniffImageMediaType` 识别容器格式,这是附件 Service Definition 包导出的纯文件签名辅助函数。识别出的类型随后经过同一套部署媒体类型政策和 `saveImage` 准入,存储实现的完整解码仍是权威;扩展名声明了非图片格式的路径依旧在任何 I/O 之前被拒绝。
+
+读取对象路径会重新保存其字节,而不是通过反向路径查找走捷径。归一化让已归一化的图片字节原样通过,因此重新保存会去重到同一个内容寻址引用;一个存储测试固定了重编码对象的这一幂等性。工具处不需要会话引用证明:对象路径是不可猜测的内容摘要,已发布对象本来就能通过挂载的文件系统(包括 Bash)读到,而会话引用检查继续保护远程客户端 RPC,那才是仅凭附件 id 就能获得字节的边界。
+
+顺带收紧了两处诊断:`INVALID_IMAGE` 准入失败现在会指出出错的路径而不是透传存储的裸消息,无扩展名路径的准入不匹配归因于文件签名而不是不存在的扩展名。
+
+## 影响
+
+模型在 native 和 PTC 两种模式下都可以直接读取描述中的规范化附件路径,无需复制、改名或重新上传;录制场景 `read-image-attachment-path` 和 `ptc-read-image-attachment-path` 端到端固定了这两条流程,包括去重到已存储引用。普通的无扩展名图片文件也通过同一套内容识别变得可读,而扩展名错误的路径保留 I/O 之前的快速拒绝和修复消息。将来任何接受无扩展名图片路径的消费方都可以复用 `sniffImageMediaType`,不必再维护一张签名表。
+
+## 考虑过的替代方案
+
+**在磁盘上给对象命名加扩展名。** 这会改动存储布局、去重提交路径、损坏检查、导出归档以及每一处已提交的对象路径期望,只为迁就工具的扩展名启发式,而启发式本身(以扩展名为媒体类型权威)依然是错的。存储实现一直以解码内容为权威。
+
+**指示模型先复制对象再读取。** 提示词挡不住直接调用,误导性的拒绝依然存在;复制为一次纯读取平添一次文件系统写入和一轮往返;重新读取本来就会把相同字节提交到同一引用,复制除了成本什么也不改变。

+ 2 - 2
docs/subsystems/attachment.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/subsystems/attachment.md
-attachment.md: 15daa2b8d541ba847d48f5c43a06c1c537df6d11
-attachment.zh.md: c74a18f6bec63e117afcdb141512be04f8d75f9a
+attachment.md: e93e6782eae97811be2060807a867d3036931a8c
+attachment.zh.md: a0167cec6d1ddd8a06bdd7501edcb3d1c6ababe5

+ 1 - 1
docs/subsystems/attachment.md

@@ -10,7 +10,7 @@ Source: [`packages/attachment/attachment/src/types.ts`](../../packages/attachmen
 
 ## Identity and verified metadata
 
-`AttachmentId` is a branded opaque string. The local backend currently emits `sha256:<digest>`, but consumers must neither parse that representation nor derive a filesystem path from it. A consumer may ask the attachment provider for its object location through `imageHostPath()`, then must use the current execution filesystem to decide whether model tools can read that host path.
+`AttachmentId` is a branded opaque string. The local backend currently emits `sha256:<digest>`, but consumers must neither parse that representation nor derive a filesystem path from it. A consumer may ask the attachment provider for its object location through `imageHostPath()`, then must use the current execution filesystem to decide whether model tools can read that host path. The model may pass such a path back to `read_image` directly: extension-less files are identified from their file signature, and re-saving a normalized object deduplicates to the same content-addressed reference.
 
 ```ts type-equiv
 /** Raster image formats accepted by the version-one attachment path. */

+ 1 - 1
docs/subsystems/attachment.zh.md

@@ -10,7 +10,7 @@
 
 ## 标识与经过校验的元数据
 
-`AttachmentId` 是带类型标记的不透明字符串。本地后端目前生成 `sha256:<digest>`,但消费方既不能解析这种表示,也不能据此派生文件系统路径。消费方可以通过 `imageHostPath()` 询问附件提供方所持对象的位置,然后必须由当前执行文件系统判断模型工具能否读取该宿主路径。
+`AttachmentId` 是带类型标记的不透明字符串。本地后端目前生成 `sha256:<digest>`,但消费方既不能解析这种表示,也不能据此派生文件系统路径。消费方可以通过 `imageHostPath()` 询问附件提供方所持对象的位置,然后必须由当前执行文件系统判断模型工具能否读取该宿主路径。模型可以把这样的路径直接传回 `read_image`:无扩展名文件按文件签名识别格式,重新保存规范化对象会去重到同一个内容寻址引用。
 
 ```ts type-equiv
 /** Raster image formats accepted by the version-one attachment path. */

+ 2 - 2
docs/tool-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/tool-catalog.md
-tool-catalog.md: 91ff093e79cc05a2c08b5aa1130378440cd963f3
-tool-catalog.zh.md: 35daf5a7c6b1c27975721c0b9d8ddfe71eaeb803
+tool-catalog.md: f69f6e3650dccc06a5cbe243c26f0411befdc9e2
+tool-catalog.zh.md: 5ac0948c3beb50b9c361ca575029a9a2763716fa

+ 1 - 1
docs/tool-catalog.md

@@ -731,7 +731,7 @@ Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts
 
 ### `read_image`
 
-Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.
+Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.
 
 ```json
 {

+ 1 - 1
docs/tool-catalog.zh.md

@@ -737,7 +737,7 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
 
 ### `read_image`
 
-读取 PNG/JPEG/WebP/GIF 文件并返回图像本身。Harness 会在下一次模型请求前校验并缩小受支持的大图,因此仅为查看图片时应直接使用此工具,无需安装图片库或创建缩略图。可以用小批次并发读取彼此独立的文件。要求当前模型接受图像输入。
+读取 PNG/JPEG/WebP/GIF 文件并返回图像本身。无扩展名的路径同样被接受;格式按文件内容检测,因此规范化附件路径可以直接传入,无需复制或重命名。Harness 会在下一次模型请求前校验并缩小受支持的大图,因此仅为查看图片时应直接使用此工具,无需安装图片库或创建缩略图。可以用小批次并发读取彼此独立的文件。要求当前模型接受图像输入。
 
 ```json
 {

+ 22 - 0
packages/attachment/attachment-local/tests/store.spec.ts

@@ -172,6 +172,28 @@ describe('local attachment store', () => {
     expect(String(saved.attachmentId)).toBe(`sha256:${createHash('sha256').update(read.data).digest('hex')}`)
   })
 
+  it('re-saves a re-encoded normalized object byte-identically to the same reference', async () => {
+    const storageRoot = await root()
+    const oversized = new Uint8Array(await sharp({
+      create: { width: 4, height: 4, channels: 3, background: { r: 9, g: 9, b: 9 } },
+    }).png().toBuffer())
+    const policy = { maxPixels: POLICY.maxPixels, maxDimension: 2, maxBytes: 1024 * 1024 }
+    const saved = await saveImageFile(storageRoot, {
+      data: oversized, mediaType: 'image/png', name: 'big.png',
+    }, { ...LIMITS, maxImagePixels: 64 }, policy)
+
+    // Normalization passes an already-normalized object through unchanged, so
+    // reading the object file and saving those bytes again deduplicates: the
+    // read_image tool can accept object paths without minting new attachments.
+    const objectBytes = (await readImageFile(storageRoot, saved)).data
+    const resaved = await saveImageFile(storageRoot, {
+      data: objectBytes, mediaType: saved.mediaType, name: 'big',
+    }, { ...LIMITS, maxImagePixels: 64 }, policy)
+    expect(resaved.attachmentId).toBe(saved.attachmentId)
+    expect(resaved.bytes).toBe(saved.bytes)
+    expect(resaved.originalDimensions).toBeUndefined()
+  })
+
   it('keeps admitted history readable after deployment limits become stricter', async () => {
     const storageRoot = await root()
     const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS, POLICY)

+ 2 - 2
packages/attachment/attachment/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/attachment/attachment/README.md
-README.md: 01a5d6ee143ff53175dd7327cd6d419af61938a3
-README.zh.md: 1a1d297297a6815ce5338391af0182e471f99000
+README.md: feb8b14a2a46543169820e444994ef7cb1514210
+README.zh.md: 0f088e170cde9fb27032351dc1f9861c9111b6fd

+ 2 - 1
packages/attachment/attachment/README.md

@@ -67,7 +67,7 @@ This section explains the design decisions behind the seam and the service opera
 
 ### Service operations
 
-The service family runs one admission-and-storage flow: every entry point enforces source batch limits and canonical base64, prepares provider-independent normalized attachments before publishing any member, and commits them durably in input order without partial results. `readImageRequest` derives deterministic route-sized variants whose identity includes the attachment id, transform version, pixel and byte budgets, and encoder settings. The pure `requestImageDimensions` export computes each projection's aspect-preserving dimensions from a total-pixel budget, so providers and request pricing share one geometry. `imageHostPath` exposes an implementation-owned host location only to trusted same-process consumers that need execution-world mapping. Callers compose ordered batches while the implementation owns compression concurrency, caching, and singleflight. Reads and projections preserve caller cancellation. Failures carry stable machine-readable codes, and the caller-correctable admission subset is recognizable at runtime so each protocol adapter maps its own vocabulary; the exact per-operation contracts live in [`src/index.ts`](src/index.ts) and [`src/error.ts`](src/error.ts).
+The service family runs one admission-and-storage flow: every entry point enforces source batch limits and canonical base64, prepares provider-independent normalized attachments before publishing any member, and commits them durably in input order without partial results. `readImageRequest` derives deterministic route-sized variants whose identity includes the attachment id, transform version, pixel and byte budgets, and encoder settings. The pure `requestImageDimensions` export computes each projection's aspect-preserving dimensions from a total-pixel budget, so providers and request pricing share one geometry. The pure `sniffImageMediaType` export identifies a supported container from its file signature for consumers that accept extension-less image paths; persisting callers keep the store's full decode authoritative. `imageHostPath` exposes an implementation-owned host location only to trusted same-process consumers that need execution-world mapping. Callers compose ordered batches while the implementation owns compression concurrency, caching, and singleflight. Reads and projections preserve caller cancellation. Failures carry stable machine-readable codes, and the caller-correctable admission subset is recognizable at runtime so each protocol adapter maps its own vocabulary; the exact per-operation contracts live in [`src/index.ts`](src/index.ts) and [`src/error.ts`](src/error.ts).
 
 ### Source map
 
@@ -77,6 +77,7 @@ The service family runs one admission-and-storage flow: every entry point enforc
 | [`src/types.ts`](src/types.ts) | Durable vocabulary: references, limits, upload and store payloads |
 | [`src/admission.ts`](src/admission.ts) | `admitEncodedImages`: canonical-base64 enforcement, then `saveImages` delegation |
 | [`src/error.ts`](src/error.ts) | `AttachmentError` class and the `isImageAdmissionError` runtime subset |
+| [`src/sniff.ts`](src/sniff.ts) | `sniffImageMediaType`: file-signature container identification |
 | [`src/brand.ts`](src/brand.ts) | `AttachmentId` branded opaque identifier |
 | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; implementations enforce immutable-store checks) |
 

+ 2 - 1
packages/attachment/attachment/README.zh.md

@@ -67,7 +67,7 @@ kind: "package-reference"
 
 ### 服务操作
 
-服务族运行同一条准入与存储流程:每个入口都强制执行源批次限制与规范 base64,在发布任何成员前准备提供方无关的规范化附件,再按输入顺序持久提交而不产生部分结果。`readImageRequest` 派生确定性的路由尺寸变体,其身份包含附件 id、变换版本、像素与字节预算及编码参数。纯函数导出 `requestImageDimensions` 会按总像素预算计算每个投影保持宽高比的尺寸,使提供方与请求定价共享同一套几何计算。`imageHostPath` 只向需要执行世界映射的受信任同进程消费方暴露实现拥有的宿主位置。调用方组合有序批次,而实现拥有压缩并发、缓存与 singleflight。读取和投影保留调用方的取消语义。失败带有稳定且机器可读的错误码,运行时即可识别可由调用方修正的准入子集,让每个协议适配器映射自己的词汇;各操作的确切约定见 [`src/index.ts`](src/index.ts) 与 [`src/error.ts`](src/error.ts)。
+服务族运行同一条准入与存储流程:每个入口都强制执行源批次限制与规范 base64,在发布任何成员前准备提供方无关的规范化附件,再按输入顺序持久提交而不产生部分结果。`readImageRequest` 派生确定性的路由尺寸变体,其身份包含附件 id、变换版本、像素与字节预算及编码参数。纯函数导出 `requestImageDimensions` 会按总像素预算计算每个投影保持宽高比的尺寸,使提供方与请求定价共享同一套几何计算。纯函数导出 `sniffImageMediaType` 按文件签名识别受支持的图片容器,供接受无扩展名图片路径的消费方使用;持久化调用方仍以存储实现的完整解码为权威。`imageHostPath` 只向需要执行世界映射的受信任同进程消费方暴露实现拥有的宿主位置。调用方组合有序批次,而实现拥有压缩并发、缓存与 singleflight。读取和投影保留调用方的取消语义。失败带有稳定且机器可读的错误码,运行时即可识别可由调用方修正的准入子集,让每个协议适配器映射自己的词汇;各操作的确切约定见 [`src/index.ts`](src/index.ts) 与 [`src/error.ts`](src/error.ts)。
 
 ### 源码地图
 
@@ -77,6 +77,7 @@ kind: "package-reference"
 | [`src/types.ts`](src/types.ts) | 持久词汇:引用、限额、上传与存储载荷 |
 | [`src/admission.ts`](src/admission.ts) | `admitEncodedImages`:规范 base64 强制,随后委托 `saveImages` |
 | [`src/error.ts`](src/error.ts) | `AttachmentError` 类与 `isImageAdmissionError` 运行时子集 |
+| [`src/sniff.ts`](src/sniff.ts) | `sniffImageMediaType`:按文件签名识别图片容器 |
 | [`src/brand.ts`](src/brand.ts) | `AttachmentId` 带类型标记的不透明标识符 |
 | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;实现负责强制不可变存储检查) |
 

+ 1 - 0
packages/attachment/attachment/src/index.ts

@@ -16,6 +16,7 @@ export { AttachmentError, isImageAdmissionError } from './error.ts'
 export type { AttachmentErrorCode, ImageAdmissionErrorCode } from './error.ts'
 export { admitEncodedImages } from './admission.ts'
 export { requestImageDimensions } from './request-projection.ts'
+export { sniffImageMediaType } from './sniff.ts'
 export type {
   AttachmentId as AttachmentIdType,
   EncodedImageAttachment,

+ 39 - 0
packages/attachment/attachment/src/sniff.ts

@@ -0,0 +1,39 @@
+/**
+ * Leading-byte image-format identification for consumers that accept image
+ * files without a media-type-bearing file name, such as normalized attachment
+ * object paths. The result names the container the signature claims; callers
+ * that persist bytes keep the attachment service's full decode authoritative.
+ * @module @deepseek-ai/dsh-attachment/src/sniff
+ */
+
+import type { ImageMediaType } from './types.ts'
+
+const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] as const
+const JPEG_SIGNATURE = [0xff, 0xd8, 0xff] as const
+
+function matchesBytes(data: Uint8Array, offset: number, expected: readonly number[]): boolean {
+  if (data.byteLength < offset + expected.length) return false
+  return expected.every((byte, index) => data[offset + index] === byte)
+}
+
+function matchesAscii(data: Uint8Array, offset: number, text: string): boolean {
+  if (data.byteLength < offset + text.length) return false
+  for (let index = 0; index < text.length; index += 1) {
+    if (data[offset + index] !== text.charCodeAt(index)) return false
+  }
+  return true
+}
+
+/**
+ * Identify a supported image container from its file signature.
+ * @param data - the leading file bytes; passing the complete file is fine.
+ * @returns the media type the signature claims, or undefined when the bytes
+ *   carry no complete PNG/JPEG/WebP/GIF signature.
+ */
+export function sniffImageMediaType(data: Uint8Array): ImageMediaType | undefined {
+  if (matchesBytes(data, 0, PNG_SIGNATURE)) return 'image/png'
+  if (matchesBytes(data, 0, JPEG_SIGNATURE)) return 'image/jpeg'
+  if (matchesAscii(data, 0, 'GIF87a') || matchesAscii(data, 0, 'GIF89a')) return 'image/gif'
+  if (matchesAscii(data, 0, 'RIFF') && matchesAscii(data, 8, 'WEBP')) return 'image/webp'
+  return undefined
+}

二進制
packages/attachment/attachment/tests/sniff.spec.ts


+ 2 - 2
packages/fs/tool-fs/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/fs/tool-fs/README.md
-README.md: 193f842aa76b7d3f4df717d1d23960ef4d3ee329
-README.zh.md: 3cbd6bec6d9a4b200d90c15b32291cacc2e41aa8
+README.md: c4c8f9270a7b9422d52e6f89567c0d7eeab1f5c6
+README.zh.md: 8bd62f68010e6363515b39e7e71d6361f746713b

文件差異過大導致無法顯示
+ 1 - 2
packages/fs/tool-fs/README.md


文件差異過大導致無法顯示
+ 1 - 2
packages/fs/tool-fs/README.zh.md


+ 40 - 12
packages/fs/tool-fs/src/read-image.ts

@@ -1,5 +1,8 @@
 /**
- * The model-facing `read_image` tool commits a PNG/JPEG/WebP/GIF file.
+ * The model-facing `read_image` tool commits a PNG/JPEG/WebP/GIF file. A path
+ * without a file extension — normalized attachment objects are named by their
+ * content digest alone — is accepted too: its format comes from the file
+ * signature, and the attachment service's full decode stays authoritative.
  *
  * The route gate is deliberately stricter than the host upload preflight. An
  * image-reading tool is useful only when the exact calling route can inspect
@@ -10,8 +13,8 @@
 
 import { basename, extname } from 'node:path'
 import type { Context } from '@deepseek-ai/cordis'
-import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
-import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
+import { AttachmentError, AttachmentId, sniffImageMediaType } from '@deepseek-ai/dsh-attachment'
+import type { AttachmentStore, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
 import type { ContentBlock } from '@deepseek-ai/dsh-llm'
 import { defineTool } from '@deepseek-ai/dsh-tools'
 import type { GenericCallView, ToolExecution } from '@deepseek-ai/dsh-tools'
@@ -98,6 +101,13 @@ export async function assertImageCapableRoute(ctx: Context, exec: ToolExecution,
   }
 }
 
+/** Refuse a media type outside the deployment's accepted set, naming the offending path. */
+function assertDeploymentAccepts(attachments: AttachmentStore, mediaType: ImageMediaType, displayPath: string): void {
+  if (!attachments.imageLimits.mediaTypes.includes(mediaType)) {
+    throw new Error(`cannot read "${displayPath}": ${mediaType} images are not accepted by this deployment`)
+  }
+}
+
 /**
  * Re-brand a structured image outcome into the durable attachment reference an
  * `ImageBlock` carries.
@@ -170,6 +180,7 @@ export function applyReadImageTool(ctx: Context): void {
   ctx.tools.register(defineTool({
     name: 'read_image',
     description: 'Read a PNG/JPEG/WebP/GIF file and return the image itself. '
+      + 'A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. '
       + 'Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. '
       + 'Independent files may be read concurrently in small batches. Requires the current model to accept image input.',
     parameters: {
@@ -192,19 +203,20 @@ export function applyReadImageTool(ctx: Context): void {
     async execute(args, exec) {
       if (args.file_path.trim().length === 0) throw new Error('file_path must be a non-empty string')
 
-      // Every gate runs before any filesystem I/O so a refusal never leaks
-      // partial reads or attachment writes.
-      const mediaType = imageMediaTypeForPath(args.file_path)
-      if (mediaType === undefined) {
-        throw new Error(`cannot read "${args.file_path}": read_image only accepts PNG/JPEG/WebP/GIF paths`)
+      // Every pre-read gate runs before any filesystem I/O so a refusal never
+      // leaks partial reads or attachment writes. An extension-less path
+      // declares no format, so only its format and deployment media-type
+      // checks wait for the bytes.
+      const extension = extname(args.file_path).toLowerCase()
+      const declared = imageMediaTypeForPath(args.file_path)
+      if (declared === undefined && extension !== '') {
+        throw new Error(`cannot read "${args.file_path}": the ${extension} extension does not declare a supported image format; read_image accepts PNG/JPEG/WebP/GIF files, including extension-less files in those formats`)
       }
       const attachments = ctx.get('attachments')
       if (attachments === undefined) {
         throw new Error(`cannot read "${args.file_path}" as an image: no attachment service is mounted`)
       }
-      if (!attachments.imageLimits.mediaTypes.includes(mediaType)) {
-        throw new Error(`cannot read "${args.file_path}": ${mediaType} images are not accepted by this deployment`)
-      }
+      if (declared !== undefined) assertDeploymentAccepts(attachments, declared, args.file_path)
       await assertImageCapableRoute(ctx, exec, args.file_path)
 
       const { target, info } = await resolveRegularReadTarget(ctx, exec, args.file_path)
@@ -213,6 +225,11 @@ export function applyReadImageTool(ctx: Context): void {
       // aggregate bound applies beside the per-image bound.
       const byteCap = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes)
       const data = await ctx.fs.readBytes(target, exec.signal, byteCap)
+      const mediaType = declared ?? sniffImageMediaType(data)
+      if (mediaType === undefined) {
+        throw new Error(`cannot read "${target.displayPath}": the file content is not a supported image format; read_image accepts PNG/JPEG/WebP/GIF`)
+      }
+      if (declared === undefined) assertDeploymentAccepts(attachments, mediaType, target.displayPath)
       // Persist before returning: the image block must reference a durably
       // committed object by the time the tool/result event is appended.
       let ref: ImageAttachmentRef
@@ -247,8 +264,19 @@ export function applyReadImageTool(ctx: Context): void {
             { cause: error },
           )
         }
+        if (error.code === 'INVALID_IMAGE') {
+          throw new Error(
+            `cannot read "${target.displayPath}": the bytes do not decode as a supported PNG/JPEG/WebP/GIF image; the file may be truncated or corrupt`,
+            { cause: error },
+          )
+        }
         if (error.code !== 'IMAGE_TYPE_MISMATCH') throw error
-        const extension = extname(target.displayPath).toLowerCase()
+        if (declared === undefined) {
+          throw new Error(
+            `cannot read "${target.displayPath}": the file signature claims ${mediaType}, but the bytes decode as a different image format; the file may be corrupt`,
+            { cause: error },
+          )
+        }
         throw new Error(
           `cannot read "${target.displayPath}": the ${extension} extension declares ${mediaType}, but the bytes use a different image format; rename the file to match its actual format if it is PNG/JPEG/WebP/GIF, or convert it to one of those formats`,
           { cause: error },

+ 186 - 4
packages/fs/tool-fs/tests/read-image.spec.ts

@@ -1,8 +1,9 @@
 /**
  * The `read_image` tool over the REAL local filesystem and attachment store:
- * extension routing, the strict image-modality gate (every refusal arm),
- * durable commit + image-block rendering, attachment admission failures, and
- * the regression that `read` keeps its text-only contract.
+ * extension routing, extension-less content sniffing (attachment object paths
+ * included), the strict image-modality gate (every refusal arm), durable
+ * commit + image-block rendering, attachment admission failures, and the
+ * regression that `read` keeps its text-only contract.
  */
 
 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
@@ -34,6 +35,10 @@ import {
 const PNG_1X1 = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64')
 /** 3x3 red PNG used to trip a tiny configured pixel limit. */
 const PNG_3X3 = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEElEQVR4nGP4z8AAQQxYWACPjgj4kWPEuQAAAABJRU5ErkJggg==', 'base64')
+/** 1x1 red JPEG already inside every normalization limit (byte-identical passthrough). */
+const JPEG_1X1 = Buffer.from('/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z', 'base64')
+/** 1x1 red WebP already inside every normalization limit (byte-identical passthrough). */
+const WEBP_1X1 = Buffer.from('UklGRjwAAABXRUJQVlA4IDAAAADQAQCdASoBAAEAAUAmJaACdLoB+AADsAD+8ut//NgVzXPv9//S4P0uD9Lg/9KQAAA=', 'base64')
 
 const testToolSignal = new AbortController().signal
 
@@ -254,6 +259,175 @@ describe('read_image happy path', () => {
   })
 })
 
+/** The mounted attachment service, asserted present for direct store calls. */
+function mountedStore(ctx: Context): AttachmentStore {
+  const attachments = ctx.get('attachments')
+  if (attachments === undefined) throw new Error('expected the attachment service')
+  return attachments
+}
+
+/** The host object path behind a reference, asserted present for the local store. */
+function objectPathOf(attachments: AttachmentStore, ref: ImageAttachmentRef): string {
+  const hostPath = attachments.imageHostPath(ref)
+  if (hostPath === undefined) throw new Error('expected a host-file-backed store')
+  return hostPath
+}
+
+describe('extension-less paths', () => {
+  it('reads a normalized attachment object path directly and dedups to the stored reference', async () => {
+    await writeFile(join(dir, 'red.png'), PNG_1X1)
+    const ctx = await setup()
+    const first = await readImage(ctx, { file_path: 'red.png' }, agentOn('vision-model'))
+    expect(first.isError).toBe(false)
+    const ref = (first.content[1] as { attachment: ImageAttachmentRef }).attachment
+    const attachments = mountedStore(ctx)
+    const objectPath = objectPathOf(attachments, ref)
+
+    const second = await readImage(ctx, { file_path: objectPath }, agentOn('vision-model'))
+    expect(second.isError).toBe(false)
+    const reread = (second.content[1] as { attachment: ImageAttachmentRef }).attachment
+    expect(reread.attachmentId).toBe(ref.attachmentId)
+    expect(reread.mediaType).toBe('image/png')
+    expect(text(second)).toContain(`<path>${objectPath}</path>`)
+  })
+
+  it.each([
+    ['image/jpeg', JPEG_1X1],
+    ['image/webp', WEBP_1X1],
+  ] as const)('dedups a re-read %s object to its stored reference', async (mediaType, bytes) => {
+    const ctx = await setup()
+    const attachments = mountedStore(ctx)
+    const ref = await attachments.saveImage({ data: bytes, mediaType, name: 'source' })
+    const result = await readImage(ctx, { file_path: objectPathOf(attachments, ref) }, agentOn('vision-model'))
+    expect(result.isError).toBe(false)
+    const reread = (result.content[1] as { attachment: ImageAttachmentRef }).attachment
+    expect(reread.attachmentId).toBe(ref.attachmentId)
+    expect(reread.mediaType).toBe(mediaType)
+  })
+
+  it('forwards an attachment object path read through the outer run_code context', async () => {
+    const ctx = await setup({ toolMode: 'ptc' })
+    const attachments = mountedStore(ctx)
+    const ref = await attachments.saveImage({ data: PNG_1X1, mediaType: 'image/png', name: 'red.png' })
+    const objectPath = objectPathOf(attachments, ref)
+    const runtime = ctx.codeRuntime as FakeRuntime
+    runtime.behavior = async (request) => {
+      const value = await request.bindings[0]!.functions.read_image!({ file_path: objectPath })
+      return { logs: [], value }
+    }
+
+    const result = await call(ctx, RUN_CODE_NAME, {
+      code: 'return await tools.read_image({ file_path: attachmentPath })',
+      description: 'Read the attachment object through PTC mode',
+    }, agentOn('vision-model'))
+
+    expect(result.isError).toBe(false)
+    const forwarded = result.additionalContexts?.[0]?.content
+    expect(forwarded?.[1]).toMatchObject({
+      type: 'image',
+      attachment: { attachmentId: ref.attachmentId, mediaType: 'image/png' },
+    })
+  })
+
+  it('reads an ordinary extension-less image file by sniffing its content', async () => {
+    await writeFile(join(dir, 'avatar'), PNG_1X1)
+    const ctx = await setup()
+    const result = await readImage(ctx, { file_path: 'avatar' }, agentOn('vision-model'))
+    expect(result.isError).toBe(false)
+    const image = result.content[1] as { attachment: ImageAttachmentRef }
+    expect(image.attachment.mediaType).toBe('image/png')
+    expect(image.attachment.name).toBe('avatar')
+  })
+
+  it('refuses extension-less bytes that are not a supported image', async () => {
+    await writeFile(join(dir, 'notes'), 'plain text, not an image')
+    const ctx = await setup()
+    const result = await readImage(ctx, { file_path: 'notes' }, agentOn('vision-model'))
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain(`cannot read "${join(dir, 'notes')}": the file content is not a supported image format`)
+  })
+
+  it('explains extension-less bytes that sniff as an image but do not decode', async () => {
+    await writeFile(join(dir, 'broken'), PNG_1X1.subarray(0, 16))
+    const ctx = await setup()
+    const result = await readImage(ctx, { file_path: 'broken' }, agentOn('vision-model'))
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain('do not decode as a supported PNG/JPEG/WebP/GIF image')
+  })
+
+  it('reports a missing attachment object through the fs vocabulary', async () => {
+    const ctx = await setup()
+    const bogus = join(home, 'attachments', 'v1', 'objects', 'ab', 'ab'.repeat(32))
+    const result = await readImage(ctx, { file_path: bogus }, agentOn('vision-model'))
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain('not found')
+  })
+
+  it('applies the deployment media-type policy to the sniffed format', async () => {
+    /** Store whose deployment accepts JPEG only; sniffed PNG bytes must refuse before any save. */
+    class JpegOnlySniffStore extends AttachmentStore {
+      readonly imageLimits: ImageAttachmentLimits = Object.freeze({
+        maxImageBytes: 1024,
+        maxImagesPerMessage: 1,
+        maxMessageImageBytes: 1024,
+        maxImagePixels: 100,
+        maxImageDimension: 2000,
+        mediaTypes: Object.freeze(['image/jpeg'] as const),
+      })
+
+      validateImage(_input: SaveImageAttachment): Promise<void> {
+        throw new Error('unreachable: the sniffed-format policy refuses before validation')
+      }
+
+      saveImage(_input: SaveImageAttachment): Promise<ImageAttachmentRef> {
+        throw new Error('unreachable: the sniffed-format policy refuses before save')
+      }
+
+      readImage(_ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
+        throw new Error('unreachable in this test')
+      }
+    }
+    await writeFile(join(dir, 'avatar'), PNG_1X1)
+    const ctx = await setup({ attachments: false })
+    await ctx.plugin(JpegOnlySniffStore)
+    const result = await readImage(ctx, { file_path: 'avatar' }, agentOn('vision-model'))
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain('image/png images are not accepted by this deployment')
+  })
+
+  it('names a signature/decoded-format disagreement on an extension-less path', async () => {
+    /** Store whose admission reports a media-type mismatch; the tool cannot blame an extension. */
+    class MismatchStore extends AttachmentStore {
+      readonly imageLimits: ImageAttachmentLimits = Object.freeze({
+        maxImageBytes: 1024,
+        maxImagesPerMessage: 1,
+        maxMessageImageBytes: 1024,
+        maxImagePixels: 100,
+        maxImageDimension: 2000,
+        mediaTypes: Object.freeze(['image/png'] as const),
+      })
+
+      validateImage(_input: SaveImageAttachment): Promise<void> {
+        return Promise.resolve()
+      }
+
+      saveImage(_input: SaveImageAttachment): Promise<ImageAttachmentRef> {
+        throw new AttachmentError('Declared image type does not match its bytes.', 'IMAGE_TYPE_MISMATCH')
+      }
+
+      readImage(_ref: ImageAttachmentRef): Promise<StoredImageAttachment> {
+        throw new Error('unreachable in this test')
+      }
+    }
+    await writeFile(join(dir, 'sniffed'), PNG_1X1)
+    const ctx = await setup({ attachments: false })
+    await ctx.plugin(MismatchStore)
+    const result = await readImage(ctx, { file_path: 'sniffed' }, agentOn('vision-model'))
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain('the file signature claims image/png, but the bytes decode as a different image format')
+  })
+})
+
 describe('strict image-modality gate', () => {
   it('accepts an exact visual route even when the advisory model catalog omits it', async () => {
     await writeFile(join(dir, 'red.png'), PNG_1X1)
@@ -309,7 +483,7 @@ describe('argument and service preconditions', () => {
 
     const nonImage = await readImage(ctx, { file_path: 'notes.txt' }, agentOn('vision-model'))
     expect(nonImage.isError).toBe(true)
-    expect(text(nonImage)).toContain('only accepts PNG/JPEG/WebP/GIF paths')
+    expect(text(nonImage)).toContain('the .txt extension does not declare a supported image format')
   })
 
   it('refuses when no attachment service is mounted', async () => {
@@ -373,6 +547,14 @@ describe('image admission failures', () => {
     expect(text(result)).toContain('rename the file to match its actual format if it is PNG/JPEG/WebP/GIF, or convert it to one of those formats')
   })
 
+  it('explains a named image file whose bytes do not decode', async () => {
+    await writeFile(join(dir, 'broken.png'), PNG_1X1.subarray(0, 16))
+    const ctx = await setup()
+    const result = await readImage(ctx, { file_path: 'broken.png' }, agentOn('vision-model'))
+    expect(result.isError).toBe(true)
+    expect(text(result)).toContain(`cannot read "${join(dir, 'broken.png')}": the bytes do not decode as a supported PNG/JPEG/WebP/GIF image`)
+  })
+
   it('fails with FS_TOO_LARGE before reading a file past maxImageBytes', async () => {
     await writeFile(join(dir, 'red.png'), PNG_1X1)
     const ctx = await setup({ storeConfig: { maxImageBytes: PNG_1X1.length - 1 } })

+ 1 - 1
snapshots/sdk/bash-tool/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json

@@ -323,7 +323,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-report/tool-schemas.1.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/text-turn/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 2 - 2
snapshots/session/agent-instructions/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -1007,7 +1007,7 @@
       },
       {
         "name": "read_image",
-        "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+        "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
         "parameters": {
           "type": "object",
           "properties": {

+ 1 - 1
snapshots/session/both-mode-turn/system-prompt.expected.md

@@ -154,7 +154,7 @@ interface ToolArgsMap {
     /** Maximum number of lines to return. Defaults to 2000. */
     limit?: number;
   } & Record<string, JsonValue>;
-  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
+  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
   read_image: {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;

+ 1 - 1
snapshots/session/both-mode-turn/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 2 - 2
snapshots/session/compaction-recovery/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -1007,7 +1007,7 @@
       },
       {
         "name": "read_image",
-        "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+        "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
         "parameters": {
           "type": "object",
           "properties": {

+ 1 - 1
snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md

@@ -321,7 +321,7 @@ interface ToolArgsMap {
     /** Maximum number of lines to return. Defaults to 2000. */
     limit?: number;
   } & Record<string, JsonValue>;
-  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
+  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
   read_image: {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;

+ 1 - 1
snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json

@@ -503,7 +503,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/fs-glob-sampling/tool-schemas.expected.json

@@ -226,7 +226,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/lsp-definition/tool-schemas.expected.json

@@ -343,7 +343,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/product-subagent-both/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/product-subagent-codex/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 29 - 0
snapshots/session/ptc-read-image-attachment-path/replay.override.json

@@ -0,0 +1,29 @@
+[
+  {
+    "kind": "chunks",
+    "chunks": [
+      { "type": "block-start", "index": 0, "blockType": "tool-call" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "code-image-source", "name": "run_code", "arguments": "{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}" } },
+      { "type": "usage", "usage": { "inputTokens": 3, "outputTokens": 3 } },
+      { "type": "finish", "reason": { "kind": "tool-calls" } }
+    ]
+  },
+  {
+    "kind": "chunks",
+    "chunks": [
+      { "type": "block-start", "index": 0, "blockType": "tool-call" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "code-image-object", "name": "run_code", "arguments": "{\"code\":\"return await tools.read_image({ file_path: '{{fromRequest:<path>(.+)/red\\.png</path>}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}" } },
+      { "type": "usage", "usage": { "inputTokens": 3, "outputTokens": 3 } },
+      { "type": "finish", "reason": { "kind": "tool-calls" } }
+    ]
+  },
+  {
+    "kind": "chunks",
+    "chunks": [
+      { "type": "block-start", "index": 0, "blockType": "text" },
+      { "type": "block-end", "index": 0, "block": { "type": "text", "text": "DONE" } },
+      { "type": "usage", "usage": { "inputTokens": 3, "outputTokens": 3 } },
+      { "type": "finish", "reason": { "kind": "stop" } }
+    ]
+  }
+]

+ 48 - 0
snapshots/session/ptc-read-image-attachment-path/session.jsonl

@@ -0,0 +1,48 @@
+{"type":"session","version":0,"id":"{{session:1}}","createdAt":1783954000000,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"permission/preset","data":{"preset":"danger-full-access"}}
+{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
+{"type":"approval/policy","data":{"policy":"never"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Using run_code, call read_image on red.png, then call read_image again with the extension-less normalized attachment object path it produced, and reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
+{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":1}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Using run_code, call read_image on red.png, then call read_image again with the extension-less normalized attachment object path it produced, and reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
+{"type":"session/title","data":{"title":"Using run_code, call read_image on","messageSeqs":[7],"source":{"kind":"fallback"}}}
+{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"code-image-source","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"code-image-source","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[[12,15]],"surfaceOp":"append"}
+{"type":"tool/call","data":{"turn":1,"step":1,"callId":"code-image-source","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}"}}
+{"type":"tool/code-dispatch-start","data":{"rootCallId":"code-image-source","parentCallId":"code-image-source","subCallId":"code-image-source:code:1","name":"read_image","arguments":{"file_path":"red.png"}}}
+{"type":"tool/code-dispatch","data":{"rootCallId":"code-image-source","parentCallId":"code-image-source","subCallId":"code-image-source:code:1","name":"read_image","arguments":{"file_path":"red.png"},"isError":false,"content":[{"type":"text","text":"<path>{{cwd}}/red.png</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}]}}
+{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"code-image-source"},"content":[{"type":"tool-result","toolCallId":"code-image-source","content":[{"type":"text","text":"{\n  \"path\": \"{{cwd}}/red.png\",\n  \"image\": {\n    \"attachmentId\": \"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\",\n    \"mediaType\": \"image/png\",\n    \"bytes\": 69,\n    \"width\": 1,\n    \"height\": 1,\n    \"name\": \"red.png\"\n  }\n}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"<path>{{cwd}}/red.png</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:5}}"}]}}
+{"type":"step/end","data":{"turn":1,"step":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":2}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"<path>{{cwd}}/red.png</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:5}}"},"surfaceOp":"append"}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"code-image-object","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: '{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"code-image-object","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: '{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[[26,29]],"surfaceOp":"append"}
+{"type":"tool/call","data":{"turn":1,"step":2,"callId":"code-image-object","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: '{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}"}}
+{"type":"tool/code-dispatch-start","data":{"rootCallId":"code-image-object","parentCallId":"code-image-object","subCallId":"code-image-object:code:1","name":"read_image","arguments":{"file_path":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}}
+{"type":"tool/code-dispatch","data":{"rootCallId":"code-image-object","parentCallId":"code-image-object","subCallId":"code-image-object:code:1","name":"read_image","arguments":{"file_path":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"},"isError":false,"content":[{"type":"text","text":"<path>{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}]}}
+{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"code-image-object"},"content":[{"type":"tool-result","toolCallId":"code-image-object","content":[{"type":"text","text":"{\n  \"path\": \"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\",\n  \"image\": {\n    \"attachmentId\": \"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\",\n    \"mediaType\": \"image/png\",\n    \"bytes\": 69,\n    \"width\": 1,\n    \"height\": 1,\n    \"name\": \"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"\n  }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[31],"surfaceOp":"append"}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"<path>{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:8}}"}]}}
+{"type":"step/end","data":{"turn":1,"step":2}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":3}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"<path>{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[[40,43]],"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":3}}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}

+ 10 - 0
snapshots/session/ptc-read-image-attachment-path/snapshot.yml

@@ -0,0 +1,10 @@
+version: 1
+scenario: ptc-read-image-attachment-path
+profile: headless
+composition: ptc-image
+recording: authored
+header:
+  class: ptc-image
+replay:
+  override: true
+platform: posix

二進制
snapshots/session/ptc-read-image-attachment-path/workspace/red.png


+ 1 - 1
snapshots/session/ptc-read-image/system-prompt.expected.md

@@ -156,7 +156,7 @@ interface ToolArgsMap {
     /** Maximum number of lines to return. Defaults to 2000. */
     limit?: number;
   } & Record<string, JsonValue>;
-  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
+  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
   read_image: {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;

+ 1 - 1
snapshots/session/ptc-turn/system-prompt.expected.md

@@ -156,7 +156,7 @@ interface ToolArgsMap {
     /** Maximum number of lines to return. Defaults to 2000. */
     limit?: number;
   } & Record<string, JsonValue>;
-  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
+  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
   read_image: {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;

+ 1 - 1
snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/ralph-loop/tool-schemas.1.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/ralph-loop/tool-schemas.2.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 29 - 0
snapshots/session/read-image-attachment-path/replay.override.json

@@ -0,0 +1,29 @@
+[
+  {
+    "kind": "chunks",
+    "chunks": [
+      { "type": "block-start", "index": 0, "blockType": "tool-call" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "read-image-source", "name": "read_image", "arguments": "{\"file_path\":\"red.png\"}" } },
+      { "type": "usage", "usage": { "inputTokens": 3, "outputTokens": 3 } },
+      { "type": "finish", "reason": { "kind": "tool-calls" } }
+    ]
+  },
+  {
+    "kind": "chunks",
+    "chunks": [
+      { "type": "block-start", "index": 0, "blockType": "tool-call" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "read-image-object", "name": "read_image", "arguments": "{\"file_path\":\"{{fromRequest:<path>(.+)/red\\.png</path>}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"}" } },
+      { "type": "usage", "usage": { "inputTokens": 3, "outputTokens": 3 } },
+      { "type": "finish", "reason": { "kind": "tool-calls" } }
+    ]
+  },
+  {
+    "kind": "chunks",
+    "chunks": [
+      { "type": "block-start", "index": 0, "blockType": "text" },
+      { "type": "block-end", "index": 0, "block": { "type": "text", "text": "DONE" } },
+      { "type": "usage", "usage": { "inputTokens": 3, "outputTokens": 3 } },
+      { "type": "finish", "reason": { "kind": "stop" } }
+    ]
+  }
+]

+ 38 - 0
snapshots/session/read-image-attachment-path/session.jsonl

@@ -0,0 +1,38 @@
+{"type":"session","version":0,"id":"{{session:1}}","createdAt":1783953000000,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"permission/preset","data":{"preset":"danger-full-access"}}
+{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
+{"type":"approval/policy","data":{"policy":"never"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use read_image to look at red.png in the current directory, then call read_image again with the extension-less normalized attachment object path under .dsh, and reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
+{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":1}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Use read_image to look at red.png in the current directory, then call read_image again with the extension-less normalized attachment object path under .dsh, and reply with exactly the single word DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
+{"type":"session/title","data":{"title":"Use read_image to look at","messageSeqs":[7],"source":{"kind":"fallback"}}}
+{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"read-image-source","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-source","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[[12,15]],"surfaceOp":"append"}
+{"type":"tool/call","data":{"turn":1,"step":1,"callId":"read-image-source","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}
+{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"read-image-source"},"content":[{"type":"tool-result","toolCallId":"read-image-source","content":[{"type":"text","text":"<path>{{cwd}}/red.png</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":1}}
+{"type":"step/start","data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"read-image-object","name":"read_image","arguments":"{\"file_path\":\"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"}"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-object","name":"read_image","arguments":"{\"file_path\":\"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[[21,24]],"surfaceOp":"append"}
+{"type":"tool/call","data":{"turn":1,"step":2,"callId":"read-image-object","name":"read_image","arguments":"{\"file_path\":\"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"}"}}
+{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"read-image-object"},"content":[{"type":"tool-result","toolCallId":"read-image-object","content":[{"type":"text","text":"<path>{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":2}}
+{"type":"step/start","data":{"turn":1,"step":3}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[[30,33]],"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":3}}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}

+ 9 - 0
snapshots/session/read-image-attachment-path/snapshot.yml

@@ -0,0 +1,9 @@
+version: 1
+scenario: read-image-attachment-path
+profile: headless
+composition: image
+recording: authored
+header:
+  class: image
+replay:
+  override: true

二進制
snapshots/session/read-image-attachment-path/workspace/red.png


+ 1 - 1
snapshots/session/session-query-spill/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json

@@ -369,7 +369,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/text-turn/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/web-fetch/tool-schemas.expected.json

@@ -306,7 +306,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/web/cordis-tool-round/tool-schemas.expected.json

@@ -566,7 +566,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/web/fresh-round-trip/tool-schemas.expected.json

@@ -369,7 +369,7 @@
     },
     {
       "name": "read_image",
-      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
+      "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/web/ptc-round/system-prompt.expected.md

@@ -184,7 +184,7 @@ interface ToolArgsMap {
     /** Maximum number of lines to return. Defaults to 2000. */
     limit?: number;
   } & Record<string, JsonValue>;
-  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
+  /** Read a PNG/JPEG/WebP/GIF file and return the image itself. A path without a file extension is accepted; the format is detected from the file content, so normalized attachment paths can be passed directly without copying or renaming. Harness validates and downscales large supported images before the next model request, so use this tool directly instead of installing image libraries or creating thumbnails merely to inspect an image. Independent files may be read concurrently in small batches. Requires the current model to accept image input. */
   read_image: {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;

部分文件因文件數量過多而無法顯示