@deepseek-ai/dsh-tool-fs 0.1.0-rc.8 → 0.1.1-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/fs/tool-fs/README.md
5
- README.md: ce7c0ea9070e30c1e6b538933ff5c4605b8d59cc
6
- README.zh.md: 28360fbe6d0ab0f1a2b5ff11e44155ad6468274c
5
+ README.md: 763cb831233da5b1f14c73e353920e9d6a87ced9
6
+ README.zh.md: aa55de59452e7779ef278748abac17837800ba02
package/README.md CHANGED
@@ -14,7 +14,7 @@ await ctx.plugin(ToolFs) // this package — re
14
14
 
15
15
  `@deepseek-ai/dsh-fs-observation-policy` is **optional**: omit it and the tools run against the bare provider (unconditional write/overwrite/edit, no observed-state). A deployment that loads these tools is expected to also load it, so the behavior is read-before-write/edit.
16
16
 
17
- `read_image` registers only while a durable `ctx.attachments` service is mounted — without one the deployment cannot commit image bytes, so the tool never appears. Execution additionally requires the exact routed model to declare `image` input (resolved through `ctx.llm.resolveModelInfo` from the session's latest request header, falling back to agent options); an unknown or text-only route gets a refusal result before any filesystem I/O, so a text route's durable history stays free of image blocks.
17
+ `read_image` registers only while a durable `ctx.attachments` service is mounted. Execution additionally requires the exact routed model to declare `image` input, resolved through `ctx.llm.resolveModelInfo` from the session's latest request header and then from agent options.
18
18
 
19
19
  ## Config
20
20
 
@@ -32,13 +32,13 @@ All keys are optional; the defaults are the shipped read caps.
32
32
  | Tool | Arguments | Behavior |
33
33
  |---|---|---|
34
34
  | `read` | `file_path`, `offset?`, `limit?` | Line-numbered UTF-8 content with a pagination footer. `offset` is 1-based; `limit` defaults to and caps at the configured `readLimit` (2000). |
35
- | `read_image` | `file_path` | Reads a PNG/JPEG/WebP/GIF file through the bounded byte seam, persists it through `ctx.attachments.saveImage`, and returns an image block beside a small metadata envelope. It succeeds only when the exact routed model declares image input. |
35
+ | `read_image` | `file_path` | Reads a PNG/JPEG/WebP/GIF file through the bounded byte seam, persists it through `ctx.attachments.saveImage`, and returns an image block beside a small metadata envelope. Harness validates and downscales large supported images before the next model request, so the model can read the source directly without first creating a thumbnail. It succeeds only when the exact routed model declares image input. |
36
36
  | `write` | `file_path`, `content` | Create or fully replace a file. With the policy plugin: overwriting an existing file requires a prior `read` at the unchanged version; creating a new file does not. Without it: unconditional. |
37
37
  | `edit` | `file_path`, non-empty `old_string`, `new_string`, `replace_all?` | Literal replacement; unique match required unless `replace_all` is true. With the policy plugin: requires a prior `read` (any window) and the file unchanged since. Without it: unconditional. |
38
38
 
39
39
  Field names are snake_case to match Claude Code and existing harness tool schemas.
40
40
 
41
- Canonical successes are `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `read_image` → `{ path, image: { attachmentId, mediaType, bytes, width, height, name? } }`, `write` → `{ path, operation: 'create' | 'update', before: string | null, after }`, and `edit` → `{ path, before, after }`. Native renderers preserve the line-numbered read and mutation acknowledgements below. `write`/`edit` derive replayable diff-card metadata, and `read` derives a replayable read-card window `{ path, offset, lines, totalLines, lang? }`, from these canonical values; the canonical values themselves are execution-local and are not added to `tool/result`, only the derived presentation metadata is persisted.
41
+ Structured successes are `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `read_image` → `{ path, image: { attachmentId, mediaType, bytes, width, height, name?, originalDimensions?: { width, height } } }`, `write` → `{ path, operation: 'create' | 'update', before: string | null, after }`, and `edit` → `{ path, before, after }`. `originalDimensions` appears only when normalization downscaled the submitted raster and records its orientation-applied input size. Native renderers preserve the line-numbered read and mutation acknowledgements below. `write`/`edit` derive replayable diff-card metadata, and `read` derives a replayable read-card window `{ path, offset, lines, totalLines, lang? }`; execution-local structured values are not added to `tool/result`, while image renderers emit the durable image blocks that the result logs.
42
42
 
43
43
  ## The tool is the executor; policy is an event gate
44
44
 
@@ -99,7 +99,7 @@ Prefix-stable while the plugin scope and guidance text are unchanged. Tool restr
99
99
 
100
100
  #### What the model sees
101
101
 
102
- The model sees the generated [`read`, `read_image`, `write`, and `edit` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs), with snake_case arguments. `read_image` appears only while a durable attachment store is mounted; the schema itself is route-independent, and the strict gate refuses at execution. Scoped tool restrictions can remove any definition for one agent.
102
+ The model sees the generated [`read`, `read_image`, `write`, and `edit` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs), with snake_case arguments. The image tool appears only while a durable attachment store is mounted; its schema is route-independent, and the strict gate refuses at execution. Scoped tool restrictions can remove any definition for one agent.
103
103
 
104
104
  #### Token effect
105
105
 
@@ -127,7 +127,7 @@ Append-only; newly visible content follows the reusable request prefix and does
127
127
 
128
128
  #### What the model sees
129
129
 
130
- A successful `read_image` returns `<path><displayPath></path>`, `<type>image</type>`, and a `<content>` envelope naming the media type, dimensions, and byte size, followed by the image itself as a native image block. The session log stores only the durable `sha256:` attachment reference; the routed provider re-reads and digest-verifies the bytes on each request.
130
+ A successful `read_image` returns `<path><displayPath></path>`, `<type>image</type>`, and a `<content>` envelope naming the media type, normalized dimensions, and byte size, followed by the image itself as a native image block. The result is logged with its durable reference before the next model request.
131
131
 
132
132
  #### Token effect
133
133
 
@@ -155,7 +155,7 @@ Append-only; newly visible content follows the reusable request prefix and does
155
155
 
156
156
  #### What the model sees
157
157
 
158
- Failures are normalized as `Error: <message>`. This package's stable validation and read messages are `file_path must be a non-empty string`, `limit must be less than or equal to <max>`, `old_string must be a non-empty string`, `old_string and new_string must differ`, `cannot read "<path>": not found`, `cannot read "<path>": not a regular file`, `offset <offset> is out of range for "<path>" (<total> lines)`, `cannot read "<path>": read_image only accepts PNG/JPEG/WebP/GIF paths`, `cannot read "<path>" as an image: model "<model>" does not declare image input; switch to an image-capable model to read images`, and the mismatch repair `cannot read "<path>": the <ext> extension declares <type>, 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`; provider and policy templates are quoted in their package READMEs. Guarded-mutation failures additionally carry their recovery instruction in the message, appended by this package's model-facing error wrapper: `FS_STALE_VERSION` gets `— re-read the file, then retry`, and `FS_NOT_OBSERVED` gets `— read the file, then retry`; the structured code is preserved. After that reread confirms absence, edit reports `FS_NOT_FOUND` instead of repeating a stale remedy, while write uses guarded creation.
158
+ Failures are normalized as `Error: <message>`. This package's stable validation and read messages are `file_path must be a non-empty string`, `limit must be less than or equal to <max>`, `old_string must be a non-empty string`, `old_string and new_string must differ`, `cannot read "<path>": not found`, `cannot read "<path>": not a regular file`, `offset <offset> is out of range for "<path>" (<total> lines)`, `cannot read "<path>": read_image only accepts PNG/JPEG/WebP/GIF paths`, `cannot read "<path>" as an image: model "<model>" does not declare image input; switch to an image-capable model to read images`, and the mismatch repair `cannot read "<path>": the <ext> extension declares <type>, 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`. A failed 16-bit conversion reports `cannot read "<path>": the 16-bit PNG could not be converted to the normalized 8-bit sRGB form; convert it to an 8-bit PNG/JPEG/WebP and retry`. Provider and policy templates are quoted in their package READMEs. Guarded-mutation failures additionally carry their recovery instruction in the message, appended by this package's model-facing error wrapper: `FS_STALE_VERSION` gets `— re-read the file, then retry`, and `FS_NOT_OBSERVED` gets `— read the file, then retry`; the structured code is preserved. After that reread confirms absence, edit reports `FS_NOT_FOUND` instead of repeating a stale remedy, while write uses guarded creation.
159
159
 
160
160
  #### Token effect
161
161
 
@@ -169,7 +169,7 @@ Append-only; newly visible content follows the reusable request prefix and does
169
169
 
170
170
  - **No model-facing directory listing ships** — `ctx.fs.listDir` serves provider code such as skill discovery, while the sibling [`dsh-tool-fs-search`](../tool-fs-search/) package supplies ripgrep-backed `glob` and `grep` rather than extending the filesystem seam.
171
171
  - **`read` handles UTF-8 text files only** — images use the separate extension-routed `read_image` tool; PDF, audio, and video remain deferred. A directory target is `FS_NOT_REGULAR_FILE`.
172
- - **The route gate races a concurrent model switch** — `read_image` checks the latest routed model at execution; a switch committed between that check and the next request can leave an image block on a route that rejects image content. The Web host already refuses switching an image-bearing session to a text-only model; other front doors own their equivalent guard.
173
172
  - **Extension-declared media type** — the extension selects the declared type and the attachment store's magic-byte validation stays authoritative; a correctly formatted image under a wrong extension is refused with the rename remedy rather than sniffed.
174
173
  - **No inline image preview on the tool-result card** — UI surfaces render the image result generically (the durable reference, not pixels); inline rendering is deferred to the UI packages.
174
+ - **No attachment-region tool** — an agent may crop an image through other available tools when it has a filesystem path. A pasted or dragged image without a path cannot be re-read at a higher resolution.
175
175
  - **No timeout surface** — `read`/`write`/`edit` take no timeout argument and declare no `timeout-policy` budget; cancellation rides `exec.signal` only ([provider rationale](../README.md#no-timeouts-on-file-io)).
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- **面向模型的文件系统工具**(`read`、`read_image`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-observation-policy`](../fs-observation-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会为文件系统变更提供升权路径。
5
+ **面向模型的文件系统工具**(`read`、`read_image`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))读取、写入和编辑。新鲜度与观察策略由独立插件([`@deepseek-ai/dsh-fs-observation-policy`](../fs-observation-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会为文件系统变更提供升权路径。
6
6
 
7
7
  ```ts ignore-check
8
8
  // Default deployment: a ctx.fs provider, the policy plugin, then the tools.
@@ -14,7 +14,7 @@ await ctx.plugin(ToolFs) // this package — re
14
14
 
15
15
  `@deepseek-ai/dsh-fs-observation-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供写入/编辑前读取行为。
16
16
 
17
- `read_image` 只在持久 `ctx.attachments` 服务已挂载时注册:没有它,部署无法持久提交图像字节,工具就不会出现。执行时还要求确切路由的模型声明 `image` 输入(通过 `ctx.llm.resolveModelInfo` 从会话最新请求 header 解析,缺失时回退到 agent 选项);未知或纯文本路由在任何文件系统 I/O 之前就得到拒绝结果,因此文本路由的持久历史不会出现图像块。
17
+ `read_image` 只在持久 `ctx.attachments` 服务已挂载时注册。执行时还要求确切路由的模型声明 `image` 输入,通过 `ctx.llm.resolveModelInfo` 依次从会话最新请求 header 和 agent 选项解析。
18
18
 
19
19
  ## 配置
20
20
 
@@ -27,22 +27,22 @@ await ctx.plugin(ToolFs) // this package — re
27
27
  | `readMaxBytes` | `51200` | 一次 `read` 调用所选行的字节上限;溢出时以「已达上限」footer 结束窗口。 |
28
28
  | `readStreamMinSize` | `10485760` | 大于等于该大小或大小未知的文件采用流式读取,而不是整体加载到内存。 |
29
29
 
30
- ## 工具(schema 见[文件系统工具 schema Agent Note](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md))
30
+ ## 工具(schema 见[文件系统工具 schema Agent Note](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.zh.md))
31
31
 
32
32
  | 工具 | 参数 | 行为 |
33
33
  |---|---|---|
34
34
  | `read` | `file_path`、`offset?`、`limit?` | 带行号的 UTF-8 内容和分页 footer。`offset` 从 1 开始;`limit` 默认为配置的 `readLimit`(2000),上限也为该值。 |
35
- | `read_image` | `file_path` | 通过有界字节 seam 读取 PNG/JPEG/WebP/GIF 文件,经 `ctx.attachments.saveImage` 持久保存,并在小型元数据信封旁返回图像块。只有确切路由的模型声明图像输入时才会成功。 |
35
+ | `read_image` | `file_path` | 通过有界字节 seam 读取 PNG/JPEG/WebP/GIF 文件,经 `ctx.attachments.saveImage` 持久保存,并在小型元数据信封旁返回图像块。Harness 会在下一次模型请求前校验并缩小受支持的大图,因此模型可以直接读取源文件,无需先创建缩略图。只有确切路由的模型声明图像输入时才会成功。 |
36
36
  | `write` | `file_path`、`content` | 创建文件或完整替换文件。有策略插件时:覆盖现有文件要求先在未变版本上执行 `read`;创建新文件不需要。没有插件时:无条件执行。 |
37
37
  | `edit` | `file_path`、非空 `old_string`、`new_string`、`replace_all?` | 字面量替换;除非 `replace_all` 为 true,否则要求唯一匹配。有策略插件时:要求先执行 `read`(任何窗口),且文件此后未变。没有插件时:无条件执行。 |
38
38
 
39
39
  字段名使用 snake_case,与 Claude Code 和现有 harness 工具 schema 一致。
40
40
 
41
- 规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`read_image` → `{ path, image: { attachmentId, mediaType, bytes, width, height, name? } }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。原生渲染器会保留下方带行号的读取结果和变更确认。`write`/`edit` 从这些规范值派生可回放的 diff 卡片元数据,`read` 派生可回放的读取卡片窗口 `{ path, offset, lines, totalLines, lang? }`;规范值本身仅限于本次执行,不会添加到 `tool/result`,只有派生出的呈现元数据会被持久化。
41
+ 结构化成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`read_image` → `{ path, image: { attachmentId, mediaType, bytes, width, height, name?, originalDimensions?: { width, height } } }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。`originalDimensions` 只在规范化过程缩小提交光栅时出现,并记录应用方向后的输入尺寸。原生渲染器会保留下方带行号的读取结果和变更确认。`write` 和 `edit` 从这些值派生可回放的 diff 卡片元数据,`read` 派生可回放的读取卡片窗口 `{ path, offset, lines, totalLines, lang? }`;仅用于执行的结构化值不会添加到 `tool/result`,图片渲染器则会发出由结果记录的持久图片块。
42
42
 
43
43
  ## 工具就是执行器;策略是事件门禁
44
44
 
45
- 工具**不**注入策略服务,也不检查任何缓存。每个工具通过 `ctx.fs.resolve(path, { cwd, signal })` 解析路径;它会传入调用 agent(智能体)的会话 cwd(`exec.agent.session.header.cwd`),使相对路径以会话工作区为基准解析并与 `dsh-tool-bash` 一致,同时把工具取消转发到解析过程(见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md))。随后执行:
45
+ 工具**不**注入策略服务,也不检查任何缓存。每个工具通过 `ctx.fs.resolve(path, { cwd, signal })` 解析路径;它会传入调用 agent(智能体)的会话 cwd(`exec.agent.session.header.cwd`),使相对路径以会话工作区为基准解析并与 `dsh-tool-bash` 一致,同时把工具取消转发到解析过程(见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md))。随后执行:
46
46
 
47
47
  - **read**:一次 `ctx.fs.stat`(用于类型、大小路由和版本),随后调用 `readText`/`streamText`,构建行窗口,再发出 `fs/observed`,使用普通 `ctx.emit`。(1 次 stat。)
48
48
  - **read_image**:在任何 I/O 之前校验参数、扩展名、附件可用性、部署接受的媒体类型和图像路由;随后一次 `ctx.fs.stat`(目标缺失时与 `read` 一样记录 `absent` 观察)、以 `imageLimits.maxImageBytes` 与 `imageLimits.maxMessageImageBytes` 中较小者为上限的有界 `ctx.fs.readBytes`(结果是携带一张图像的一条消息)、`attachments.saveImage`(内容寻址,因此在 `tool/result` 事件追加时图像块引用的对象已持久提交),最后发出 `fs/observed`。(1 次 stat。)
@@ -57,7 +57,7 @@ await ctx.plugin(ToolFs) // this package — re
57
57
 
58
58
  `fs/observed` 在 read/read_image/write/edit 已经成功之后,通过普通 `ctx.emit` 发出。监听器的约定是同步且只有副作用的记录器(`@deepseek-ai/dsh-fs-observation-policy` 使用 `WeakMap.set`);工具不保护这次发出,因此监听器抛出会作为工具的 `isError` 结果出现。异步或可能失败的观察不属于该事件。
59
59
 
60
- `read` 允许并发调度,因为它唯一会改变状态的操作是同步记录版本。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此即使记录器发生竞态,系统也会安全地拒绝操作;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
60
+ `read` 允许并发调度,因为它唯一会改变状态的操作是同步记录版本。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此即使记录器发生竞态,系统也会安全地拒绝操作;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md)。
61
61
 
62
62
  包根目录只导出 Cordis 插件约定(`name`、`inject`、`Config` 和 `apply`)。读取渲染(行窗口与输出格式化)位于 `src/read-render.ts`(不依赖 Cordis,单独进行单元测试);`src/read.ts`/`read-image.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。
63
63
 
@@ -99,7 +99,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
99
99
 
100
100
  #### 模型看到的内容
101
101
 
102
- 模型会看到已生成的 [`read`、`read_image`、`write` 和 `edit` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs),参数使用 snake_case。`read_image` 只在持久附件存储已挂载时出现;schema 本身与路由无关,严格门禁在执行时拒绝。作用域工具限制可以为某个 agent 移除任一定义。
102
+ 模型会看到已生成的 [`read`、`read_image`、`write` 和 `edit` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-fs),参数使用 snake_case。图片工具只在持久附件存储已挂载时出现;schema 本身与路由无关,严格门禁在执行时拒绝。作用域工具限制可以为某个 agent 移除任一定义。
103
103
 
104
104
  #### Token 影响
105
105
 
@@ -127,7 +127,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
127
127
 
128
128
  #### 模型看到的内容
129
129
 
130
- 成功的 `read_image` 返回 `<path><displayPath></path>`、`<type>image</type>` 和写明媒体类型、尺寸与字节数的 `<content>` 信封,随后是作为原生图像块的图像本身。会话日志只存储持久的 `sha256:` 附件引用;路由到的提供方在每次请求时重新读取并校验字节摘要。
130
+ 成功的 `read_image` 返回 `<path><displayPath></path>`、`<type>image</type>` 和写明媒体类型、规范化尺寸与字节数的 `<content>` 信封,随后是作为原生图像块的图像本身。结果会随持久引用写入会话日志,然后才进入下一次模型请求。
131
131
 
132
132
  #### Token 影响
133
133
 
@@ -155,7 +155,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
155
155
 
156
156
  #### 模型看到的内容
157
157
 
158
- 失败会规范化为 `Error: <message>`。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`limit must be less than or equal to <max>`、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read "<path>": not found`、`cannot read "<path>": not a regular file`、`offset <offset> is out of range for "<path>" (<total> lines)`、`cannot read "<path>": read_image only accepts PNG/JPEG/WebP/GIF paths`、`cannot read "<path>" as an image: model "<model>" does not declare image input; switch to an image-capable model to read images`,以及类型不匹配的修复消息 `cannot read "<path>": the <ext> extension declares <type>, 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`;提供方和策略模板在各自包的 README 中逐字列出。防护变更失败还会在消息中携带恢复指令,由本包面向模型的错误包装追加:`FS_STALE_VERSION` 追加 `— re-read the file, then retry`,`FS_NOT_OBSERVED` 追加 `— read the file, then retry`;结构化错误码保持不变。该次重新读取确认缺失后,edit 会报告 `FS_NOT_FOUND`,而不会重复陈旧恢复指令;write 则使用带防护的创建。
158
+ 失败会规范化为 `Error: <message>`。本包稳定的校验和读取消息是 `file_path must be a non-empty string`、`limit must be less than or equal to <max>`、`old_string must be a non-empty string`、`old_string and new_string must differ`、`cannot read "<path>": not found`、`cannot read "<path>": not a regular file`、`offset <offset> is out of range for "<path>" (<total> lines)`、`cannot read "<path>": read_image only accepts PNG/JPEG/WebP/GIF paths`、`cannot read "<path>" as an image: model "<model>" does not declare image input; switch to an image-capable model to read images`,以及类型不匹配的修复消息 `cannot read "<path>": the <ext> extension declares <type>, 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`。16-bit 转换失败会报告 `cannot read "<path>": the 16-bit PNG could not be converted to the normalized 8-bit sRGB form; convert it to an 8-bit PNG/JPEG/WebP and retry`。提供方和策略模板在各自包的 README 中逐字列出。防护变更失败还会在消息中携带恢复指令,由本包面向模型的错误包装追加:`FS_STALE_VERSION` 追加 `re-read the file, then retry`,`FS_NOT_OBSERVED` 追加 `read the file, then retry`;结构化错误码保持不变。该次重新读取确认缺失后,edit 会报告 `FS_NOT_FOUND`,不会重复陈旧恢复指令;write 则使用带防护的创建。
159
159
 
160
160
  #### Token 影响
161
161
 
@@ -169,7 +169,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
169
169
 
170
170
  - **未交付面向模型的目录列表工具**:`ctx.fs.listDir` 服务于 skill(技能)发现等提供方代码,同级 [`dsh-tool-fs-search`](../tool-fs-search/) 包则提供基于 ripgrep 的 `glob` 与 `grep`,而不是扩展文件系统 seam。
171
171
  - **`read` 只处理 UTF-8 文本文件**:图像使用独立的、按扩展名路由的 `read_image` 工具;PDF、音频和视频仍延期处理。目录目标为 `FS_NOT_REGULAR_FILE`。
172
- - **路由门禁与并发模型切换存在竞态**:`read_image` 在执行时检查最新路由的模型;在该检查与下一次请求之间提交的切换,可能让图像块落在拒绝图像内容的路由上。Web 宿主已拒绝把含图像的会话切到纯文本模型;其他前端拥有各自的等价防护。
173
172
  - **媒体类型按扩展名声明**:扩展名选择声明类型,附件存储的魔数校验保持权威;扩展名错误但格式正确的图像会得到改名修复提示,而不是被嗅探接受。
174
173
  - **工具结果卡片没有内嵌图像预览**:UI 表面以通用形式渲染图像结果(持久引用而非像素);内嵌渲染延后到 UI 包处理。
175
- - **没有超时接口**:`read`/`write`/`edit` 不接受超时参数,也不声明 `timeout-policy` 预算;取消只通过 `exec.signal` 传递(见[提供方理由](../README.md#no-timeouts-on-file-io))。
174
+ - **没有附件局部读取工具**:图片具有文件路径时,agent 可以用其他可用工具裁剪。粘贴或拖入但没有路径的图片无法按更高分辨率重新读取。
175
+ - **没有超时接口**:`read`/`write`/`edit` 不接受超时参数,也不声明 `timeout-policy` 预算;取消只通过 `exec.signal` 传递(见[提供方理由](../README.zh.md#no-timeouts-on-file-io))。
package/lib/index.js CHANGED
@@ -851,15 +851,12 @@ function applyEditTool(ctx, sandbox) {
851
851
  //#endregion
852
852
  //#region lib/types/read-image.js
853
853
  /**
854
- * The model-facing `read_image` tool: reads a PNG/JPEG/WebP/GIF file, durably
855
- * commits its bytes through the attachment service (the same lifecycle as a
856
- * user-uploaded image), and returns an image block so the image enters model
857
- * context from the next request onward.
854
+ * The model-facing `read_image` tool commits a PNG/JPEG/WebP/GIF file.
858
855
  *
859
- * The route gate is deliberately stricter than the host upload preflight: a
860
- * tool result enters durable session history, so emitting an image on a route
861
- * that cannot carry it would break that route's continuation. Unknown
862
- * capability therefore refuses instead of relying on the adapter guard.
856
+ * The route gate is deliberately stricter than the host upload preflight. An
857
+ * image-reading tool is useful only when the exact calling route can inspect
858
+ * its result, so unknown capability refuses instead of relying on an adapter
859
+ * failure after filesystem and attachment work.
863
860
  * @module @deepseek-ai/dsh-tool-fs/src/read-image
864
861
  */
865
862
  /** Extensions `read_image` accepts; magic-byte validation at the attachment service stays authoritative. */
@@ -870,6 +867,54 @@ const IMAGE_EXTENSIONS = {
870
867
  ".webp": "image/webp",
871
868
  ".gif": "image/gif"
872
869
  };
870
+ const IMAGE_VALUE_SCHEMA = {
871
+ type: "object",
872
+ additionalProperties: false,
873
+ required: true,
874
+ properties: {
875
+ attachmentId: {
876
+ type: "string",
877
+ required: true
878
+ },
879
+ mediaType: {
880
+ type: "string",
881
+ enum: [
882
+ "image/png",
883
+ "image/jpeg",
884
+ "image/webp",
885
+ "image/gif"
886
+ ],
887
+ required: true
888
+ },
889
+ bytes: {
890
+ type: "integer",
891
+ required: true
892
+ },
893
+ width: {
894
+ type: "integer",
895
+ required: true
896
+ },
897
+ height: {
898
+ type: "integer",
899
+ required: true
900
+ },
901
+ name: { type: "string" },
902
+ originalDimensions: {
903
+ type: "object",
904
+ additionalProperties: false,
905
+ properties: {
906
+ width: {
907
+ type: "integer",
908
+ required: true
909
+ },
910
+ height: {
911
+ type: "integer",
912
+ required: true
913
+ }
914
+ }
915
+ }
916
+ }
917
+ };
873
918
  /**
874
919
  * Map a model-supplied path to its declared image media type by extension.
875
920
  * @param filePath - the raw `file_path` argument (not yet resolved).
@@ -896,9 +941,9 @@ async function assertImageCapableRoute(ctx, exec, requestedPath) {
896
941
  if (active.inputModalities === void 0 || !active.inputModalities.includes("image")) throw new Error(`cannot read "${requestedPath}" as an image: model "${model}" does not declare image input; switch to an image-capable model to read images`);
897
942
  }
898
943
  /**
899
- * Re-brand a canonical image outcome into the durable attachment reference an
944
+ * Re-brand a structured image outcome into the durable attachment reference an
900
945
  * `ImageBlock` carries.
901
- * @param image - the canonical image metadata from the output schema.
946
+ * @param image - the image metadata from the output schema.
902
947
  * @returns the branded attachment reference.
903
948
  */
904
949
  function imageRefFromValue(image) {
@@ -908,25 +953,35 @@ function imageRefFromValue(image) {
908
953
  bytes: image.bytes,
909
954
  width: image.width,
910
955
  height: image.height,
911
- ...image.name === void 0 ? {} : { name: image.name }
956
+ ...image.name === void 0 ? {} : { name: image.name },
957
+ ...image.originalDimensions === void 0 ? {} : { originalDimensions: { ...image.originalDimensions } }
912
958
  };
913
959
  }
914
960
  /**
915
961
  * Format an image read as the model-facing envelope beside its image block.
962
+ * A downscaled read names the on-disk dimensions and the multiplier that maps
963
+ * coordinates measured on the attached image back onto the original file.
916
964
  * @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
917
- * @param image - the canonical image metadata to summarize.
965
+ * @param image - the image metadata to summarize.
918
966
  * @returns the model-facing envelope; the image itself rides the adjacent image block.
919
967
  */
920
968
  function formatImageReadOutput(displayPath, image) {
969
+ let scaled = "";
970
+ if (image.originalDimensions !== void 0) {
971
+ const x = (image.originalDimensions.width / image.width).toFixed(2);
972
+ const y = (image.originalDimensions.height / image.height).toFixed(2);
973
+ const advice = x === y ? `multiply coordinates by ${x}` : `multiply x coordinates by ${x} and y coordinates by ${y}`;
974
+ scaled = ` (downscaled from ${image.originalDimensions.width}x${image.originalDimensions.height} px; ${advice} to locate features in the original file)`;
975
+ }
921
976
  return `<path>${displayPath}</path>
922
977
  <type>image</type>
923
978
  <content>
924
- ${image.mediaType} image, ${image.width}x${image.height} px, ${image.bytes} bytes
979
+ ${image.mediaType} image, ${image.width}x${image.height} px, ${image.bytes} bytes${scaled}
925
980
  </content>`;
926
981
  }
927
982
  /**
928
- * Project one canonical image read into its model-facing envelope and image.
929
- * @param value - the canonical image-read outcome.
983
+ * Project one structured image read into its model-facing envelope and image.
984
+ * @param value - the image-read outcome.
930
985
  * @returns the two content blocks used by native and nested dispatches.
931
986
  */
932
987
  function imageReadContent(value) {
@@ -950,7 +1005,7 @@ function imageReadContent(value) {
950
1005
  function applyReadImageTool(ctx) {
951
1006
  ctx.tools.register(defineTool({
952
1007
  name: "read_image",
953
- description: "Read a PNG/JPEG/WebP/GIF file and return the image itself. Requires the current model to accept image input.",
1008
+ 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.",
954
1009
  parameters: { file_path: {
955
1010
  type: "string",
956
1011
  required: true,
@@ -965,40 +1020,7 @@ function applyReadImageTool(ctx) {
965
1020
  type: "string",
966
1021
  required: true
967
1022
  },
968
- image: {
969
- type: "object",
970
- additionalProperties: false,
971
- required: true,
972
- properties: {
973
- attachmentId: {
974
- type: "string",
975
- required: true
976
- },
977
- mediaType: {
978
- type: "string",
979
- enum: [
980
- "image/png",
981
- "image/jpeg",
982
- "image/webp",
983
- "image/gif"
984
- ],
985
- required: true
986
- },
987
- bytes: {
988
- type: "integer",
989
- required: true
990
- },
991
- width: {
992
- type: "integer",
993
- required: true
994
- },
995
- height: {
996
- type: "integer",
997
- required: true
998
- },
999
- name: { type: "string" }
1000
- }
1001
- }
1023
+ image: IMAGE_VALUE_SCHEMA
1002
1024
  }
1003
1025
  },
1004
1026
  render: (_args, value) => imageReadContent(value)
@@ -1026,6 +1048,8 @@ function applyReadImageTool(ctx) {
1026
1048
  if (!(error instanceof AttachmentError)) throw error;
1027
1049
  if (error.code === "IMAGE_DIMENSION_TOO_LARGE") throw new Error(`cannot read "${target.displayPath}": at least one image side exceeds the ${attachments.imageLimits.maxImageDimension}px limit; downscale the image and read the smaller copy`, { cause: error });
1028
1050
  if (error.code === "IMAGE_TOO_MANY_PIXELS") throw new Error(`cannot read "${target.displayPath}": the image exceeds the ${attachments.imageLimits.maxImagePixels}-pixel decoded-size limit; downscale the image and read the smaller copy`, { cause: error });
1051
+ if (error.code === "IMAGE_TOO_LARGE") throw new Error(`cannot read "${target.displayPath}": the image cannot be stored within the deployment's byte limits; downscale the image and read the smaller copy`, { cause: error });
1052
+ if (error.code === "ATTACHMENT_WRITE_FAILED" && /16-bit PNG/iu.test(error.message)) throw new Error(`cannot read "${target.displayPath}": the 16-bit PNG could not be converted to the normalized 8-bit sRGB form; convert it to an 8-bit PNG/JPEG/WebP and retry`, { cause: error });
1029
1053
  if (error.code !== "IMAGE_TYPE_MISMATCH") throw error;
1030
1054
  const extension = extname(target.displayPath).toLowerCase();
1031
1055
  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 });
@@ -1042,7 +1066,8 @@ function applyReadImageTool(ctx) {
1042
1066
  bytes: ref.bytes,
1043
1067
  width: ref.width,
1044
1068
  height: ref.height,
1045
- ...ref.name === void 0 ? {} : { name: ref.name }
1069
+ ...ref.name === void 0 ? {} : { name: ref.name },
1070
+ ...ref.originalDimensions === void 0 ? {} : { originalDimensions: { ...ref.originalDimensions } }
1046
1071
  }
1047
1072
  };
1048
1073
  },
