@deepseek-ai/dsh-tool-fs 0.0.1-rc.1 → 0.0.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 +2 -2
- package/README.md +30 -8
- package/README.zh.md +30 -8
- package/lib/index.js +254 -15
- package/lib/types/edit.d.ts +3 -3
- package/lib/types/index.d.ts +2 -2
- package/lib/types/read-image.d.ts +67 -0
- package/lib/types/read-target.d.ts +19 -0
- package/lib/types/sandbox.d.ts +3 -3
- package/lib/types/write.d.ts +3 -3
- package/package.json +28 -26
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 7e334f886747cd8dc566a572c699cd80c7cf62fe
|
|
6
|
+
README.zh.md: b5eb5ae38aba049d77375d31d1509342a23f13fc
|
package/README.md
CHANGED
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
The **model-facing filesystem tools** — `read`, `write`, `edit` — and their **executor**. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, **read windowing**, and result formatting. It reads/writes/edits through the `ctx.fs` provider contract ([`@deepseek-ai/dsh-fs`](../fs)) **directly**. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. Under a confining provider, the shared sandbox-policy service is required for per-session execution and the tool exposes escalation for filesystem mutations.
|
|
5
|
+
The **model-facing filesystem tools** — `read`, `read_image`, `write`, `edit` — and their **executor**. This is the consumer layer of the filesystem stack: it owns tool names, JSON schemas, argument validation, prompt sections, **read windowing**, and result formatting. It reads/writes/edits through the `ctx.fs` provider contract ([`@deepseek-ai/dsh-fs`](../fs)) **directly**. The freshness/observation policy is contributed by a separate plugin ([`@deepseek-ai/dsh-fs-policy`](../fs-policy)) through the `fs/*` event gate; the tool is not method-coupled to it. Under a confining provider, the shared sandbox-policy service is required for per-session execution and the tool exposes escalation for filesystem mutations.
|
|
6
6
|
|
|
7
7
|
```ts ignore-check
|
|
8
8
|
// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
|
|
9
9
|
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @deepseek-ai/dsh-fs-local
|
|
10
10
|
await ctx.plugin(FsPolicy) // @deepseek-ai/dsh-fs-policy (policy gate)
|
|
11
|
-
await ctx.plugin(
|
|
11
|
+
await ctx.plugin(LocalAttachmentStore, { dshHome }) // optional — enables durable read_image results
|
|
12
|
+
await ctx.plugin(ToolFs) // this package — read/write/edit, plus read_image with attachments
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
`@deepseek-ai/dsh-fs-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.
|
|
15
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.
|
|
18
|
+
|
|
16
19
|
## Config
|
|
17
20
|
|
|
18
21
|
All keys are optional; the defaults are the shipped read caps.
|
|
@@ -29,18 +32,20 @@ All keys are optional; the defaults are the shipped read caps.
|
|
|
29
32
|
| Tool | Arguments | Behavior |
|
|
30
33
|
|---|---|---|
|
|
31
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. |
|
|
32
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. |
|
|
33
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. |
|
|
34
38
|
|
|
35
39
|
Field names are snake_case to match Claude Code and existing harness tool schemas.
|
|
36
40
|
|
|
37
|
-
Canonical successes are `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `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
|
+
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.
|
|
38
42
|
|
|
39
43
|
## The tool is the executor; policy is an event gate
|
|
40
44
|
|
|
41
45
|
The tools do **not** inject a policy service or inspect any cache. Each tool resolves the path via `ctx.fs.resolve(path, { cwd, signal })` — passing the calling agent's session cwd (`exec.agent.session.header.cwd`) so a relative path resolves against the session's workspace, matching `dsh-tool-bash`, and forwarding tool cancellation through resolution (see [the per-session cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)) — then:
|
|
42
46
|
|
|
43
47
|
- **read** — one `ctx.fs.stat` (type + size routing + version), then `readText`/`streamText`, then builds the line window, then emits `fs/observed` with a plain `ctx.emit`. (1 stat.)
|
|
48
|
+
- **read_image** — validates the argument, extension, attachment availability, deployment media types, and the image-capable route before any I/O; then one `ctx.fs.stat` (recording an `absent` observation for a missing target, like `read`), a bounded `ctx.fs.readBytes` capped at the smaller of `imageLimits.maxImageBytes` and `imageLimits.maxMessageImageBytes` (the result is one message carrying one image), `attachments.saveImage` (content-addressed, so the image block references a durably committed object by the time `tool/result` is appended), and finally `fs/observed`. (1 stat.)
|
|
44
49
|
- **write** — `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.writeText(target, content, intent)`, then `fs/observed`. (0 stat.)
|
|
45
50
|
- **edit** — `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.editText(target, edit, intent)`, then `fs/observed`. (0 stat.)
|
|
46
51
|
|
|
@@ -50,11 +55,11 @@ When `ctx.fs.sandboxMode` reports confinement, write/edit advertise `sandbox_per
|
|
|
50
55
|
|
|
51
56
|
## `fs/observed` is fire-and-forget
|
|
52
57
|
|
|
53
|
-
`fs/observed` fires AFTER the read/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event.
|
|
58
|
+
`fs/observed` fires AFTER the read/read_image/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event.
|
|
54
59
|
|
|
55
60
|
`read` opts into concurrent scheduling because its only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
|
|
56
61
|
|
|
57
|
-
The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them.
|
|
62
|
+
The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`read-image.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them.
|
|
58
63
|
|
|
59
64
|
## Model Experience
|
|
60
65
|
|
|
@@ -94,7 +99,7 @@ Prefix-stable while the plugin scope and guidance text are unchanged. Tool restr
|
|
|
94
99
|
|
|
95
100
|
#### What the model sees
|
|
96
101
|
|
|
97
|
-
The model sees the generated [`read`, `write`, and `edit` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs), with snake_case arguments. 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. `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.
|
|
98
103
|
|
|
99
104
|
#### Token effect
|
|
100
105
|
|
|
@@ -118,6 +123,20 @@ Read output is capped by `readLimit`, `readMaxLineLength`, and `readMaxBytes`; t
|
|
|
118
123
|
|
|
119
124
|
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
120
125
|
|
|
126
|
+
### Image read result
|
|
127
|
+
|
|
128
|
+
#### What the model sees
|
|
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.
|
|
131
|
+
|
|
132
|
+
#### Token effect
|
|
133
|
+
|
|
134
|
+
The image is billed on every later request until compaction. Each call is independently bounded by the attachment store's `maxImageBytes`/`maxImagePixels`; repeated successful calls accumulate history, and content addressing deduplicates only the stored bytes, not the per-request token cost.
|
|
135
|
+
|
|
136
|
+
#### KV Cache effect
|
|
137
|
+
|
|
138
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
139
|
+
|
|
121
140
|
### Write and edit results
|
|
122
141
|
|
|
123
142
|
#### What the model sees
|
|
@@ -136,7 +155,7 @@ Append-only; newly visible content follows the reusable request prefix and does
|
|
|
136
155
|
|
|
137
156
|
#### What the model sees
|
|
138
157
|
|
|
139
|
-
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`,
|
|
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.
|
|
140
159
|
|
|
141
160
|
#### Token effect
|
|
142
161
|
|
|
@@ -149,5 +168,8 @@ Append-only; newly visible content follows the reusable request prefix and does
|
|
|
149
168
|
## Known Limitations and Deferred Work
|
|
150
169
|
|
|
151
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.
|
|
152
|
-
- **`read` handles UTF-8 text files only** —
|
|
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
|
+
- **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
|
+
- **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.
|
|
153
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,17 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
**面向模型的文件系统工具**(`read`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会为文件系统变更提供升权路径。
|
|
5
|
+
**面向模型的文件系统工具**(`read`、`read_image`、`write`、`edit`)及其**执行器**。这是文件系统栈的消费方层:拥有工具名称、JSON Schema、参数校验、提示词段、**读取窗口逻辑**和结果格式化。它**直接**通过 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))读取/写入/编辑。新鲜度/观察策略由独立插件([`@deepseek-ai/dsh-fs-policy`](../fs-policy))通过 `fs/*` 事件门禁贡献;工具不与其方法耦合。使用施加沙箱限制的提供方时,逐会话执行需要共享沙箱策略服务,工具还会为文件系统变更提供升权路径。
|
|
6
6
|
|
|
7
7
|
```ts ignore-check
|
|
8
8
|
// Default deployment: a ctx.fs provider, the policy plugin, then the tools.
|
|
9
9
|
await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) // @deepseek-ai/dsh-fs-local
|
|
10
10
|
await ctx.plugin(FsPolicy) // @deepseek-ai/dsh-fs-policy (policy gate)
|
|
11
|
-
await ctx.plugin(
|
|
11
|
+
await ctx.plugin(LocalAttachmentStore, { dshHome }) // optional — enables durable read_image results
|
|
12
|
+
await ctx.plugin(ToolFs) // this package — read/write/edit, plus read_image with attachments
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
`@deepseek-ai/dsh-fs-policy` 是**可选的**:省略时,工具直接使用裸提供方(无条件写入/覆盖/编辑,无已观察状态)。加载这些工具的部署也应加载该插件,从而提供写入/编辑前读取行为。
|
|
15
16
|
|
|
17
|
+
`read_image` 只在持久 `ctx.attachments` 服务已挂载时注册:没有它,部署无法持久提交图像字节,工具就不会出现。执行时还要求确切路由的模型声明 `image` 输入(通过 `ctx.llm.resolveModelInfo` 从会话最新请求 header 解析,缺失时回退到 agent 选项);未知或纯文本路由在任何文件系统 I/O 之前就得到拒绝结果,因此文本路由的持久历史不会出现图像块。
|
|
18
|
+
|
|
16
19
|
## 配置
|
|
17
20
|
|
|
18
21
|
所有键均为可选;默认值是随产品交付的读取上限。
|
|
@@ -29,18 +32,20 @@ await ctx.plugin(ToolFs) // this package — re
|
|
|
29
32
|
| 工具 | 参数 | 行为 |
|
|
30
33
|
|---|---|---|
|
|
31
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` 持久保存,并在小型元数据信封旁返回图像块。只有确切路由的模型声明图像输入时才会成功。 |
|
|
32
36
|
| `write` | `file_path`、`content` | 创建文件或完整替换文件。有策略插件时:覆盖现有文件要求先在未变版本上执行 `read`;创建新文件不需要。没有插件时:无条件执行。 |
|
|
33
37
|
| `edit` | `file_path`、非空 `old_string`、`new_string`、`replace_all?` | 字面量替换;除非 `replace_all` 为 true,否则要求唯一匹配。有策略插件时:要求先执行 `read`(任何窗口),且文件此后未变。没有插件时:无条件执行。 |
|
|
34
38
|
|
|
35
39
|
字段名使用 snake_case,与 Claude Code 和现有 harness 工具 schema 一致。
|
|
36
40
|
|
|
37
|
-
规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`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? } }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。原生渲染器会保留下方带行号的读取结果和变更确认。`write`/`edit` 从这些规范值派生可回放的 diff 卡片元数据,`read` 派生可回放的读取卡片窗口 `{ path, offset, lines, totalLines, lang? }`;规范值本身仅限于本次执行,不会添加到 `tool/result`,只有派生出的呈现元数据会被持久化。
|
|
38
42
|
|
|
39
43
|
## 工具就是执行器;策略是事件门禁
|
|
40
44
|
|
|
41
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))。随后执行:
|
|
42
46
|
|
|
43
47
|
- **read**:一次 `ctx.fs.stat`(用于类型、大小路由和版本),随后调用 `readText`/`streamText`,构建行窗口,再发出 `fs/observed`,使用普通 `ctx.emit`。(1 次 stat。)
|
|
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。)
|
|
44
49
|
- **write**:调用 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.writeText(target, content, intent)`,再发出 `fs/observed`。(0 次 stat。)
|
|
45
50
|
- **edit**:调用 `ctx.waterfall('fs/edit-intent', target, exec, () => undefined)` 取得可选防护,然后调用 `ctx.fs.editText(target, edit, intent)`,再发出 `fs/observed`。(0 次 stat。)
|
|
46
51
|
|
|
@@ -50,11 +55,11 @@ await ctx.plugin(ToolFs) // this package — re
|
|
|
50
55
|
|
|
51
56
|
## `fs/observed` 发后即忘
|
|
52
57
|
|
|
53
|
-
`fs/observed`
|
|
58
|
+
`fs/observed` 在 read/read_image/write/edit 已经成功之后,通过普通 `ctx.emit` 发出。监听器的约定是同步且只有副作用的记录器(`@deepseek-ai/dsh-fs-policy` 使用 `WeakMap.set`);工具不保护这次发出,因此监听器抛出会作为工具的 `isError` 结果出现。异步或可能失败的观察不属于该事件。
|
|
54
59
|
|
|
55
60
|
`read` 允许并发调度,因为其唯一变更是同步版本记录器。稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此记录器竞态会以拒绝方式关闭;两个变更工具仍保持互斥。见[并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
|
|
56
61
|
|
|
57
|
-
包根目录只导出 Cordis 插件约定(`name`、`inject`、`Config` 和 `apply`)。读取渲染(行窗口与输出格式化)位于 `src/read-render.ts`(不依赖 Cordis,单独进行单元测试);`src/read.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。
|
|
62
|
+
包根目录只导出 Cordis 插件约定(`name`、`inject`、`Config` 和 `apply`)。读取渲染(行窗口与输出格式化)位于 `src/read-render.ts`(不依赖 Cordis,单独进行单元测试);`src/read.ts`/`read-image.ts`/`write.ts`/`edit.ts` 是工具执行器,`src/index.ts` 负责组合。
|
|
58
63
|
|
|
59
64
|
## 模型体验
|
|
60
65
|
|
|
@@ -94,7 +99,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
|
|
94
99
|
|
|
95
100
|
#### 模型看到的内容
|
|
96
101
|
|
|
97
|
-
模型会看到已生成的 [`read`、`write` 和 `edit` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs),参数使用 snake_case
|
|
102
|
+
模型会看到已生成的 [`read`、`read_image`、`write` 和 `edit` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs),参数使用 snake_case。`read_image` 只在持久附件存储已挂载时出现;schema 本身与路由无关,严格门禁在执行时拒绝。作用域工具限制可以为某个 agent 移除任一定义。
|
|
98
103
|
|
|
99
104
|
#### Token 影响
|
|
100
105
|
|
|
@@ -118,6 +123,20 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
|
|
118
123
|
|
|
119
124
|
仅追加;新增可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
120
125
|
|
|
126
|
+
### 图像读取结果
|
|
127
|
+
|
|
128
|
+
#### 模型看到的内容
|
|
129
|
+
|
|
130
|
+
成功的 `read_image` 返回 `<path><displayPath></path>`、`<type>image</type>` 和写明媒体类型、尺寸与字节数的 `<content>` 信封,随后是作为原生图像块的图像本身。会话日志只存储持久的 `sha256:` 附件引用;路由到的提供方在每次请求时重新读取并校验字节摘要。
|
|
131
|
+
|
|
132
|
+
#### Token 影响
|
|
133
|
+
|
|
134
|
+
图像在之后每次请求中都会计费,直到压缩。每次调用都独立受附件存储的 `maxImageBytes`/`maxImagePixels` 约束;重复成功调用会在历史中累积,内容寻址只去重存储的字节,不去重每次请求的 token 成本。
|
|
135
|
+
|
|
136
|
+
#### KV Cache 影响
|
|
137
|
+
|
|
138
|
+
仅追加;新可见内容跟在可复用请求前缀之后,不会使既有 KV 缓存条目失效。
|
|
139
|
+
|
|
121
140
|
### 写入与编辑结果
|
|
122
141
|
|
|
123
142
|
#### 模型看到的内容
|
|
@@ -136,7 +155,7 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
|
|
136
155
|
|
|
137
156
|
#### 模型看到的内容
|
|
138
157
|
|
|
139
|
-
失败会规范化为 `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
|
|
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 则使用带防护的创建。
|
|
140
159
|
|
|
141
160
|
#### Token 影响
|
|
142
161
|
|
|
@@ -149,5 +168,8 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
|
|
149
168
|
## 已知限制与暂缓事项
|
|
150
169
|
|
|
151
170
|
- **未交付面向模型的目录列表工具**:`ctx.fs.listDir` 服务于 skill(技能)发现等提供方代码,同级 [`dsh-tool-fs-search`](../tool-fs-search/) 包则提供基于 ripgrep 的 `glob` 与 `grep`,而不是扩展文件系统 seam。
|
|
152
|
-
- **`read` 只处理 UTF-8
|
|
171
|
+
- **`read` 只处理 UTF-8 文本文件**:图像使用独立的、按扩展名路由的 `read_image` 工具;PDF、音频和视频仍延期处理。目录目标为 `FS_NOT_REGULAR_FILE`。
|
|
172
|
+
- **路由门禁与并发模型切换存在竞态**:`read_image` 在执行时检查最新路由的模型;在该检查与下一次请求之间提交的切换,可能让图像块落在拒绝图像内容的路由上。Web 宿主已拒绝把含图像的会话切到纯文本模型;其他前端拥有各自的等价防护。
|
|
173
|
+
- **媒体类型按扩展名声明**:扩展名选择声明类型,附件存储的魔数校验保持权威;扩展名错误但格式正确的图像会得到改名修复提示,而不是被嗅探接受。
|
|
174
|
+
- **工具结果卡片没有内嵌图像预览**:UI 表面以通用形式渲染图像结果(持久引用而非像素);内嵌渲染延后到 UI 包处理。
|
|
153
175
|
- **没有超时接口**:`read`/`write`/`edit` 不接受超时参数,也不声明 `timeout-policy` 预算;取消只通过 `exec.signal` 传递(见[提供方理由](../README.md#no-timeouts-on-file-io))。
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,9 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
3
3
|
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
4
4
|
import { ESCALATION_TARGETS, approveEscalation, canonicalPath, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@deepseek-ai/dsh-sandbox";
|
|
5
5
|
import { structuredPatch } from "diff";
|
|
6
|
+
import { basename, extname } from "node:path";
|
|
7
|
+
import { AttachmentError, AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
8
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
9
|
//#region lib/types/read-render.js
|
|
7
10
|
/**
|
|
8
11
|
* Pure read presentation: turn provider-decoded text into a bounded, line-numbered window and
|
|
@@ -256,6 +259,32 @@ function sessionResolveOptions(exec, requestedPath, policyWorkspaceRoot) {
|
|
|
256
259
|
};
|
|
257
260
|
}
|
|
258
261
|
//#endregion
|
|
262
|
+
//#region lib/types/read-target.js
|
|
263
|
+
/**
|
|
264
|
+
* Shared path resolution and regular-file validation for model-facing read tools.
|
|
265
|
+
* @module @deepseek-ai/dsh-tool-fs/src/read-target
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* Resolve a model-supplied path, observe absence, and require a regular file.
|
|
269
|
+
* @param ctx - the plugin context providing filesystem resolution and observation events.
|
|
270
|
+
* @param exec - the current tool execution, including session cwd and cancellation.
|
|
271
|
+
* @param requestedPath - the raw path supplied to the tool.
|
|
272
|
+
* @returns the resolved target and its single stat result.
|
|
273
|
+
*/
|
|
274
|
+
async function resolveRegularReadTarget(ctx, exec, requestedPath) {
|
|
275
|
+
const target = await ctx.fs.resolve(requestedPath, sessionResolveOptions(exec, requestedPath));
|
|
276
|
+
const info = await ctx.fs.stat(target, exec.signal);
|
|
277
|
+
if (info === void 0) {
|
|
278
|
+
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
279
|
+
throw new FsError(`cannot read "${target.displayPath}": not found`, "FS_NOT_FOUND");
|
|
280
|
+
}
|
|
281
|
+
if (info.type !== "file") throw new FsError(`cannot read "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
|
|
282
|
+
return {
|
|
283
|
+
target,
|
|
284
|
+
info
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
259
288
|
//#region lib/types/read.js
|
|
260
289
|
/**
|
|
261
290
|
* Model-facing UTF-8 read. It performs one provider stat for type, routing, and observed version,
|
|
@@ -387,13 +416,7 @@ function applyReadTool(ctx, caps) {
|
|
|
387
416
|
isConcurrencySafe: () => true,
|
|
388
417
|
async execute(args, exec) {
|
|
389
418
|
const input = parseReadArgs(args, caps.limit);
|
|
390
|
-
const target = await ctx
|
|
391
|
-
const info = await ctx.fs.stat(target, exec.signal);
|
|
392
|
-
if (!info) {
|
|
393
|
-
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
394
|
-
throw new FsError(`cannot read "${target.displayPath}": not found`, "FS_NOT_FOUND");
|
|
395
|
-
}
|
|
396
|
-
if (info.type !== "file") throw new FsError(`cannot read "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
|
|
419
|
+
const { target, info } = await resolveRegularReadTarget(ctx, exec, input.filePath);
|
|
397
420
|
const window = await buildWindow(info.size === void 0 || info.size >= caps.streamMinSize ? await ctx.fs.streamText(target, exec.signal) : [await ctx.fs.readText(target, exec.signal)], {
|
|
398
421
|
offset: input.offset,
|
|
399
422
|
limit: input.limit,
|
|
@@ -570,7 +593,7 @@ ${outcome.operation === "create" ? "Created" : "Updated"} file
|
|
|
570
593
|
/**
|
|
571
594
|
* Register the `write` tool and its system-prompt guidance.
|
|
572
595
|
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
|
|
573
|
-
* @param sandbox - the shared sandbox-escalation
|
|
596
|
+
* @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
|
|
574
597
|
*/
|
|
575
598
|
function applyWriteTool(ctx, sandbox) {
|
|
576
599
|
ctx.systemPrompt.section({
|
|
@@ -715,7 +738,7 @@ function formatEditOutput(displayPath, replaceAll) {
|
|
|
715
738
|
/**
|
|
716
739
|
* Register the `edit` tool and its system-prompt guidance.
|
|
717
740
|
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
|
|
718
|
-
* @param sandbox - the shared sandbox-escalation
|
|
741
|
+
* @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
|
|
719
742
|
*/
|
|
720
743
|
function applyEditTool(ctx, sandbox) {
|
|
721
744
|
ctx.systemPrompt.section({
|
|
@@ -827,9 +850,222 @@ function applyEditTool(ctx, sandbox) {
|
|
|
827
850
|
}));
|
|
828
851
|
}
|
|
829
852
|
//#endregion
|
|
853
|
+
//#region lib/types/read-image.js
|
|
854
|
+
/**
|
|
855
|
+
* The model-facing `read_image` tool: reads a PNG/JPEG/WebP/GIF file, durably
|
|
856
|
+
* commits its bytes through the attachment service (the same lifecycle as a
|
|
857
|
+
* user-uploaded image), and returns an image block so the image enters model
|
|
858
|
+
* context from the next request onward.
|
|
859
|
+
*
|
|
860
|
+
* The route gate is deliberately stricter than the host upload preflight: a
|
|
861
|
+
* tool result enters durable session history, so emitting an image on a route
|
|
862
|
+
* that cannot carry it would break that route's continuation. Unknown
|
|
863
|
+
* capability therefore refuses instead of relying on the adapter guard.
|
|
864
|
+
* @module @deepseek-ai/dsh-tool-fs/src/read-image
|
|
865
|
+
*/
|
|
866
|
+
/** Extensions `read_image` accepts; magic-byte validation at the attachment service stays authoritative. */
|
|
867
|
+
const IMAGE_EXTENSIONS = {
|
|
868
|
+
".png": "image/png",
|
|
869
|
+
".jpg": "image/jpeg",
|
|
870
|
+
".jpeg": "image/jpeg",
|
|
871
|
+
".webp": "image/webp",
|
|
872
|
+
".gif": "image/gif"
|
|
873
|
+
};
|
|
874
|
+
/**
|
|
875
|
+
* Map a model-supplied path to its declared image media type by extension.
|
|
876
|
+
* @param filePath - the raw `file_path` argument (not yet resolved).
|
|
877
|
+
* @returns the declared media type, or undefined when the path does not claim an image.
|
|
878
|
+
*/
|
|
879
|
+
function imageMediaTypeForPath(filePath) {
|
|
880
|
+
return IMAGE_EXTENSIONS[extname(filePath).toLowerCase()];
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Enforce the strict image-capability gate for the calling route. Resolves the
|
|
884
|
+
* session's latest routed provider/model (request header config, then agent
|
|
885
|
+
* options) and requires the exact resolved route to declare `image` input explicitly.
|
|
886
|
+
* @param ctx - the plugin context used to resolve the optional `llm` service.
|
|
887
|
+
* @param exec - the tool-execution context supplying the calling agent.
|
|
888
|
+
* @param requestedPath - the raw, not-yet-resolved path rendered in refusal messages.
|
|
889
|
+
*/
|
|
890
|
+
async function assertImageCapableRoute(ctx, exec, requestedPath) {
|
|
891
|
+
const routed = exec.agent?.session.requestHeader()?.config;
|
|
892
|
+
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
893
|
+
const model = routed?.model ?? exec.agent?.options.model;
|
|
894
|
+
const llm = ctx.get("llm");
|
|
895
|
+
if (provider === void 0 || model === void 0 || llm === void 0) throw new Error(`cannot read "${requestedPath}" as an image: the current model route could not be resolved`);
|
|
896
|
+
const active = await llm.resolveModelInfo(provider, model, exec.signal);
|
|
897
|
+
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`);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Re-brand a canonical image outcome into the durable attachment reference an
|
|
901
|
+
* `ImageBlock` carries.
|
|
902
|
+
* @param image - the canonical image metadata from the output schema.
|
|
903
|
+
* @returns the branded attachment reference.
|
|
904
|
+
*/
|
|
905
|
+
function imageRefFromValue(image) {
|
|
906
|
+
return {
|
|
907
|
+
attachmentId: AttachmentId(image.attachmentId),
|
|
908
|
+
mediaType: image.mediaType,
|
|
909
|
+
bytes: image.bytes,
|
|
910
|
+
width: image.width,
|
|
911
|
+
height: image.height,
|
|
912
|
+
...image.name === void 0 ? {} : { name: image.name }
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Format an image read as the model-facing envelope beside its image block.
|
|
917
|
+
* @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
|
|
918
|
+
* @param image - the canonical image metadata to summarize.
|
|
919
|
+
* @returns the model-facing envelope; the image itself rides the adjacent image block.
|
|
920
|
+
*/
|
|
921
|
+
function formatImageReadOutput(displayPath, image) {
|
|
922
|
+
return `<path>${displayPath}</path>
|
|
923
|
+
<type>image</type>
|
|
924
|
+
<content>
|
|
925
|
+
${image.mediaType} image, ${image.width}x${image.height} px, ${image.bytes} bytes
|
|
926
|
+
</content>`;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Project one canonical image read into its model-facing envelope and image.
|
|
930
|
+
* @param value - the canonical image-read outcome.
|
|
931
|
+
* @returns the two content blocks used by native and nested dispatches.
|
|
932
|
+
*/
|
|
933
|
+
function imageReadContent(value) {
|
|
934
|
+
return [{
|
|
935
|
+
type: "text",
|
|
936
|
+
text: formatImageReadOutput(value.path, value.image)
|
|
937
|
+
}, {
|
|
938
|
+
type: "image",
|
|
939
|
+
attachment: imageRefFromValue(value.image)
|
|
940
|
+
}];
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Register the `read_image` tool into the given context. The composing plugin
|
|
944
|
+
* owns the attachments gate: `src/index.ts` calls this inside
|
|
945
|
+
* `ctx.inject(['attachments'], …)` so the tool exists only while a durable
|
|
946
|
+
* store is mounted. Execution still re-checks `ctx.get('attachments')` for
|
|
947
|
+
* direct callers and gates on the calling route's declared image input.
|
|
948
|
+
* @param ctx - the registration scope; execution uses its `fs` service plus
|
|
949
|
+
* the optional `attachments`/`llm` services.
|
|
950
|
+
*/
|
|
951
|
+
function applyReadImageTool(ctx) {
|
|
952
|
+
ctx.tools.register(defineTool({
|
|
953
|
+
name: "read_image",
|
|
954
|
+
description: "Read a PNG/JPEG/WebP/GIF file and return the image itself. Requires the current model to accept image input.",
|
|
955
|
+
parameters: { file_path: {
|
|
956
|
+
type: "string",
|
|
957
|
+
required: true,
|
|
958
|
+
description: "Path to the image file, resolved by the filesystem backend."
|
|
959
|
+
} },
|
|
960
|
+
output: {
|
|
961
|
+
schema: {
|
|
962
|
+
type: "object",
|
|
963
|
+
additionalProperties: false,
|
|
964
|
+
properties: {
|
|
965
|
+
path: {
|
|
966
|
+
type: "string",
|
|
967
|
+
required: true
|
|
968
|
+
},
|
|
969
|
+
image: {
|
|
970
|
+
type: "object",
|
|
971
|
+
additionalProperties: false,
|
|
972
|
+
required: true,
|
|
973
|
+
properties: {
|
|
974
|
+
attachmentId: {
|
|
975
|
+
type: "string",
|
|
976
|
+
required: true
|
|
977
|
+
},
|
|
978
|
+
mediaType: {
|
|
979
|
+
type: "string",
|
|
980
|
+
enum: [
|
|
981
|
+
"image/png",
|
|
982
|
+
"image/jpeg",
|
|
983
|
+
"image/webp",
|
|
984
|
+
"image/gif"
|
|
985
|
+
],
|
|
986
|
+
required: true
|
|
987
|
+
},
|
|
988
|
+
bytes: {
|
|
989
|
+
type: "integer",
|
|
990
|
+
required: true
|
|
991
|
+
},
|
|
992
|
+
width: {
|
|
993
|
+
type: "integer",
|
|
994
|
+
required: true
|
|
995
|
+
},
|
|
996
|
+
height: {
|
|
997
|
+
type: "integer",
|
|
998
|
+
required: true
|
|
999
|
+
},
|
|
1000
|
+
name: { type: "string" }
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
render: (_args, value) => imageReadContent(value)
|
|
1006
|
+
},
|
|
1007
|
+
isConcurrencySafe: () => true,
|
|
1008
|
+
async execute(args, exec) {
|
|
1009
|
+
if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
|
|
1010
|
+
const mediaType = imageMediaTypeForPath(args.file_path);
|
|
1011
|
+
if (mediaType === void 0) throw new Error(`cannot read "${args.file_path}": read_image only accepts PNG/JPEG/WebP/GIF paths`);
|
|
1012
|
+
const attachments = ctx.get("attachments");
|
|
1013
|
+
if (attachments === void 0) throw new Error(`cannot read "${args.file_path}" as an image: no attachment service is mounted`);
|
|
1014
|
+
if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`cannot read "${args.file_path}": ${mediaType} images are not accepted by this deployment`);
|
|
1015
|
+
await assertImageCapableRoute(ctx, exec, args.file_path);
|
|
1016
|
+
const { target, info } = await resolveRegularReadTarget(ctx, exec, args.file_path);
|
|
1017
|
+
const byteCap = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
|
|
1018
|
+
const data = await ctx.fs.readBytes(target, exec.signal, byteCap);
|
|
1019
|
+
let ref;
|
|
1020
|
+
try {
|
|
1021
|
+
ref = await attachments.saveImage({
|
|
1022
|
+
data,
|
|
1023
|
+
mediaType,
|
|
1024
|
+
name: basename(target.displayPath)
|
|
1025
|
+
});
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (!(error instanceof AttachmentError) || error.code !== "IMAGE_TYPE_MISMATCH") throw error;
|
|
1028
|
+
const extension = extname(target.displayPath).toLowerCase();
|
|
1029
|
+
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 });
|
|
1030
|
+
}
|
|
1031
|
+
ctx.emit("fs/observed", target, {
|
|
1032
|
+
kind: "present",
|
|
1033
|
+
version: info.version
|
|
1034
|
+
}, exec);
|
|
1035
|
+
const value = {
|
|
1036
|
+
path: target.displayPath,
|
|
1037
|
+
image: {
|
|
1038
|
+
attachmentId: ref.attachmentId,
|
|
1039
|
+
mediaType: ref.mediaType,
|
|
1040
|
+
bytes: ref.bytes,
|
|
1041
|
+
width: ref.width,
|
|
1042
|
+
height: ref.height,
|
|
1043
|
+
...ref.name === void 0 ? {} : { name: ref.name }
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
if (exec.parent !== void 0) exec.deferContext(createUserMessage({
|
|
1047
|
+
content: imageReadContent(value),
|
|
1048
|
+
source: {
|
|
1049
|
+
kind: "plugin",
|
|
1050
|
+
plugin: "tool-fs"
|
|
1051
|
+
}
|
|
1052
|
+
}));
|
|
1053
|
+
return value;
|
|
1054
|
+
},
|
|
1055
|
+
presentCall(args) {
|
|
1056
|
+
return {
|
|
1057
|
+
card: "generic",
|
|
1058
|
+
title: `Read image ${args.file_path}`,
|
|
1059
|
+
kind: "read",
|
|
1060
|
+
locations: [{ path: args.file_path }]
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
}));
|
|
1064
|
+
}
|
|
1065
|
+
//#endregion
|
|
830
1066
|
//#region lib/types/sandbox.js
|
|
831
1067
|
/**
|
|
832
|
-
* The sandbox-escalation
|
|
1068
|
+
* The sandbox-escalation API shared by the `write` and `edit` tools: the
|
|
833
1069
|
* per-call policy resolution, the advertised escalation fields, and the denial-marker
|
|
834
1070
|
* mapping — all delegating the vocabulary and the fail-closed approval
|
|
835
1071
|
* sequence to `@deepseek-ai/dsh-sandbox` (the same pieces `@deepseek-ai/dsh-tool-bash`
|
|
@@ -840,11 +1076,11 @@ function applyEditTool(ctx, sandbox) {
|
|
|
840
1076
|
* @module @deepseek-ai/dsh-tool-fs/sandbox
|
|
841
1077
|
*/
|
|
842
1078
|
/**
|
|
843
|
-
* The filesystem escalation
|
|
1079
|
+
* The filesystem escalation API: advertisement gating, per-call policy
|
|
844
1080
|
* resolution, the one-approved wider retry, and denial-marker mapping. A pure
|
|
845
1081
|
* product of `ctx` at plugin apply time.
|
|
846
1082
|
*/
|
|
847
|
-
var
|
|
1083
|
+
var FsSandboxController = class {
|
|
848
1084
|
ctx;
|
|
849
1085
|
/** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */
|
|
850
1086
|
escalationModes;
|
|
@@ -935,7 +1171,7 @@ var FsSandboxSurface = class {
|
|
|
935
1171
|
//#endregion
|
|
936
1172
|
//#region lib/types/index.js
|
|
937
1173
|
/**
|
|
938
|
-
* Model-facing read, write, and edit tools over `ctx.fs`. This package owns schemas, validation,
|
|
1174
|
+
* Model-facing read, read_image, write, and edit tools over `ctx.fs`. This package owns schemas, validation,
|
|
939
1175
|
* read windows, formatting, and observation events, never a concrete provider. An optional
|
|
940
1176
|
* event policy supplies mutation guards; without one the tools use unconditional provider calls.
|
|
941
1177
|
* @module @deepseek-ai/dsh-tool-fs
|
|
@@ -958,7 +1194,7 @@ const Config = z.object({
|
|
|
958
1194
|
function assertPositiveInteger(name, value) {
|
|
959
1195
|
if (!Number.isInteger(value) || value < 1) throw new Error(`tool-fs: ${name} must be a positive integer`);
|
|
960
1196
|
}
|
|
961
|
-
/** Register the full `read`/`write`/`edit` filesystem tool suite. */
|
|
1197
|
+
/** Register the full `read`/`write`/`edit` filesystem tool suite, plus `read_image` while `attachments` is mounted. */
|
|
962
1198
|
function apply(ctx, config) {
|
|
963
1199
|
const resolved = config;
|
|
964
1200
|
assertPositiveInteger("readLimit", resolved.readLimit);
|
|
@@ -971,7 +1207,10 @@ function apply(ctx, config) {
|
|
|
971
1207
|
maxBytes: resolved.readMaxBytes,
|
|
972
1208
|
streamMinSize: resolved.readStreamMinSize
|
|
973
1209
|
});
|
|
974
|
-
|
|
1210
|
+
ctx.inject(["attachments"], (imageCtx) => {
|
|
1211
|
+
applyReadImageTool(imageCtx);
|
|
1212
|
+
});
|
|
1213
|
+
const sandbox = new FsSandboxController(ctx);
|
|
975
1214
|
applyWriteTool(ctx, sandbox);
|
|
976
1215
|
applyEditTool(ctx, sandbox);
|
|
977
1216
|
}
|
package/lib/types/edit.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module @deepseek-ai/dsh-tool-fs/src/edit
|
|
6
6
|
*/
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
-
import type {
|
|
8
|
+
import type { FsSandboxController } from './sandbox.ts';
|
|
9
9
|
/** Validated `edit` arguments after defaulting. */
|
|
10
10
|
interface EditInput {
|
|
11
11
|
filePath: string;
|
|
@@ -36,8 +36,8 @@ export declare function formatEditOutput(displayPath: string, replaceAll: boolea
|
|
|
36
36
|
/**
|
|
37
37
|
* Register the `edit` tool and its system-prompt guidance.
|
|
38
38
|
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
|
|
39
|
-
* @param sandbox - the shared sandbox-escalation
|
|
39
|
+
* @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
|
|
40
40
|
*/
|
|
41
|
-
export declare function applyEditTool(ctx: Context, sandbox:
|
|
41
|
+
export declare function applyEditTool(ctx: Context, sandbox: FsSandboxController): void;
|
|
42
42
|
export {};
|
|
43
43
|
//# sourceMappingURL=edit.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model-facing read, write, and edit tools over `ctx.fs`. This package owns schemas, validation,
|
|
2
|
+
* Model-facing read, read_image, write, and edit tools over `ctx.fs`. This package owns schemas, validation,
|
|
3
3
|
* read windows, formatting, and observation events, never a concrete provider. An optional
|
|
4
4
|
* event policy supplies mutation guards; without one the tools use unconditional provider calls.
|
|
5
5
|
* @module @deepseek-ai/dsh-tool-fs
|
|
@@ -22,6 +22,6 @@ export interface Config {
|
|
|
22
22
|
readStreamMinSize?: number;
|
|
23
23
|
}
|
|
24
24
|
export declare const Config: z<Config>;
|
|
25
|
-
/** Register the full `read`/`write`/`edit` filesystem tool suite. */
|
|
25
|
+
/** Register the full `read`/`write`/`edit` filesystem tool suite, plus `read_image` while `attachments` is mounted. */
|
|
26
26
|
export declare function apply(ctx: Context, config: Config): void;
|
|
27
27
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
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.
|
|
6
|
+
*
|
|
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.
|
|
11
|
+
* @module @deepseek-ai/dsh-tool-fs/src/read-image
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
15
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
16
|
+
/** The canonical outcome declared by the `read_image` output schema. */
|
|
17
|
+
export interface ImageReadValue {
|
|
18
|
+
path: string;
|
|
19
|
+
image: {
|
|
20
|
+
attachmentId: string;
|
|
21
|
+
mediaType: ImageMediaType;
|
|
22
|
+
bytes: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
name?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Map a model-supplied path to its declared image media type by extension.
|
|
30
|
+
* @param filePath - the raw `file_path` argument (not yet resolved).
|
|
31
|
+
* @returns the declared media type, or undefined when the path does not claim an image.
|
|
32
|
+
*/
|
|
33
|
+
export declare function imageMediaTypeForPath(filePath: string): ImageMediaType | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Enforce the strict image-capability gate for the calling route. Resolves the
|
|
36
|
+
* session's latest routed provider/model (request header config, then agent
|
|
37
|
+
* options) and requires the exact resolved route to declare `image` input explicitly.
|
|
38
|
+
* @param ctx - the plugin context used to resolve the optional `llm` service.
|
|
39
|
+
* @param exec - the tool-execution context supplying the calling agent.
|
|
40
|
+
* @param requestedPath - the raw, not-yet-resolved path rendered in refusal messages.
|
|
41
|
+
*/
|
|
42
|
+
export declare function assertImageCapableRoute(ctx: Context, exec: ToolExecution, requestedPath: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Re-brand a canonical image outcome into the durable attachment reference an
|
|
45
|
+
* `ImageBlock` carries.
|
|
46
|
+
* @param image - the canonical image metadata from the output schema.
|
|
47
|
+
* @returns the branded attachment reference.
|
|
48
|
+
*/
|
|
49
|
+
export declare function imageRefFromValue(image: ImageReadValue['image']): ImageAttachmentRef;
|
|
50
|
+
/**
|
|
51
|
+
* Format an image read as the model-facing envelope beside its image block.
|
|
52
|
+
* @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
|
|
53
|
+
* @param image - the canonical image metadata to summarize.
|
|
54
|
+
* @returns the model-facing envelope; the image itself rides the adjacent image block.
|
|
55
|
+
*/
|
|
56
|
+
export declare function formatImageReadOutput(displayPath: string, image: ImageReadValue['image']): string;
|
|
57
|
+
/**
|
|
58
|
+
* Register the `read_image` tool into the given context. The composing plugin
|
|
59
|
+
* owns the attachments gate: `src/index.ts` calls this inside
|
|
60
|
+
* `ctx.inject(['attachments'], …)` so the tool exists only while a durable
|
|
61
|
+
* store is mounted. Execution still re-checks `ctx.get('attachments')` for
|
|
62
|
+
* direct callers and gates on the calling route's declared image input.
|
|
63
|
+
* @param ctx - the registration scope; execution uses its `fs` service plus
|
|
64
|
+
* the optional `attachments`/`llm` services.
|
|
65
|
+
*/
|
|
66
|
+
export declare function applyReadImageTool(ctx: Context): void;
|
|
67
|
+
//# sourceMappingURL=read-image.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared path resolution and regular-file validation for model-facing read tools.
|
|
3
|
+
* @module @deepseek-ai/dsh-tool-fs/src/read-target
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import type { FsInfo, FsTarget } from '@deepseek-ai/dsh-fs';
|
|
7
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a model-supplied path, observe absence, and require a regular file.
|
|
10
|
+
* @param ctx - the plugin context providing filesystem resolution and observation events.
|
|
11
|
+
* @param exec - the current tool execution, including session cwd and cancellation.
|
|
12
|
+
* @param requestedPath - the raw path supplied to the tool.
|
|
13
|
+
* @returns the resolved target and its single stat result.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveRegularReadTarget(ctx: Context, exec: ToolExecution, requestedPath: string): Promise<{
|
|
16
|
+
target: FsTarget;
|
|
17
|
+
info: FsInfo;
|
|
18
|
+
}>;
|
|
19
|
+
//# sourceMappingURL=read-target.d.ts.map
|
package/lib/types/sandbox.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The sandbox-escalation
|
|
2
|
+
* The sandbox-escalation API shared by the `write` and `edit` tools: the
|
|
3
3
|
* per-call policy resolution, the advertised escalation fields, and the denial-marker
|
|
4
4
|
* mapping — all delegating the vocabulary and the fail-closed approval
|
|
5
5
|
* sequence to `@deepseek-ai/dsh-sandbox` (the same pieces `@deepseek-ai/dsh-tool-bash`
|
|
@@ -30,11 +30,11 @@ export interface EscalationSchemaFields {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* The filesystem escalation
|
|
33
|
+
* The filesystem escalation API: advertisement gating, per-call policy
|
|
34
34
|
* resolution, the one-approved wider retry, and denial-marker mapping. A pure
|
|
35
35
|
* product of `ctx` at plugin apply time.
|
|
36
36
|
*/
|
|
37
|
-
export declare class
|
|
37
|
+
export declare class FsSandboxController {
|
|
38
38
|
private readonly ctx;
|
|
39
39
|
/** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */
|
|
40
40
|
readonly escalationModes: readonly SandboxMode[];
|
package/lib/types/write.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import type { FsWriteOutcome } from '@deepseek-ai/dsh-fs';
|
|
9
|
-
import type {
|
|
9
|
+
import type { FsSandboxController } from './sandbox.ts';
|
|
10
10
|
/**
|
|
11
11
|
* Validate value constraints the schema DSL can't express: only a non-blank
|
|
12
12
|
* `file_path` — an empty `content` is legitimate (it writes an empty file).
|
|
@@ -30,7 +30,7 @@ export declare function formatWriteOutput(displayPath: string, outcome: Pick<FsW
|
|
|
30
30
|
/**
|
|
31
31
|
* Register the `write` tool and its system-prompt guidance.
|
|
32
32
|
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
|
|
33
|
-
* @param sandbox - the shared sandbox-escalation
|
|
33
|
+
* @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
|
|
34
34
|
*/
|
|
35
|
-
export declare function applyWriteTool(ctx: Context, sandbox:
|
|
35
|
+
export declare function applyWriteTool(ctx: Context, sandbox: FsSandboxController): void;
|
|
36
36
|
//# sourceMappingURL=write.d.ts.map
|
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.0.1-rc.
|
|
4
|
+
"version": "0.0.1-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "restricted"
|
|
7
7
|
},
|
|
@@ -36,33 +36,35 @@
|
|
|
36
36
|
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.
|
|
44
|
-
"@deepseek-ai/dsh-session": "^0.0.1-rc.
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
39
|
+
"@deepseek-ai/dsh-attachment": "^0.0.1-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-fs": "^0.0.1-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-user-approval": "^0.0.1-rc.2",
|
|
48
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.2"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/dsh-agent": "^0.0.1-rc.
|
|
52
|
-
"@deepseek-ai/dsh-agent-loop": "^0.0.1-rc.
|
|
53
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.0.1-rc.
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-fs
|
|
56
|
-
"@deepseek-ai/dsh-fs-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.0.1-rc.
|
|
60
|
-
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.
|
|
61
|
-
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.
|
|
62
|
-
"@deepseek-ai/dsh-session": "^0.0.1-rc.
|
|
63
|
-
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.
|
|
64
|
-
"@deepseek-ai/dsh-tools": "^0.0.1-rc.
|
|
52
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-agent-loop": "^0.0.1-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.0.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-attachment": "^0.0.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-fs": "^0.0.1-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-fs-local": "^0.0.1-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-fs-policy": "^0.0.1-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.0.1-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-sandbox": "^0.0.1-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-user-approval": "^0.0.1-rc.2",
|
|
65
67
|
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
66
|
-
"@deepseek-ai/dsh-
|
|
68
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.2"
|
|
67
69
|
}
|
|
68
70
|
}
|