@@ -1,19 +1,16 @@
1
1
  /**
2
- * The model-facing `read_image` tool: reads a PNG/JPEG/WebP/GIF file, durably
3
- * commits its bytes through the attachment service (the same lifecycle as a
4
- * user-uploaded image), and returns an image block so the image enters model
5
- * context from the next request onward.
2
+ * The model-facing `read_image` tool commits a PNG/JPEG/WebP/GIF file.
6
3
  *
7
- * The route gate is deliberately stricter than the host upload preflight: a
8
- * tool result enters durable session history, so emitting an image on a route
9
- * that cannot carry it would break that route's continuation. Unknown
10
- * capability therefore refuses instead of relying on the adapter guard.
4
+ * The route gate is deliberately stricter than the host upload preflight. An
5
+ * image-reading tool is useful only when the exact calling route can inspect
6
+ * its result, so unknown capability refuses instead of relying on an adapter
7
+ * failure after filesystem and attachment work.
11
8
  * @module @deepseek-ai/dsh-tool-fs/src/read-image
12
9
  */
13
10
  import type { Context } from '@deepseek-ai/cordis';
14
11
  import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
15
12
  import type { ToolExecution } from '@deepseek-ai/dsh-tools';
16
- /** The canonical outcome declared by the `read_image` output schema. */
13
+ /** The structured outcome declared by the `read_image` output schema. */
17
14
  export interface ImageReadValue {
18
15
  path: string;
19
16
  image: {
@@ -23,6 +20,11 @@ export interface ImageReadValue {
23
20
  width: number;
24
21
  height: number;
25
22
  name?: string;
23
+ /** Orientation-applied file dimensions before normalization; present only when storage reduced it. */
24
+ originalDimensions?: {
25
+ width: number;
26
+ height: number;
27
+ };
26
28
  };
27
29
  }
28
30
  /**
@@ -41,16 +43,18 @@ export declare function imageMediaTypeForPath(filePath: string): ImageMediaType
41
43
  */
42
44
  export declare function assertImageCapableRoute(ctx: Context, exec: ToolExecution, requestedPath: string): Promise<void>;
43
45
  /**
44
- * Re-brand a canonical image outcome into the durable attachment reference an
46
+ * Re-brand a structured image outcome into the durable attachment reference an
45
47
  * `ImageBlock` carries.
46
- * @param image - the canonical image metadata from the output schema.
48
+ * @param image - the image metadata from the output schema.
47
49
  * @returns the branded attachment reference.
48
50
  */
49
51
  export declare function imageRefFromValue(image: ImageReadValue['image']): ImageAttachmentRef;
50
52
  /**
51
53
  * Format an image read as the model-facing envelope beside its image block.
54
+ * A downscaled read names the on-disk dimensions and the multiplier that maps
55
+ * coordinates measured on the attached image back onto the original file.
52
56
  * @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
53
- * @param image - the canonical image metadata to summarize.
57
+ * @param image - the image metadata to summarize.
54
58
  * @returns the model-facing envelope; the image itself rides the adjacent image block.
55
59
  */
56
60
  export declare function formatImageReadOutput(displayPath: string, image: ImageReadValue['image']): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-tool-fs",
3
3
  "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)",
4
- "version": "0.1.0-rc.8",
4
+ "version": "0.1.1-rc.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -36,35 +36,35 @@
36
36
  "@deepseek-ai/schemastery": "^3.18.1"
37
37
  },
38
38
  "peerDependencies": {
39
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8",
40
- "@deepseek-ai/dsh-fs": "^0.1.0-rc.8",
41
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
42
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
43
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8",
44
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
45
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
46
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
47
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8",
39
+ "@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
40
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
41
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
42
+ "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2",
43
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.2",
44
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
45
+ "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
47
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
48
48
  "@deepseek-ai/cordis": "^4.0.1",
49
- "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8"
49
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
53
- "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.8",
54
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8",
55
- "@deepseek-ai/dsh-fs": "^0.1.0-rc.8",
56
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.0-rc.8",
57
- "@deepseek-ai/dsh-fs-observation-policy": "^0.1.0-rc.8",
58
- "@deepseek-ai/dsh-fs-local": "^0.1.0-rc.8",
59
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
60
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
61
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.8",
62
- "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.8",
63
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8",
64
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
65
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
66
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
67
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8",
68
- "@deepseek-ai/cordis": "^4.0.1"
52
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
53
+ "@deepseek-ai/dsh-agent-loop": "^0.1.1-rc.2",
54
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.1-rc.2",
55
+ "@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
56
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
57
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
58
+ "@deepseek-ai/dsh-fs-observation-policy": "^0.1.1-rc.2",
59
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.1-rc.2",
60
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
61
+ "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2",
62
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.2",
63
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
64
+ "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
65
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
66
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2",
67
+ "@deepseek-ai/cordis": "^4.0.1",
68
+ "@deepseek-ai/dsh-fs-local": "^0.1.1-rc.2"
69
69
  }
70
70
  }