diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml
index dc2902d1ca..9c49645780 100644
--- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md
-2026-08-23-client-derived-tool-presentation.md: 5598c1fbc6073a71f63a20274cd5a791b6b5e6b3
-2026-08-23-client-derived-tool-presentation.zh.md: 5a87433377af58b1ca9d378b76393dada4ca4a1e
+2026-08-23-client-derived-tool-presentation.md: a5d395a4afb7b880ed4401de86296b0b2c85b0c5
+2026-08-23-client-derived-tool-presentation.zh.md: 8784e28dcb53fe96ff3ef0a21cfde82d23e457f7
diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md
index 5598c1fbc6..a5d395a4af 100644
--- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md
+++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md
@@ -324,10 +324,7 @@ Standard and persistent providers sharing the same tool name are a special compa
|---|---|
| running `write` | intended added-only diff from `file_path` and `content` |
| running `edit` | intended replacement diff from `file_path`, `old_string`, and `new_string` |
-| running `str_replace_editor create` | intended added-only diff from `path` and `file_text` |
-| running `str_replace_editor str_replace` | intended replacement diff from `path`, `old_str`, and `new_str` |
| successful settled `write`/`edit` | applied contextual hunks from `meta.diffs` |
-| settled `str_replace_editor` | Generic, because the tool defines no result presenter |
| write create or missing/malformed/empty applied metadata | current argument fallback |
| error, malformed arguments, edit with malformed metadata, or Code Dispatch child | Generic |
@@ -375,8 +372,6 @@ The Deliverables Definition observes root `tool/call` and successful `tool/resul
|---|---|---|
| `write` | any successful call | `file_path` |
| `edit` | any successful call | `file_path` |
-| `str_replace_editor` | `create`, `str_replace`, or `insert` | `path` |
-| `str_replace_editor` | `view` | produces no path |
| Other | no current first-party mutation semantics | produces no path |
Failures, interruptions, orphan results, missing paths, and malformed arguments produce no deliverable. Paths retain first-seen deduplication, and results settled after the closing Assistant seq remain excluded.
@@ -532,8 +527,6 @@ This change does not promise to preserve differences expressed only through a Ho
### Deliverables
- Successful write/edit calls produce `file_path`.
-- str_replace_editor create/str_replace/insert calls produce `path`.
-- str_replace_editor view produces no path.
- failure, interruption, malformed input, and orphan results produce no path.
- First-seen deduplication and the closing-seq cutoff remain unchanged.
diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md
index 5a87433377..8784e28dcb 100644
--- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md
@@ -324,10 +324,7 @@ TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复
|---|---|
| running `write` | 从 `file_path` 与 `content` 生成 intended added-only diff |
| running `edit` | 从 `file_path`、`old_string`、`new_string` 生成 intended replacement diff |
-| running `str_replace_editor create` | 从 `path` 与 `file_text` 生成 intended added-only diff |
-| running `str_replace_editor str_replace` | 从 `path`、`old_str` 与 `new_str` 生成 intended replacement diff |
| settled `write`/`edit` success | 从 `meta.diffs` 生成 applied contextual hunks |
-| settled `str_replace_editor` | Generic,因为该工具没有 result presenter |
| write create 或 applied metadata 缺失、畸形、为空 | 当前 args fallback |
| error、畸形 args、edit 的 metadata 畸形、Code Dispatch child | Generic |
@@ -375,8 +372,6 @@ Deliverables Definition 按 callId 观察 root `tool/call` 与成功 `tool/resul
|---|---|---|
| `write` | 任意成功调用 | `file_path` |
| `edit` | 任意成功调用 | `file_path` |
-| `str_replace_editor` | `create`、`str_replace`、`insert` | `path` |
-| `str_replace_editor` | `view` | 不产生 path |
| 其他 | 无当前第一方 mutation 语义 | 不产生 path |
失败、interrupted、orphan result、缺失 path 与畸形 args 不产生 deliverable。同一路径保持 first-seen 去重,closing Assistant seq 之后落定的结果继续排除。
@@ -532,8 +527,6 @@ Host registry 允许不同 scope 为同一 tool name 提供不同定义;Sessio
### Deliverables
- write/edit 成功产生 `file_path`。
-- str_replace_editor create/str_replace/insert 产生 `path`。
-- str_replace_editor view 不产生 path。
- failure、interrupted、malformed 与 orphan 不产生 path。
- first-seen 去重与 closing seq cut 不变。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml
index 2656ce9926..d2d1256907 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml
+++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md
-2026-08-09-filesystem-absence-observation.md: 40338c4806607b262b93222b4a73a34538414e51
-2026-08-09-filesystem-absence-observation.zh.md: 4544424800844e61f839d5af1f92cef3b3655a27
+2026-08-09-filesystem-absence-observation.md: 5f34ec29c85424a9979bfe48bbb71fd0149f676d
+2026-08-09-filesystem-absence-observation.zh.md: 1ea214ddeb72b25331e0a60157b28029e6e346a7
diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md
index 40338c4806..5f34ec29c8 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md
@@ -12,7 +12,7 @@ Treating a failed read as permission to create also exposes a second boundary. B
## Decision
-`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` or the `str_replace_editor` `view`, `str_replace`, or `insert` command emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence.
+`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence.
`dsh-fs-observation-policy` stores three logical states per owner and target without injecting or calling `ctx.fs`: missing map entry is unseen, `absent` is confirmed absence, and `present(version)` is a replacement/edit basis. Write maps unseen and absent to the existing `createIfAbsent` intent and present to `replaceIfVersion`. Edit maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. A successful create or mutation replaces absence with its produced present version.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md
index 4544424800..1ea214ddeb 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.zh.md
@@ -12,7 +12,7 @@ Status: implemented
## 决策
-`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中时,都会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。
+`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中时,会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。
`dsh-fs-observation-policy` 按所有者与目标存储三种逻辑状态,既不注入也不调用 `ctx.fs`:映射中无条目即未见,`absent` 表示确认缺失,`present(version)` 是替换/编辑基准。写入把未见和缺失映射到现有 `createIfAbsent` 意图,把存在映射到 `replaceIfVersion`。编辑把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。成功创建或变更后,系统会用其产生的存在版本取代缺失状态。
diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml
index 9ba413c09f..23fff50840 100644
--- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
-2026-07-29-persistent-bash-str-replace-editor.md: 02ad2bfa8ab8051eff2c408c8dbbca7acca2daaa
-2026-07-29-persistent-bash-str-replace-editor.zh.md: beb2e17d2d342e621e0f1f829910ecf48e35ac80
+2026-07-29-persistent-bash-str-replace-editor.md: a1289d3f2c7c42851ce6eb5d0ec071786e797614
+2026-07-29-persistent-bash-str-replace-editor.zh.md: 49e315fc789f0f031bf09dd96d2de035cf7dd609
diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
index 02ad2bfa8a..a1289d3f2c 100644
--- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
+++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
@@ -14,11 +14,13 @@ Some deployments need a one-call Bash schema whose shell state survives across m
`@deepseek-ai/dsh-tool-str-replace-editor` independently consumes `ctx.fs` and registers `str_replace_editor` with `view`, `create`, `str_replace`, and `insert`. It provides numbered text views, filtered two-level directory listings, unique literal replacement, canonical insertion boundaries, and bounded output. Paths are absolute; file views preserve content tabs so copied text remains valid literal replacement input; mutations preserve tabs outside the requested edit; and the public schema and failures use only `old_str`. Command-specific fields accept `null` placeholders: execution treats them as omitted when the selected command does not use them, preserves required-field checks, treats `view_range: null` as a full view, and rejects `str_replace.new_str: null` so only omission requests deletion. The plugin can compose with persistent Bash, one-shot Bash, sandboxed Bash, or no shell.
+The [complete-removal decision](../simplification/2026-09-03-remove-str-replace-editor.md) later supersedes the preceding editor half and deletes that package. This note retains authority only for the persistent-shell behavior and the historical reason the capabilities were originally separate.
+
`dsh-system-prompt` accepts `includeHarnessIdentity: false`, and profile compositions own that row together with their selected shell-tool rows. `sdk-minimal` sets the value to `false` and mounts only its platform-selected persistent shell, so a deployment can own an exact persona without duplicate prompt or tool registrations. Existing defaults remain unchanged.
-Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable.
+The persistent-shell plugin is included in the Python runtime closure together with the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with a `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch checks `DSH_NODE_PTY_SPAWN_HELPER` first, so it remains a true override for a current external consumer that supplies a non-sibling helper. When the override is unset, the patch resolves the packaged executable sibling if present and otherwise preserves upstream lookup in ordinary Node runs. The macOS builders fail before publication when the helper is absent or not executable.
-The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the editor's absence from minimal compositions; the editor remains independently mountable elsewhere.
+The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml) composes only the persistent-shell plugin for the minimal model contract. Its entry-local PTY realm carries the registry, local backend, and persistent Bash or PowerShell tool. The preset fixes the complete system prompt, follows the deployment tool-presentation mode, omits every other model-facing consumer, and leaves browser, Workspace, persistence, sandbox, and permission services on the shared Web host. The local PTY backend resolves the effective session sandbox mode when it creates the shell. While that owner has an open shell or a spawn in progress, a different permission mode is rejected before its session event commits. The [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) owns the exact minimal composition; the complete-removal decision owns the editor package's absence from the distribution.
## Alternatives considered
@@ -34,4 +36,4 @@ The shipped [`minimal` agent preset](../../../../packages/preset/agent-presets/p
## Consequences
-Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. Nullable branches increase the command-specific fields' schema cost so unused placeholders do not force retries; execution keeps the selected command's required and deletion semantics explicit. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper.
+Profiles can reproduce an external agent by configuring persona and descriptions while the persistent-shell package remains general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. A minimal Web agent retains Web permissions but must close its persistent shell before changing modes. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper. The editor-specific consequences no longer apply after its package removal.
diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md
index beb2e17d2d..49e315fc78 100644
--- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md
@@ -14,11 +14,13 @@ Status: implemented
`@deepseek-ai/dsh-tool-str-replace-editor` 独立消费 `ctx.fs`,注册包含 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`。它提供带行号文本查看、过滤后的两层目录列表、唯一字面量替换、规范插入边界和有界输出。路径必须为绝对路径;文件查看会保留内容中的制表符,因此复制的文本仍可作为有效的字面量替换输入;变更会保留请求编辑范围之外的制表符;公开 schema 与错误则只使用 `old_str`。命令专属字段接受 `null` 占位参数:当前命令不使用该字段时,执行会将其视为未提供;必填检查保持不变;`view_range: null` 表示查看完整文件;`str_replace.new_str: null` 会被拒绝,只有省略该字段才表示删除。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或无 shell 组合。
+后续的[完整移除决策](../simplification/2026-09-03-remove-str-replace-editor.zh.md)取代了前述 editor 部分并删除该包。本 note 只继续负责持久 shell 行为,以及两个能力最初分离的历史理由。
+
`dsh-system-prompt` 接受 `includeHarnessIdentity: false`,profile 组合同时拥有该配置行与所选 shell 工具配置行。`sdk-minimal` 将该值设为 `false`,且只挂载按平台选择的持久 shell,因此部署可以拥有精确 persona,而不会重复注册提示词或工具。既有默认值不变。
-两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。
+持久 shell 插件进入 Python runtime 闭包,并携带 PTY 服务/本地后端以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个 `-spawn-helper` 伴随文件;Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁会先检查 `DSH_NODE_PTY_SPAWN_HELPER`,因此对当前提供非伴随 helper 的外部消费方而言,该变量仍是真正的覆盖项。未设置该覆盖时,补丁会在打包可执行文件的伴随文件存在时解析它,否则在普通 Node 运行中保留上游查找方式。若 helper 缺失或不可执行,macOS 构建器会在发布前失败。
-随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责极简组合不包含 editor 的事实;其他组合仍可独立挂载 editor。
+随附的 [`minimal` agent preset](../../../../packages/preset/agent-presets/presets/minimal/agent.cordis.yml)只为极简模型约定组合持久 shell 插件。其 entry 本地 PTY realm 持有注册表、本地后端和持久 Bash 或 PowerShell 工具。Preset 会固定完整系统提示词、跟随部署的工具呈现模式,省略其他所有面向模型的消费方,并将浏览器、Workspace、持久化、沙箱与权限服务留在共享 Web 宿主上。本地 PTY 后端会在创建 shell 时解析会话的有效沙箱模式。只要该所有者仍有打开的 shell 或仍在进行中的 spawn,另一种权限模式就会在对应的会话事件提交前遭到拒绝。[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)负责精确极简组合;完整移除决策负责发行物不再包含 editor 包。
## 考虑过的替代方案
@@ -34,4 +36,4 @@ Status: implemented
## 后果
-Profile 可以通过配置 persona 和描述复现外部 Agent,而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。可为 `null` 的分支增加了命令专属字段的 schema 成本,使未使用的占位参数不会迫使模型重试;执行仍明确保留当前命令的必填与删除语义。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。
+Profile 可以通过配置 persona 和描述复现外部 Agent,而持久 shell 包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端;shell 退出、超时或取消会丢失状态。minimal Web agent 保留 Web 权限,但必须先关闭持久 shell 才能更改权限模式。运行时 wheel 包的消费方仍无需安装 Node;Linux wheel 包包含一个可执行文件,macOS wheel 包还包含其私有原生 helper。删除 editor 包后,其专属后果不再适用。
diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
index 33e4ac93a8..e8b141e733 100644
--- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
-2026-07-31-even-out-shipped-tool-rosters.md: 4fea5fb30860f3254aec6178068158b4e7756493
-2026-07-31-even-out-shipped-tool-rosters.zh.md: 32c188f6419b9ede74838215848d77f139280cde
+2026-07-31-even-out-shipped-tool-rosters.md: c16b8676f666f648f7e6ed87d24ebc9661cbc48a
+2026-07-31-even-out-shipped-tool-rosters.zh.md: 2784ca5bdc86eb48729134b5fc8bf24fd865cda2
diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
index 4fea5fb308..c16b8676f6 100644
--- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
+++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
@@ -12,11 +12,11 @@ The result was a user-visible difference nobody had decided: the same model, ask
## Decision
-The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, and the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions.
+The rows that are not surface-specific move into [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml), and three more join them: `tool-session-query`, `tool-str-replace-editor`, and `repeat-tool-reminder`. Web search moves there too; its [deployment decision](2026-07-31-web-default-search.md) owns the security boundary while the shared base owns its surface-neutral mount. Both surfaces assemble the same roster, including fixed `glob` and `grep` members because `dsh-tool-fs-search` spawns the [packaged ripgrep binary](../architecture/2026-08-01-packaged-ripgrep-search.md). Later decisions narrow that roster: the [session-search decision](2026-08-02-session-search-not-shipped-default.md) keeps `tool-session-query` opt-in, the [single-editor decision](../simplification/2026-08-10-default-presets-single-editor.md) removes `tool-str-replace-editor` from general-purpose presets, the [persistent-shell-only decision](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.md) removes it from the minimal compositions, and the [complete-removal decision](../simplification/2026-09-03-remove-str-replace-editor.md) later deletes the package and shared-base row recorded here.
Two rows stay surface-specific. `tmux-context` is TUI-only because a browser surface has no terminal multiplexer to describe. `session-reference` is TUI-only because it drives the shared session-query index from the launcher's process-local path, and the browser sidebar reconciles that index on its own first search.
-**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search and single-editor decisions own their respective default-roster exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md).
+**This roster decision added only at the time.** No tool row was removed from either surface when it landed, and a catalog comparison found additions and nothing else. The later session-search, single-editor, and complete-removal decisions own their respective current exceptions. The shared executors, sandbox composition, and access default are owned independently by the [workspace-write default decision](2026-07-31-workspace-write-surface-default.md).
### What stays unmounted, and why
diff --git a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
index 32c188f641..2784ca5bdc 100644
--- a/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.zh.md
@@ -12,11 +12,11 @@ Status: implemented
## 决策
-那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)则从极简组合移除它。
+那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-reminder`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.zh.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单,其中 `glob` 和 `grep` 是固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.zh.md)。后续决策收窄了这份清单:[session-search 决策](2026-08-02-session-search-not-shipped-default.zh.md)让 `tool-session-query` 保持需显式启用,[单一 editor 决策](../simplification/2026-08-10-default-presets-single-editor.zh.md)从通用 preset 移除 `tool-str-replace-editor`,[仅持久 shell 决策](../simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从极简组合移除它,随后[完整移除决策](../simplification/2026-09-03-remove-str-replace-editor.zh.md)删除了本记录中的包与共享 base 行。
有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。
-**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search 与单一编辑器决策分别负责对应的默认清单例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.zh.md)。
+**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。后续的 session-search、单一编辑器与完整移除决策分别负责当前对应的例外。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.zh.md)。
### 什么保持不挂,以及为什么
diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml
index e8023fd069..b12f647e47 100644
--- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md
-2026-07-31-web-workspace-file-links.md: 944d7bef9b83eb34895c5f74e511600a713d786b
-2026-07-31-web-workspace-file-links.zh.md: 71f0051204e0cb8b65995fc88a9edb230db8f5be
+2026-07-31-web-workspace-file-links.md: ce0ca591cbe0ffc17d33e56f7a680912f3d86231
+2026-07-31-web-workspace-file-links.zh.md: e801c2e67a4e9c21f7d39f6644f0bf94fe8f1fa1
diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md
index 944d7bef9b..ce0ca591cb 100644
--- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md
+++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md
@@ -14,7 +14,7 @@ Two distinct defects sat behind that. The transcript never said what a turn had
## Decision
-**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing.
+**A finished turn ends with the files it produced.** The row is its own plugin, `@deepseek-ai/dsh-client-ui-deliverables`, registered into the `conversation.chat.turnTail` hole the chat view renders between a closing message's body and its IconActions — ui-conversation owns the hole and the owner currency (nodes, closing seq, `openFile`), the plugin owns every policy. `producedForClosing` reads the paths off the mutation tools' own follow-along `locations` — a diff card or a generic card whose `kind` is `edit` — so a turn's output is listed whether or not the closing message named it, and a new mutation tool joins by declaring what it does rather than by being added to a list. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row. CSS container-width bands select a prefix of up to six paths and its matching `+ N files` label, while flexbox shrinks and ellipsizes the visible basenames; the component performs no JavaScript layout observation ([decision](../simplification/2026-09-01-css-produced-file-layout.md)). One cordis.yml line composes the surface in or out; the unregistered hole renders nothing.
**The path link reads as a link.** Underlined at rest, not only on hover. This is the smaller half of the diff and the larger half of the fix.
diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md
index 71f0051204..e801c2e67a 100644
--- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md
@@ -14,7 +14,7 @@ Status: implemented
## 决策
-**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片,或 `kind` 为 `edit` 的 generic 卡片(即 `str_replace_editor` 的 insert 所呈现的形状)——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。
+**完成的一轮以它产出的文件收尾。** 该行是独立插件 `@deepseek-ai/dsh-client-ui-deliverables`,注册进 chat 视图在收尾消息正文与其 IconActions 之间渲染的 `conversation.chat.turnTail` 空位——ui-conversation 拥有空位与 owner 通货(节点、收尾 seq、`openFile`),插件拥有全部策略。`producedForClosing` 从改写工具自身的跟随文件 `locations` 中读出路径——diff 卡片或 `kind` 为 `edit` 的 generic 卡片——因此无论收尾消息是否点名,这一轮的产出都会被列出;新的改写工具靠声明自己做了什么加入,而不是靠被加进某张名单。read、删除与失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在 turn 边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。CSS 容器宽度档位会选择前六条路径中的一个前缀及其匹配的 `+ N 个文件` 标签,flexbox 则收缩可见的 basename 并用 ellipsis 省略;组件不执行 JavaScript 布局观察([决策](../simplification/2026-09-01-css-produced-file-layout.zh.md))。cordis.yml 中的一行即可把该交互面组合进来或去掉;未注册的空位什么也不渲染。
**路径链接读得出是链接。** 静止状态下就带下划线,而不只在悬停时。这是本次改动中更小的那一半,却是修复中更大的那一半。
diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml
index 5d39c528e2..858227b5e5 100644
--- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml
+++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
-2026-08-10-default-presets-single-editor.md: ef18f8d7dd2242c8af53af970df0a06b8d7d09d3
-2026-08-10-default-presets-single-editor.zh.md: c9b57e1ab3fa8d40ac587590f374f1f3747b7e7f
+2026-08-10-default-presets-single-editor.md: 02d0ddddacbb045aa704890e01b9d710179f6eac
+2026-08-10-default-presets-single-editor.zh.md: 0a4942d81a4bc58adcdf8c54e09b349cf2954377
diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
index ef18f8d7dd..02d0ddddac 100644
--- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
+++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.md
@@ -10,16 +10,16 @@ The `standard`, `code`, and `cordis` presets exposed both the `read`/`write`/`ed
## Decision
-The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount `dsh-tool-str-replace-editor`. PTC mode therefore omits `str_replace_editor` from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. Deployments and user-authored presets may still mount the plugin explicitly.
+The `standard`, `code`, and `cordis` preset configurations mount `dsh-tool-fs` and `dsh-tool-fs-search`, but do not mount the standalone editor. PTC mode therefore omits that editor schema from both its registry and generated SDK. The later [persistent-shell-only decision](2026-09-03-minimal-profiles-persistent-shell-only.md) removes the editor from the shipped `minimal` and `sdk-minimal` compositions. The [complete-removal decision](2026-09-03-remove-str-replace-editor.md) subsequently deletes the standalone package and its runtime support.
-This decision narrows the preset roster rather than removing the tool package or its Python runtime support. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the editor exception.
+This decision originally narrowed only the preset roster. The later complete-removal decision owns the package and Python runtime deletion. The earlier [shared-roster decision](../feature/2026-07-31-even-out-shipped-tool-rosters.md) continues to own why surface-neutral tools live in preset composition; this note owns the earlier preset exception.
## Alternatives considered
**Keep both editing interfaces in the general-purpose presets.** Rejected because the overlapping model-visible schemas increase tool choice without supplying a separate default operation.
-**Remove the `str_replace_editor` package from the distribution.** Rejected because explicit deployments remain valid consumers of the standalone plugin.
+**Remove the editor package from the distribution.** Rejected at the time because explicit deployments were treated as valid consumers. The later complete-removal decision accepts that compatibility break for v41.
## Consequences
-General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. Preset composition tests pin `str_replace_editor`'s absence from every shipped preset and the PTC mode SDK.
+General-purpose agents use `read`, `write`, and `edit` for filesystem mutations, while minimal agents use their persistent shell. The complete-removal decision now guarantees the obsolete editor cannot be mounted from the distribution.
diff --git a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md
index c9b57e1ab3..0a4942d81a 100644
--- a/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md
+++ b/.agents/notes/implemented/simplification/2026-08-10-default-presets-single-editor.zh.md
@@ -10,16 +10,16 @@ Status: implemented
## 决策
-`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载 `dsh-tool-str-replace-editor`。因此 PTC mode 的注册表和生成的 SDK 均不包含 `str_replace_editor`。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)会从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor。部署配置与用户自定义 preset 仍可显式挂载该插件。
+`standard`、`code` 和 `cordis` preset 配置挂载 `dsh-tool-fs` 与 `dsh-tool-fs-search`,但不挂载独立 editor。因此 PTC mode 的注册表和生成 SDK 均不包含该 editor schema。后续的[仅持久 shell 决策](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)从随附的 `minimal` 与 `sdk-minimal` 组合中移除 editor;再后的[完整移除决策](2026-09-03-remove-str-replace-editor.zh.md)删除独立包及其 runtime 支持。
-此决策收窄 preset 工具清单,不移除工具包及其 Python 运行时支持。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明编辑器例外。
+此决策最初只收窄 preset 工具清单;后续完整移除决策负责删除包与 Python runtime。较早的[共享清单决策](../feature/2026-07-31-even-out-shipped-tool-rosters.zh.md)继续说明与 surface 无关的工具为何归 preset 组合所有;本记录说明早期的编辑器例外。
## 曾考虑的替代方案
**在通用 preset 中保留两套编辑接口。** 不予采用,因为重叠的模型可见 schema 增加了工具选择,却没有提供不同的默认操作。
-**从发行物中删除 `str_replace_editor` 包。** 不予采用,因为显式部署仍是该独立插件的有效消费方。
+**从发行物中删除 editor 包。** 当时不予采用,因为显式部署被视为有效消费方。后续完整移除决策为 v41 接受了这一兼容性破坏。
## 后果
-通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。Preset 组合测试会固定每份随附 preset 及 PTC mode SDK 都不包含 `str_replace_editor`。
+通用 agent 使用 `read`、`write` 与 `edit` 完成文件系统修改,minimal agent 则使用持久 shell。完整移除决策现在保证发行物无法再挂载过时 editor。
diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml
index 077fec32c1..f3fb2579cd 100644
--- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml
+++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
-2026-09-03-minimal-profiles-persistent-shell-only.md: fbbdd63c3bd8928be49e9eb78f76c5c2824637a0
-2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 1a7cc625a2ce279da96b1d765151f17e63ce4201
+2026-09-03-minimal-profiles-persistent-shell-only.md: 8814cd94dc85f3b56d8ab55b8f7c00367ff3d0b6
+2026-09-03-minimal-profiles-persistent-shell-only.zh.md: 9b3ab9a756a9ef38410bbc2b1e9337070ef28583
diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
index fbbdd63c3b..8814cd94dc 100644
--- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
+++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md
@@ -14,20 +14,20 @@ The next-generation model's minimal runtime requires one model-facing tool. Leav
The shipped minimal compositions expose exactly one platform-selected persistent shell: `bash` on Linux and macOS, or `pwsh` on Windows. Neither composition mounts `@deepseek-ai/dsh-tool-str-replace-editor`, a filesystem tool, or the `fs-local` service that supported the editor. The fixed complete persona, absence of runtime context and compaction, shell timeout, and launch-specific host services remain unchanged.
-The standalone editor package and its consumers outside the two minimal compositions remain supported. A trusted user-authored preset or higher profile patch can mount it explicitly; the shipped `minimal` and `sdk-minimal` defaults never insert it.
+The later [complete-removal decision](2026-09-03-remove-str-replace-editor.md) deletes the standalone editor package and all consumers. This note continues to own the exact one-shell composition of the shipped `minimal` and `sdk-minimal` defaults.
-Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that its row and dependency allowlists contain neither `fs-local` nor `dsh-tool-str-replace-editor`. Web and packaged-Python model-visible snapshots pin the one-tool schema roster.
+Exact composition tests assert the single tool and the absence of a preset-local filesystem service. The `sdk-minimal` bundle test and built config dump assert that their row and dependency allowlists contain no unused filesystem provider. Web and packaged-Python model-visible snapshots pin the one-tool schema roster.
-This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for the independent editor package, prompt ownership, no-compaction behavior, profile launch, and bundle layering.
+This decision partially supersedes the minimal exceptions in [one editor family in general-purpose presets](2026-08-10-default-presets-single-editor.md), [the minimal preset composition](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md), [the bare minimal runtime](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md), and [the standalone sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md). Those notes retain authority for prompt ownership, no-compaction behavior, profile launch, and bundle layering; the complete-removal decision owns the package deletion.
## Alternatives considered
**Keep the editor row and hide its schema.** Rejected because a presentation or restriction layer would leave the capability in the minimal composition and make its absence depend on another setting.
-**Remove the editor package from the distribution.** Rejected because full profiles and explicit custom compositions remain valid consumers. The requirement concerns the two shipped minimal defaults.
+**Remove the editor package from the distribution.** Rejected at the time because explicit custom compositions were treated as valid consumers. The later complete-removal decision accepts the v41 compatibility break.
**Keep the editor only in `sdk-minimal`.** Rejected because the two minimal paths would present different tool contracts to the same model class, and the packaged SDK path would retain the schema cost and unused filesystem service.
## Consequences
-Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. Full profiles, the editor package, and explicit custom compositions keep their existing behavior.
+Minimal agents inspect and modify files through their persistent shell. Their model requests carry one tool schema, and their compositions own no filesystem service. General-purpose profiles keep the native filesystem tools; the obsolete editor package no longer ships.
diff --git a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md
index 1a7cc625a2..9b3ab9a756 100644
--- a/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md
+++ b/.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.zh.md
@@ -14,20 +14,20 @@ Status: implemented
随附的极简组合只提供一个按平台选择的持久 shell:Linux 与 macOS 使用 `bash`,Windows 使用 `pwsh`。两份组合都不挂载 `@deepseek-ai/dsh-tool-str-replace-editor`、文件系统工具或支撑 editor 的 `fs-local` 服务。固定的 complete persona、运行时上下文与 compaction 的缺失、shell 超时和各启动路径的宿主服务保持不变。
-独立 editor 包及其在两份极简组合之外的消费方继续受支持。受信任的用户自定义 preset 或更高优先级的 profile patch 可以显式挂载它;随附的 `minimal` 与 `sdk-minimal` 默认组合不会插入它。
+后续的[完整移除决策](2026-09-03-remove-str-replace-editor.zh.md)删除独立 editor 包及其全部消费方。本 note 继续负责随附 `minimal` 与 `sdk-minimal` 默认组合的精确单 shell 组成。
-精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含 `fs-local` 或 `dsh-tool-str-replace-editor`。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。
+精确组合测试会断言单工具清单以及 preset 内不存在文件系统服务。`sdk-minimal` bundle 测试与构建后配置转储会断言配置项和依赖 allowlist 都不含未使用的文件系统 provider。Web 与打包 Python 的模型可见快照会固定单工具 schema 清单。
-该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责独立 editor 包、提示词所有权、无 compaction 行为、profile 启动与 bundle 分层。
+该决策部分取代[通用 preset 使用一种 editor](2026-08-10-default-presets-single-editor.zh.md)、[minimal preset 组合](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)、[极简裸运行时](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)与[独立 sdk-minimal profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)中的极简例外。这些 Agent Note 继续负责提示词所有权、无 compaction 行为、profile 启动与 bundle 分层;完整移除决策负责删除包。
## 考虑过的替代方案
**保留 editor 配置项并隐藏其 schema。** 不予采用,因为呈现层或限制层会让该能力继续留在极简组合中,并使其缺失依赖另一项设置。
-**从发行物中删除 editor 包。** 不予采用,因为完整 profile 与显式自定义组合仍是有效消费方。本需求只涉及两份随附的极简默认组合。
+**从发行物中删除 editor 包。** 当时不予采用,因为显式自定义组合被视为有效消费方。后续完整移除决策接受了 v41 兼容性破坏。
**只在 `sdk-minimal` 中保留 editor。** 不予采用,因为两条极简路径会向同类模型提供不同的工具约定,而且打包 SDK 路径仍会承担 schema 成本和未被其他配置项使用的文件系统服务。
## 后果
-极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。完整 profile、editor 包与显式自定义组合保持现有行为。
+极简 agent 通过持久 shell 检查和修改文件。模型请求只携带一个工具 schema,组合不拥有文件系统服务。通用 profile 保留原生文件系统工具;过时 editor 包不再发布。
diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml
new file mode 100644
index 0000000000..e0a984690a
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md
+2026-09-03-remove-str-replace-editor.md: ac3f173d06af5db28f04839ae0551129ed09c586
+2026-09-03-remove-str-replace-editor.zh.md: 4f6ad91573e734dd463a1fcc5f3b0739ea1607be
diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md
new file mode 100644
index 0000000000..ac3f173d06
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.md
@@ -0,0 +1,33 @@
+# Agent Note: Remove the string-replacement editor
+
+Status: implemented
+
+English | [中文](2026-09-03-remove-str-replace-editor.zh.md)
+
+## Problem
+
+No shipped profile still needs the standalone `str_replace_editor` interface. General-purpose profiles already expose `read`, `write`, and `edit`, while the next-generation minimal contract exposes only its persistent shell. Keeping the dormant package would still ship its implementation, dependency closure, configuration surface, generated catalog entries, client presentation branches, and model-schema snapshots.
+
+A hidden or unmounted tool is not a complete removal. User-authored configuration could still mount it, and future composition changes could accidentally restore the schema.
+
+## Decision
+
+Delete `@deepseek-ai/dsh-tool-str-replace-editor` from the workspace and distribution. Remove its shared-base row, disabled Web override, CLI and Python runtime dependencies, TypeScript project aliases, tool-catalog registration, client-specific diff and produced-file handling, package documentation, tests, and recorded scenario. Regenerated configuration catalogs, module graphs, composition diagrams, and request snapshots must contain no package or model-visible tool entry.
+
+No compatibility alias, hidden registration, or replacement package is retained. General-purpose profiles keep `read`, `write`, `edit`, `glob`, and `grep`; minimal profiles keep their platform-selected persistent shell. Historical unknown tool events can use the existing generic client fallback, but the project does not advertise or test the removed contract.
+
+This decision supersedes the editor half of [the original persistent-tool decision](../feature/2026-07-29-persistent-bash-str-replace-editor.md), [the standalone-package exception](2026-08-10-default-presets-single-editor.md), and [the minimal-profile scope boundary](2026-09-03-minimal-profiles-persistent-shell-only.md). The original note continues to own persistent Bash behavior; the minimal-profile note continues to own the one-shell contract.
+
+## Alternatives considered
+
+**Keep the package for explicit custom compositions.** Rejected because there is no current shipped consumer, and an opt-in package still expands the supported and published surface.
+
+**Leave the registration in place and filter it from model requests.** Rejected because presentation settings could restore the capability and every runtime would continue carrying inactive code.
+
+**Provide an alias or compatibility shim over `read`/`write`/`edit`.** Rejected because the project is pre-release and a shim would preserve the schema and maintenance cost this change removes.
+
+## Consequences
+
+Existing user-authored configurations that name the deleted package fail loudly and must migrate to `read`, `write`, `edit`, or a shell. This is an accepted v41 pre-release compatibility break.
+
+The removed interface can return only through a new decision with a distinct current consumer, explicit composition owner, package and runtime closure, model-visible documentation, and tests. Until then, source, published manifests, generated catalogs, and current snapshots contain no `str_replace_editor` tool.
diff --git a/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md
new file mode 100644
index 0000000000..4f6ad91573
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-09-03-remove-str-replace-editor.zh.md
@@ -0,0 +1,33 @@
+# Agent Note:移除字符串替换编辑器
+
+状态:已实现
+
+[English](2026-09-03-remove-str-replace-editor.md) | 中文
+
+## 问题
+
+现有随附 profile 都不再需要独立的 `str_replace_editor` 接口。通用 profile 已提供 `read`、`write` 与 `edit`,下一代模型的 minimal contract 则只提供持久 shell。继续保留这个闲置包,仍会发布其实现、依赖闭包、配置面、生成目录条目、客户端展示分支和模型 schema 快照。
+
+隐藏或不挂载工具不等于完整移除。用户自定义配置仍可挂载它,未来的组合改动也可能意外恢复该 schema。
+
+## 决策
+
+从 workspace 与发行物中删除 `@deepseek-ai/dsh-tool-str-replace-editor`。移除共享 base 行、已禁用的 Web override、CLI 与 Python runtime 依赖、TypeScript project alias、工具目录注册、客户端专用 diff 与 produced-file 处理、包文档、测试和录制场景。重新生成的配置目录、模块图、组合图和请求快照不得包含该包或面向模型的工具条目。
+
+不保留兼容 alias、隐藏注册或替代包。通用 profile 保留 `read`、`write`、`edit`、`glob` 与 `grep`;minimal profile 保留按平台选择的持久 shell。历史中的未知工具事件可以使用现有客户端 generic fallback,但项目不再宣传或测试已移除的 contract。
+
+本决策取代[原始持久工具决策](../feature/2026-07-29-persistent-bash-str-replace-editor.zh.md)中 editor 的部分、[独立包例外](2026-08-10-default-presets-single-editor.zh.md)以及[极简 profile 范围边界](2026-09-03-minimal-profiles-persistent-shell-only.zh.md)。原始 note 继续负责持久 Bash 行为;极简 profile note 继续负责单 shell contract。
+
+## 考虑过的替代方案
+
+**为显式自定义组合保留该包。** 不予采用,因为目前没有随附消费方,而 opt-in 包仍会扩大受支持与发布的表面。
+
+**保留注册并从模型请求中过滤。** 不予采用,因为展示设置可能恢复该能力,而且每个 runtime 都会继续携带闲置代码。
+
+**在 `read`/`write`/`edit` 上提供 alias 或兼容 shim。** 不予采用,因为项目仍处于发布前阶段,shim 会保留本次改动要删除的 schema 与维护成本。
+
+## 后果
+
+点名已删除包的用户自定义配置会明确失败,必须迁移到 `read`、`write`、`edit` 或 shell。这是 v41 发布前适配中接受的兼容性破坏。
+
+只有新决策给出不同且当前存在的消费方、明确的组合 owner、包与 runtime 闭包、面向模型的文档和测试后,已移除接口才能重新引入。在此之前,源码、发布 manifest、生成目录与当前快照均不包含 `str_replace_editor` 工具。
diff --git a/apps/cli/composition.md b/apps/cli/composition.md
index f0f2dadeab..406edb4557 100644
--- a/apps/cli/composition.md
+++ b/apps/cli/composition.md
@@ -156,8 +156,6 @@ flowchart LR
cfg --> plugin_dsh_base_tool_goal
plugin_dsh_base_tool_ralph["tool-ralph
@deepseek-ai/dsh-tool-ralph"]
cfg --> plugin_dsh_base_tool_ralph
- plugin_dsh_base_tool_str_replace_editor["tool-str-replace-editor
@deepseek-ai/dsh-tool-str-replace-editor"]
- cfg --> plugin_dsh_base_tool_str_replace_editor
plugin_dsh_base_repeat_tool_reminder["repeat-tool-reminder
@deepseek-ai/dsh-repeat-tool-reminder"]
cfg --> plugin_dsh_base_repeat_tool_reminder
plugin_dsh_base_web["web
@deepseek-ai/dsh-web"]
@@ -256,7 +254,6 @@ flowchart LR
| `tool-todo` | `@deepseek-ai/dsh-tool-todo` |
| `tool-goal` | `@deepseek-ai/dsh-tool-goal` |
| `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` |
-| `tool-str-replace-editor` | `@deepseek-ai/dsh-tool-str-replace-editor` |
| `repeat-tool-reminder` | `@deepseek-ai/dsh-repeat-tool-reminder` |
| `web` | `@deepseek-ai/dsh-web` |
| `web-search-deepseek` | `@deepseek-ai/dsh-web-search-deepseek` |
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 7a389053d4..4f88ba712f 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -84,7 +84,6 @@
"@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
- "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tool-todo": "workspace:^",
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index 482412b93a..cfd7abd500 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -335,7 +335,6 @@ describe('the shipped Web composition', () => {
]))
// And it keeps the standard agent's own tools rather than replacing them.
expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
- expect(tools).not.toContain('str_replace_editor')
expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()
// The preset's own authoring skill registers into ITS layer of the host
@@ -363,10 +362,8 @@ describe('the shipped Web composition', () => {
// the capabilities — so the assembly is what carries the claim.
const assembly = await ctx.systemPrompt.assemble({ scope: coded.agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code'])
- expect(toolNames(ctx, coded.agent)).not.toContain('str_replace_editor')
expect(ctx.commands.find(coded.agent, 'goal')).toBeDefined()
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? ''
- expect(sdk).not.toContain('str_replace_editor')
expect(sdk).toContain('web_search')
// The presentation is this agent's alone: the deployment default is
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 55c4409121..fdbe59332d 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 14aedbe0c0de0d0bc6779d3993c8a03135dc93e3
-config-catalog.zh.md: 4388de1391a273e1e088f1590d0ea1da98258927
+config-catalog.md: a7b98aac4695b5c1337cfa8e9d6266239a92f68f
+config-catalog.zh.md: 82d6a5eeb76fbbfa35b01e41ea7af2b159a9c550
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 14aedbe0c0..a7b98aac46 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -2871,24 +2871,6 @@ export interface Config {
Source: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts)
-
-
-## `@deepseek-ai/dsh-tool-str-replace-editor`
-
-Requires: `tools` · `fs`
-
-```ts config-catalog
-/** Configuration for the string-replacement editor tool. */
-export interface Config {
- /** Maximum returned view characters before clipping (default 16000). */
- maxOutputChars?: number
- /** Model-facing tool description. */
- description?: string
-}
-```
-
-Source: [`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts)
-
## `@deepseek-ai/dsh-tool-subagent`
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 4388de1391..82d6a5eeb7 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -2873,24 +2873,6 @@ export interface Config {
来源:[`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts)
-
-
-## `@deepseek-ai/dsh-tool-str-replace-editor`
-
-需要:`tools` · `fs`
-
-```ts config-catalog
-/** Configuration for the string-replacement editor tool. */
-export interface Config {
- /** Maximum returned view characters before clipping (default 16000). */
- maxOutputChars?: number
- /** Model-facing tool description. */
- description?: string
-}
-```
-
-来源:[`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts)
-
## `@deepseek-ai/dsh-tool-subagent`
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index 571af289d6..6b307d229d 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/docs/event-producer-consumer.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: d04a37ee71756619b36de192011a0053e5ceccb8
-event-producer-consumer.zh.md: 226c368517d989cba2eeac515ee2c544151e8005
+event-producer-consumer.md: 02c3c268298d04cc372ef8343c6f5a071b86320b
+event-producer-consumer.zh.md: a0de0add0d6777b3f830fe214d48ca3580d6aeae
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index d04a37ee71..02c3c26829 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -38,9 +38,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
-| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
-| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
-| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
+| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 226c368517..a0de0add0d 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -40,9 +40,9 @@
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:96`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:84`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
-| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
-| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
-| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
+| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index 303dd0f885..11eee3a26b 100644
--- a/docs/module-graph.i18n.yaml
+++ b/docs/module-graph.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: e6ca3e11a28b70310ecbd3118e1c736227626abf
-module-graph.zh.md: 9025dc6cce64d89cceaded2e92e3e5b465f79c7a
+module-graph.md: 1ad7b4348e6f79865c34d395d01ffb30367e8191
+module-graph.zh.md: d9e1bb8f9eab7a8ac8fdcaad3b2a292743b4cb03
diff --git a/docs/module-graph.md b/docs/module-graph.md
index e6ca3e11a2..1ad7b4348e 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -54,7 +54,6 @@ flowchart TD
pkg_fs_sandbox["fs-sandbox"]
pkg_tool_fs["tool-fs"]
pkg_tool_fs_search["tool-fs-search"]
- pkg_tool_str_replace_editor["tool-str-replace-editor"]
end
subgraph group_skill["packages/skill"]
pkg_skill["skill"]
@@ -721,10 +720,6 @@ flowchart TD
pkg_tool_fs_search --> pkg_system_prompt
pkg_tool_fs_search --> pkg_timeout
pkg_tool_fs_search --> pkg_tools
- pkg_tool_str_replace_editor --> pkg_fs
- pkg_tool_str_replace_editor --> pkg_sandbox
- pkg_tool_str_replace_editor --> pkg_sandbox_policy
- pkg_tool_str_replace_editor --> pkg_tools
pkg_tool_skill --> pkg_agent
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
@@ -1327,7 +1322,6 @@ flowchart TD
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index 9025dc6cce..d9e1bb8f9e 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -56,7 +56,6 @@ flowchart TD
pkg_fs_sandbox["fs-sandbox"]
pkg_tool_fs["tool-fs"]
pkg_tool_fs_search["tool-fs-search"]
- pkg_tool_str_replace_editor["tool-str-replace-editor"]
end
subgraph group_skill["packages/skill"]
pkg_skill["skill"]
@@ -723,10 +722,6 @@ flowchart TD
pkg_tool_fs_search --> pkg_system_prompt
pkg_tool_fs_search --> pkg_timeout
pkg_tool_fs_search --> pkg_tools
- pkg_tool_str_replace_editor --> pkg_fs
- pkg_tool_str_replace_editor --> pkg_sandbox
- pkg_tool_str_replace_editor --> pkg_sandbox_policy
- pkg_tool_str_replace_editor --> pkg_tools
pkg_tool_skill --> pkg_agent
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
@@ -1329,7 +1324,6 @@ flowchart TD
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml
index 7b592e60fa..1b4c450319 100644
--- a/docs/subsystems/filesystem.i18n.yaml
+++ b/docs/subsystems/filesystem.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/filesystem.md
-filesystem.md: 06fb92f453f8a09f87aee817ab56ef147be9a91f
-filesystem.zh.md: ea5b6f7a0d6f45b0d7960298ea10417c0b6c2dd3
+filesystem.md: 2d64c0abacd12133be0cca275c301f361661d031
+filesystem.zh.md: e563fafc6f4b3bd5b30786c88501f9c813218097
diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md
index 06fb92f453..2d64c0abac 100644
--- a/docs/subsystems/filesystem.md
+++ b/docs/subsystems/filesystem.md
@@ -239,7 +239,7 @@ interface FileReadOutcome {
## Observed-file state (policy plugin)
-Observed state is a `WeakMap>` held inside the `dsh-fs-observation-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` or `str_replace_editor` `view`, `str_replace`, or `insert` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O.
+Observed state is a `WeakMap>` held inside the `dsh-fs-observation-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O.
## Error taxonomy (provider contract)
diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md
index ea5b6f7a0d..e563fafc6f 100644
--- a/docs/subsystems/filesystem.zh.md
+++ b/docs/subsystems/filesystem.zh.md
@@ -239,7 +239,7 @@ interface FileReadOutcome {
## 已观测文件状态(策略插件)
-已观测状态是 `dsh-fs-observation-policy` 插件内部持有的 `WeakMap>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中,从而确认缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。
+已观测状态是 `dsh-fs-observation-policy` 插件内部持有的 `WeakMap>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中确认了缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。
## 错误分类体系(提供方约定)
diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml
index a70f3d6e35..9b4b0aa4e4 100644
--- a/docs/tool-catalog.i18n.yaml
+++ b/docs/tool-catalog.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/tool-catalog.md
-tool-catalog.md: 416585106de28296a3105fafbbe6e4edc0d0f16b
-tool-catalog.zh.md: 89d2e467863c09b78805fd9db8ec785ad7936a9d
+tool-catalog.md: 5242b761b860daf5259c49281170cbaf24d3f86c
+tool-catalog.zh.md: d1b4d67ce7bcb2290de763cd9957a8ba7d122e20
diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md
index 416585106d..5242b761b8 100644
--- a/docs/tool-catalog.md
+++ b/docs/tool-catalog.md
@@ -23,7 +23,6 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-tool-cordis` | `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop`, `cordis_undefine` | `ctx.tools`, `ctx.dynamicCordisRunner` | `tool/call`, `tool/result`, `process-local dynamic package lifecycle` | - | Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. |
| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. |
| `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. |
-| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. |
| `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `read_image`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt`, `ctx.attachments (image-tool registration)`, `ctx.llm + an image-capable route (image-tool execution)` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after read presence/absence or successful file operation`, `durable attachment (read_image)`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The image tool is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. |
| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.subprocess`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. |
| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`, `terminal_list`, `terminal_open`, `terminal_read`, `terminal_send`, `terminal_signal` | `ctx.tools`, `ctx.terminals`, `ctx.systemPrompt`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. |
@@ -554,112 +553,6 @@ Source: [`packages/shell/tool-pwsh-persistent/src/index.ts`](../packages/shell/t
One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description.
-
-
-## `@deepseek-ai/dsh-tool-str-replace-editor`
-
-### `str_replace_editor`
-
-Custom editing tool for viewing, creating and editing files
-* State is persistent across command calls and discussions with the user
-* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
-* The `create` command cannot be used if the specified `path` already exists as a file
-* If a `command` generates a long output, it will be truncated and marked with ``
-* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match
-
-Notes for using the `str_replace` command:
-* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
-* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
-* The `new_str` parameter should contain the edited lines that should replace the `old_str`
-
-```json
-{
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
-}
-```
-
-Source: [`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts)
-
-Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.
-
## `@deepseek-ai/dsh-tool-fs`
diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md
index 89d2e46786..d1b4d67ce7 100644
--- a/docs/tool-catalog.zh.md
+++ b/docs/tool-catalog.zh.md
@@ -27,7 +27,6 @@
| `@deepseek-ai/dsh-tool-cordis` | `cordis_define`、`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_run`、`cordis_stop`、`cordis_undefine` | `ctx.tools`、`ctx.dynamicCordisRunner` | `tool/call`、`tool/result`、`process-local dynamic package lifecycle` | - | 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 |
| `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 |
| `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 |
-| `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`、`ctx.fs` | `tool/call`、`fs/observed after view presence/absence, edit absence, or successful mutation`、`tool/result` | - | 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 |
| `@deepseek-ai/dsh-tool-fs` | `edit`、`read`、`read_image`、`write` | `ctx.tools`、`ctx.fs`、`ctx.systemPrompt`、`ctx.attachments (image-tool registration)`、`ctx.llm + an image-capable route (image-tool execution)` | `tool/call`、`fs/write-intent or fs/edit-intent for mutations`、`fs/observed after read presence/absence or successful file operation`、`durable attachment (read_image)`、`tool/result` | - | 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时图片工具不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图片输入,否则拒绝。 |
| `@deepseek-ai/dsh-tool-fs-search` | `glob`、`grep` | `ctx.tools`、`ctx.subprocess`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 |
| `@deepseek-ai/dsh-tool-terminal` | `terminal_close`、`terminal_list`、`terminal_open`、`terminal_read`、`terminal_send`、`terminal_signal` | `ctx.tools`、`ctx.terminals`、`ctx.systemPrompt`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 |
@@ -558,114 +557,6 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。
-
-
-## `@deepseek-ai/dsh-tool-str-replace-editor`
-
-### `str_replace_editor`
-
-用于查看、创建和编辑文件的自定义编辑工具:
-
-* 状态会在命令调用以及与用户的讨论之间持久保留
-* 如果 `path` 是文件,`view` 会显示应用 `cat -n` 后的结果。如果 `path` 是目录,`view` 会列出最多向下 2 层的非隐藏文件和目录
-* 如果指定的 `create` 命令目标 `path` 已作为文件存在,则不能使用该命令
-* 如果 `command` 产生较长输出,输出会被截断并标记为 ``
-* 当前命令不使用某个参数时,值为 `null` 的占位参数视为未提供。必填参数仍须提供值;删除匹配内容时应省略 `str_replace.new_str`,而不是将其设为 `null`
-
-使用 `str_replace` 命令时请注意:
-
-* `old_str` 参数应与原文件中一行或多行连续内容**完全**匹配。请留意空白字符!
-* 如果 `old_str` 参数在文件中不唯一,则不会执行替换。请确保在 `old_str` 中包含足够的上下文,使其唯一
-* `new_str` 参数应包含用于替换 `old_str` 的已编辑行
-
-```json
-{
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
-}
-```
-
-来源:[`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool-str-replace-editor/src/index.ts)
-
-基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。
-
## `@deepseek-ai/dsh-tool-fs`
diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml
index 94b5d23204..826e0d3639 100644
--- a/packages/bundle/base/cordis.patch.yml
+++ b/packages/bundle/base/cordis.patch.yml
@@ -421,11 +421,6 @@
subagentProvider: spawn
maxRounds: 64
- - id: tool-str-replace-editor
- name: '@deepseek-ai/dsh-tool-str-replace-editor'
- config:
- maxOutputChars: 16000
-
# Consecutive-repeat reminders on the tool chain.
- id: repeat-tool-reminder
name: '@deepseek-ai/dsh-repeat-tool-reminder'
diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json
index 24ff47b915..bc400c3250 100644
--- a/packages/bundle/base/package.json
+++ b/packages/bundle/base/package.json
@@ -101,7 +101,6 @@
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
- "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
index c98cf87881..bdcdd4bc0a 100644
--- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
+++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
@@ -82,7 +82,6 @@ describe('dsh-sdk-minimal bundle', () => {
config: { shellDialect: 'pwsh', timeoutMs: 300000 },
})
expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-fs-local')
- expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-tool-str-replace-editor')
expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual(
[...new Set(rows.map(row => row.name).filter((name): name is string => name !== undefined).map(packageName))].sort(),
)
diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml
index 2c9bd25990..74a6de9d21 100644
--- a/packages/bundle/web-app/cordis.patch.yml
+++ b/packages/bundle/web-app/cordis.patch.yml
@@ -347,9 +347,6 @@
- id: tool-fs-search
disabled: true
-- id: tool-str-replace-editor
- disabled: true
-
# The `skill` REGISTRY stays in the host plane. It is host+per-scope layered
# (the tools-registry shape): deployment-level providers — repository plugins,
# a host skill-filesystem row — register into its global layer, while a preset's
diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml
index 3ccef63ed5..016b0db83a 100644
--- a/packages/client/ui-deliverables/README.i18n.yaml
+++ b/packages/client/ui-deliverables/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-deliverables/README.md
-README.md: b228106848549dcddcde84be07c47392dc92a9db
-README.zh.md: 0a9d5207fe3c09b89ac34592f04c7eeaeed9b59a
+README.md: dc8f0fb1f923b5df981700ed7b33b8d13432dd2d
+README.zh.md: bb207b7e51b0769404711e4c45776b74bd0befd4
diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md
index b228106848..dc8f0fb1f9 100644
--- a/packages/client/ui-deliverables/README.md
+++ b/packages/client/ui-deliverables/README.md
@@ -43,7 +43,7 @@ The closing prose carries the same vocabulary: an inline-code token resolves by
Implementation internals — click to expand
-The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write`, `edit`, and mutating `str_replace_editor` commands. Reads, deletes, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost.
+The Node half registers the static `ui:deliverable-file-references` system-prompt section asking the model to mention primary files from successful creation or modification calls and to write those and any other changed-file references as Markdown inline code. The browser half registers `ProducedFiles` into the chat view's `conversation.chat.turnTail` hole. `deliverablesDefinition` folds each Turn's successful first-party mutation calls into `DeliverablesTurnData` from the validated raw arguments of `write` and `edit`. Reads, unsupported tools, malformed calls, and failed results contribute nothing. A new mutation tool needs an explicit Client contribution before it joins the list. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes both surfaces and leaves the view's empty chain at zero cost.
diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md
index 0a9d5207fe..bb207b7e51 100644
--- a/packages/client/ui-deliverables/README.zh.md
+++ b/packages/client/ui-deliverables/README.zh.md
@@ -43,7 +43,7 @@ kind: "package-reference"
实现细节——点击展开
-Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、删除、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。
+Node 半部注册静态 `ui:deliverable-file-references` 系统提示词段,要求模型点名成功创建或修改的主要文件,并把这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码。浏览器半部把 `ProducedFiles` 注册进 chat 视图的 `conversation.chat.turnTail` 洞。`deliverablesDefinition` 根据 `write` 与 `edit` 中经过校验的原始参数,把每个轮次成功的第一方修改调用折叠进 `DeliverablesTurnData`。读取、不受支持的工具、格式错误的调用和失败结果不贡献任何条目。新的修改工具必须增加显式 Client contribution 才能加入列表。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会同时移除两个表面,视图的空链以零成本留下。
diff --git a/packages/client/ui-deliverables/src/client/turn-deliverables.ts b/packages/client/ui-deliverables/src/client/turn-deliverables.ts
index 90e3248516..ef6e3468aa 100644
--- a/packages/client/ui-deliverables/src/client/turn-deliverables.ts
+++ b/packages/client/ui-deliverables/src/client/turn-deliverables.ts
@@ -51,8 +51,6 @@ function mutationPath(name: string, argsRaw: string): string | null {
return typeof args.content === 'string' ? pathValue(args.file_path) : null
case 'edit':
return validEditArgs(args) ? pathValue(args.file_path) : null
- case 'str_replace_editor':
- return editorMutationPath(args)
default:
return null
}
@@ -67,31 +65,6 @@ function validEditArgs(args: Readonly>): boolean {
&& (args.replace_all === undefined || typeof args.replace_all === 'boolean')
}
-/** Extract a path only from a complete mutating editor command. */
-function editorMutationPath(args: Readonly>): string | null {
- const path = pathValue(args.path)
- if (path === null) return null
- switch (args.command) {
- case 'create':
- return typeof args.file_text === 'string' ? path : null
- case 'str_replace':
- return typeof args.old_str === 'string'
- && args.old_str.length > 0
- && (args.new_str === undefined || typeof args.new_str === 'string')
- ? path
- : null
- case 'insert':
- return typeof args.insert_line === 'number'
- && Number.isInteger(args.insert_line)
- && args.insert_line >= 0
- && typeof args.new_str === 'string'
- ? path
- : null
- default:
- return null
- }
-}
-
/** A non-blank path preserves the exact spelling supplied to the tool. */
function pathValue(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value : null
@@ -105,10 +78,10 @@ function isRecord(value: unknown): value is Record {
/**
* Files produced by one Turn data value.
*
- * The source is the arguments of successful `write`, `edit`, and mutating
- * `str_replace_editor` calls, not the closing prose: a produced file must be
- * listed whether or not the model remembered to name it. Reads, unsupported
- * tools, malformed calls, and failed results contribute nothing. Paths keep
+ * The source is the arguments of successful `write` and `edit` calls, not the
+ * closing prose: a produced file must be listed whether or not the model
+ * remembered to name it. Reads, unsupported tools, malformed calls, and failed
+ * results contribute nothing. Paths keep
* first-seen order and appear once, so a file written and then edited in the
* same turn is one entry.
*
diff --git a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx
index 49a93b4ade..061f76194b 100644
--- a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx
+++ b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx
@@ -202,31 +202,11 @@ describe('produced-file Turn data', () => {
replace_all: false,
}),
result(5, 'edit'),
- call(6, 'create', 'str_replace_editor', {
- command: 'create', path: 'notes/new.md', file_path: 'wrong-create.txt', file_text: 'new',
- }),
- result(7, 'create'),
- call(8, 'replace', 'str_replace_editor', {
- command: 'str_replace', path: 'notes/existing.md', old_str: 'old', new_str: 'new',
- }),
- result(9, 'replace'),
- call(10, 'delete-text', 'str_replace_editor', {
- command: 'str_replace', path: 'notes/deleted-text.md', old_str: 'remove me',
- }),
- result(11, 'delete-text'),
- call(12, 'insert', 'str_replace_editor', {
- command: 'insert', path: 'notes/inserted.md', insert_line: 1, new_str: 'line',
- }),
- result(13, 'insert'),
])
expect(producedForClosing(deliverablesOf(value))).toEqual([
'out/index.html',
'out/app.css',
- 'notes/new.md',
- 'notes/existing.md',
- 'notes/deleted-text.md',
- 'notes/inserted.md',
])
})
@@ -253,42 +233,6 @@ describe('produced-file Turn data', () => {
caseName: 'edit has a non-boolean replace_all', name: 'edit',
args: { file_path: 'edit.txt', old_string: 'old', new_string: 'new', replace_all: 'yes' },
},
- {
- caseName: 'editor create omits file_text', name: 'str_replace_editor',
- args: { command: 'create', path: 'create.txt' },
- },
- {
- caseName: 'editor create has non-string file_text', name: 'str_replace_editor',
- args: { command: 'create', path: 'create.txt', file_text: 1 },
- },
- {
- caseName: 'editor replace omits old_str', name: 'str_replace_editor',
- args: { command: 'str_replace', path: 'replace.txt', new_str: 'new' },
- },
- {
- caseName: 'editor replace has an empty old_str', name: 'str_replace_editor',
- args: { command: 'str_replace', path: 'replace.txt', old_str: '' },
- },
- {
- caseName: 'editor replace has non-string new_str', name: 'str_replace_editor',
- args: { command: 'str_replace', path: 'replace.txt', old_str: 'old', new_str: 1 },
- },
- {
- caseName: 'editor insert omits insert_line', name: 'str_replace_editor',
- args: { command: 'insert', path: 'insert.txt', new_str: 'new' },
- },
- {
- caseName: 'editor insert has a fractional insert_line', name: 'str_replace_editor',
- args: { command: 'insert', path: 'insert.txt', insert_line: 1.5, new_str: 'new' },
- },
- {
- caseName: 'editor insert has a negative insert_line', name: 'str_replace_editor',
- args: { command: 'insert', path: 'insert.txt', insert_line: -1, new_str: 'new' },
- },
- {
- caseName: 'editor insert omits new_str', name: 'str_replace_editor',
- args: { command: 'insert', path: 'insert.txt', insert_line: 1 },
- },
])('ignores a successful result when $caseName', ({ name, args }) => {
const value = assembler([
at(1, 'turn/start', { turn: 1 }),
@@ -299,12 +243,9 @@ describe('produced-file Turn data', () => {
expect(producedForClosing(deliverablesOf(value))).toEqual([])
})
- it('ignores editor views, unsupported tools, failures, interruptions, malformed calls, and orphan results', () => {
- const replacement = result(25, 'replacement')
+ it('ignores unsupported tools, failures, interruptions, malformed calls, and orphan results', () => {
const value = assembler([
at(1, 'turn/start', { turn: 1 }),
- call(2, 'view', 'str_replace_editor', { command: 'view', path: 'viewed.txt' }),
- result(3, 'view'),
call(4, 'read', 'read', { file_path: 'input.txt' }),
result(5, 'read'),
call(6, 'unknown', 'custom_edit', { file_path: 'custom.txt', path: 'custom.txt' }),
@@ -326,19 +267,7 @@ describe('produced-file Turn data', () => {
file_path: ' ', old_string: 'old', new_string: 'new',
}),
result(20, 'blank-path'),
- call(21, 'missing-editor-path', 'str_replace_editor', { command: 'create', file_text: 'x' }),
- result(22, 'missing-editor-path'),
result(23, 'orphan'),
- call(24, 'replacement', 'str_replace_editor', {
- command: 'insert', path: 'replaced.txt', insert_line: 0, new_str: 'new',
- }),
- {
- ...replacement,
- event: {
- ...replacement.event,
- surfaceOp: { op: 'replace', start: 1, end: 1 },
- } as SessionEvent,
- },
at(26, 'turn/end', { turn: 1, reason: { kind: 'interrupted' } }),
])
diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml
index 4b0b8c7bbc..dff7d298b2 100644
--- a/packages/client/ui-tool/README.i18n.yaml
+++ b/packages/client/ui-tool/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md
-README.md: 2fe410425935aed46ab95844c81568037ac929e9
-README.zh.md: 83440881fe16582a75fd8dd762577dc7b4d5335b
+README.md: e5a6c85e21b9383b228ae439912cd991b043febb
+README.zh.md: 1bbc43e3c4a51dab111328791300f2dc5fb8fae4
diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md
index 2fe4104259..e5a6c85e21 100644
--- a/packages/client/ui-tool/README.md
+++ b/packages/client/ui-tool/README.md
@@ -43,7 +43,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `blo
### Built-in views
-This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`.
+This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`.
-----
diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md
index 83440881fe..1bbc43e3c4 100644
--- a/packages/client/ui-tool/README.zh.md
+++ b/packages/client/ui-tool/README.zh.md
@@ -43,7 +43,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
### 内置视图
-本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
+本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
-----
diff --git a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts
index 03eaf04a3a..439cdbbc1c 100644
--- a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts
+++ b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts
@@ -47,31 +47,11 @@ function narrowDiffs(diffs: unknown): DiffHunk[] | null {
return out
}
-type IntendedDiff = { tool: 'write' | 'edit' | 'str_replace_editor'; diff: DiffHunk }
+type IntendedDiff = { tool: 'write' | 'edit'; diff: DiffHunk }
function intendedDiff(block: ToolCallBlock): IntendedDiff | null {
const parsed = parsedToolCall(block)
if (parsed === null) return null
- if (parsed.name === 'str_replace_editor') {
- const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args
- if (typeof path !== 'string' || path.trim() === '') return null
- if (command === 'create') {
- if (fileText !== undefined && typeof fileText !== 'string') return null
- return {
- tool: 'str_replace_editor',
- diff: { path, oldText: null, newText: fileText ?? '' },
- }
- }
- if (command === 'str_replace') {
- if (oldText !== undefined && typeof oldText !== 'string') return null
- if (newText !== undefined && typeof newText !== 'string') return null
- return {
- tool: 'str_replace_editor',
- diff: { path, oldText: oldText ?? null, newText: newText ?? '' },
- }
- }
- return null
- }
const { file_path: path } = parsed.args
if (typeof path !== 'string' || path.trim() === '') return null
if (!validEscalationFields(parsed.args)) return null
@@ -97,11 +77,9 @@ function appliedDiffs(meta: unknown): DiffHunk[] | 'empty' | null {
}
/**
- * Derive running diffs for root write/edit and `str_replace_editor`
- * create/replace calls, plus applied settled diffs for root write/edit calls.
+ * Derive running diffs and applied settled diffs for root write/edit calls.
* A successful write with valid empty metadata uses its argument-derived
- * whole-file diff, matching create and identical-overwrite presentation;
- * `str_replace_editor` settles through Generic because it has no result view.
+ * whole-file diff, matching create and identical-overwrite presentation.
* @param block - running or settled Tool block.
* @returns the diff-card props, or null for the generic path.
*/
@@ -110,7 +88,6 @@ export function diffCardModel(block: ToolCallBlock): DiffCardModel | null {
const intended = intendedDiff(block)
if (intended === null) return null
if (!('kind' in block)) return { card: { diffs: [intended.diff] } }
- if (intended.tool === 'str_replace_editor') return null
if (block.isError) return null
const applied = appliedDiffs(block.meta)
if (applied === null || applied === 'empty') {
diff --git a/packages/client/ui-tool/tests/diff-card.client.spec.tsx b/packages/client/ui-tool/tests/diff-card.client.spec.tsx
index ed386b8321..8388e5dcf4 100644
--- a/packages/client/ui-tool/tests/diff-card.client.spec.tsx
+++ b/packages/client/ui-tool/tests/diff-card.client.spec.tsx
@@ -63,47 +63,6 @@ describe('diffCardModel', () => {
})
})
- it.each([
- {
- command: 'create',
- args: { command: 'create', path: 'notes/new.txt', file_text: 'new file\n' },
- diff: { path: 'notes/new.txt', oldText: null, newText: 'new file\n' },
- },
- {
- command: 'str_replace',
- args: { command: 'str_replace', path: 'notes/demo.txt', old_str: 'old', new_str: 'new' },
- diff: { path: 'notes/demo.txt', oldText: 'old', newText: 'new' },
- },
- ])('preserves the running str_replace_editor $command diff', ({ args, diff }) => {
- expect(diffCardModel(running({
- name: 'str_replace_editor',
- argsRaw: JSON.stringify(args),
- }))).toEqual({ card: { diffs: [diff] } })
- })
-
- it('preserves str_replace_editor defaults and its settled Generic result', () => {
- const argsRaw = JSON.stringify({ command: 'str_replace', path: 'notes/demo.txt' })
- expect(diffCardModel(running({ name: 'str_replace_editor', argsRaw }))).toEqual({
- card: { diffs: [{ path: 'notes/demo.txt', oldText: null, newText: '' }] },
- })
- expect(diffCardModel(settled({
- call: { name: 'str_replace_editor', argsRaw },
- meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'old', newText: 'new' }] },
- }))).toBeNull()
- })
-
- it('keeps unsupported or malformed str_replace_editor calls generic', () => {
- const editor = (args: Record) => running({
- name: 'str_replace_editor', argsRaw: JSON.stringify(args),
- })
- expect(diffCardModel(editor({ command: 'view', path: 'notes/demo.txt' }))).toBeNull()
- expect(diffCardModel(editor({ command: 'insert', path: 'notes/demo.txt', new_str: 'x' }))).toBeNull()
- expect(diffCardModel(editor({ command: 'create', path: '', file_text: 'x' }))).toBeNull()
- expect(diffCardModel(editor({ command: 'create', path: 'notes/demo.txt', file_text: 1 }))).toBeNull()
- expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', old_str: 1 }))).toBeNull()
- expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', new_str: 1 }))).toBeNull()
- })
-
it('derives a settled card from result metadata, which replaces the intended diff', () => {
expect(diffCardModel(settled({
meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'a', newText: 'b' }] },
diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts
index 0630cfaa8e..33f970a180 100644
--- a/packages/core/tools/tests/gen-tool-catalog.spec.ts
+++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts
@@ -34,7 +34,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
'read', 'read_image', 'run_code', 'schedule_create', 'schedule_delete',
'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search',
'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate',
- 'str_replace_editor', 'subagent', 'team_task_create',
+ 'subagent', 'team_task_create',
'team_task_get', 'team_task_list', 'team_task_update', 'terminal_close', 'terminal_list',
'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write',
'update_goal', 'wait_agent', 'web_fetch', 'web_search', 'workflow', 'write',
diff --git a/packages/fs/README.i18n.yaml b/packages/fs/README.i18n.yaml
index 5311da4c5b..7675ae6897 100644
--- a/packages/fs/README.i18n.yaml
+++ b/packages/fs/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/README.md
-README.md: c8cfc5f8d5dd5d225149abbf192d23df1b51cf71
-README.zh.md: 9596f72e76241de43072ee590c141b043af6a861
+README.md: 797cdf1697a088958f56c9e4ec2e7de0bd854835
+README.zh.md: 900619ca1e53f2561e23dc3e7f1efe559739a4be
diff --git a/packages/fs/README.md b/packages/fs/README.md
index c8cfc5f8d5..797cdf1697 100644
--- a/packages/fs/README.md
+++ b/packages/fs/README.md
@@ -33,7 +33,6 @@ Seven packages plus the remote sibling `fs-e2b` play the filesystem roles; the s
| [`fs-observation-policy/`](fs-observation-policy/README.md) | Read-before-edit policy: records observed presence or absence and guards write/edit through the `fs/*` events | `fs/*` listeners |
| [`tool-fs/`](tool-fs/README.md) | Model-facing `read`, `read_image`, `write`, and `edit` tools plus their executor | registers on `ctx.tools` |
| [`tool-fs-search/`](tool-fs-search/README.md) | Model-facing `glob` and `grep` discovery tools backed by the packaged ripgrep binary | registers on `ctx.tools` |
-| [`tool-str-replace-editor/`](tool-str-replace-editor/README.md) | Standalone `str_replace_editor` tool: `view`, `create`, `str_replace`, and `insert` over `ctx.fs` | registers on `ctx.tools` |
The policy is a plugin, not a service the tools inject: removing it leaves the bare provider's unconditional mutation behavior instead of breaking the tools. The mode fence in `fs-sandbox` and the read-before-edit gate compose. `tool-fs-search` deliberately does not extend the provider contract — search is a process-backed ripgrep workflow, so filesystem backends stay free of a universal search API.
diff --git a/packages/fs/README.zh.md b/packages/fs/README.zh.md
index 9596f72e76..900619ca1e 100644
--- a/packages/fs/README.zh.md
+++ b/packages/fs/README.zh.md
@@ -33,7 +33,6 @@ kind: "package-group"
| [`fs-observation-policy/`](fs-observation-policy/README.zh.md) | 编辑前读取策略:记录观测到的存在或缺失,并通过 `fs/*` 事件防护写入/编辑 | `fs/*` 监听器 |
| [`tool-fs/`](tool-fs/README.zh.md) | 面向模型的 `read`、`read_image`、`write` 与 `edit` 工具及其执行器 | 注册到 `ctx.tools` |
| [`tool-fs-search/`](tool-fs-search/README.zh.md) | 由打包 ripgrep 二进制支持的面向模型 `glob` 与 `grep` 发现工具 | 注册到 `ctx.tools` |
-| [`tool-str-replace-editor/`](tool-str-replace-editor/README.zh.md) | 独立的 `str_replace_editor` 工具:基于 `ctx.fs` 的 `view`、`create`、`str_replace` 与 `insert` | 注册到 `ctx.tools` |
策略是插件,不是工具注入的服务:移除它只会让工具回到裸提供方的无条件变更行为,而不会破坏工具。`fs-sandbox` 的模式围栏与编辑前读取门禁可以组合。`tool-fs-search` 有意不扩展提供方约定——搜索是由进程支持的 ripgrep 工作流,因此文件系统后端无需承担通用搜索 API。
diff --git a/packages/fs/tool-str-replace-editor/README.i18n.yaml b/packages/fs/tool-str-replace-editor/README.i18n.yaml
deleted file mode 100644
index d753b957de..0000000000
--- a/packages/fs/tool-str-replace-editor/README.i18n.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-# pnpm run verify-translation-pairing --write packages/fs/tool-str-replace-editor/README.md
-README.md: 66663b2ab19585c30f00e6e6d91ce759b4fb8d81
-README.zh.md: 60c63b25cb5c5960aaafcf4f91b3ce23e8911cd7
diff --git a/packages/fs/tool-str-replace-editor/README.md b/packages/fs/tool-str-replace-editor/README.md
deleted file mode 100644
index 66663b2ab1..0000000000
--- a/packages/fs/tool-str-replace-editor/README.md
+++ /dev/null
@@ -1,149 +0,0 @@
----
-description: "The standalone str_replace_editor tool over ctx.fs for users and maintainers composing Claude-Code-style file editing for agents."
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-tool-str-replace-editor
-
-English | [中文](README.zh.md)
-
-## Summary
-
-`dsh-tool-str-replace-editor` provides a standalone model-facing `str_replace_editor` tool over `ctx.fs`: `view` shows numbered file content or a shallow directory listing, `create` makes a new file, `str_replace` applies a unique literal replacement, and `insert` adds lines at a chosen boundary. It is composable with persistent Bash, one-shot Bash, sandboxed Bash, or another terminal surface. Mutations obey the same read-before-edit policy and sandbox fence as the rest of the fs family, enforced by whichever backend and policy plugins are mounted. Choose it when a deployment wants the Claude-Code-style single editor tool with absolute paths; the `dsh-tool-fs` package provides the alternative `read`/`write`/`edit` suite.
-
-## Table of Contents
-
-- [Use this package](#use-this-package)
-- [Understand the implementation](#understand-the-implementation)
-- [Further Exploration](#further-exploration)
-- [Model Experience](#model-experience)
-- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
-- [Dev Note](#dev-note)
-
------
-
-
-## Use this package
-
-Mount the tool alongside a `ctx.fs` backend (and, for guarded mutations, the policy plugin) when the model should edit files through the familiar `view`/`create`/`str_replace`/`insert` command vocabulary on absolute paths.
-
-### Minimal composition
-
-A backend, optionally the policy plugin, then the tool; the editor composes with any terminal surface.
-
-```yaml
-- name: '@deepseek-ai/dsh-fs-local'
-- name: '@deepseek-ai/dsh-fs-observation-policy'
-- name: '@deepseek-ai/dsh-tool-str-replace-editor'
-```
-
-### Configuration
-
-| Key | Default | Meaning |
-|---|---|---|
-| `maxOutputChars` | `16000` | Prefix characters retained for file and directory views |
-| `description` | `Custom editing tool for viewing, creating and editing files` (multi-line) | Model-facing tool description |
-
-### The commands
-
-`view` returns one-based numbered file content (tabs preserved, so displayed text stays valid literal replacement input) or a two-level directory listing that omits hidden, dependency, and Python-cache entries. `create` makes a new file and refuses to overwrite an existing one. Command-specific fields may contain `null` placeholders when the selected command does not use them; required fields stay required, `view_range: null` selects the full view, and `str_replace.new_str: null` is rejected so deletion requires omission. `str_replace` requires one unique literal match, with errors reported in the public `old_str` vocabulary; `insert` follows the selected zero-based insertion boundary without adding an implicit trailing newline. Mutations preserve tabs outside the requested edit.
-
-### Failures and recovery
-
-A metadata miss from `view`, `str_replace`, or `insert` records confirmed absence before returning `FS_NOT_FOUND`, so a later `create` can recover an externally deleted path through the mounted policy's guarded-create flow; absence never authorizes `str_replace` or `insert`. Guarded mutations inherit the policy plugin's codes and remedies — `FS_NOT_OBSERVED` (read the file, then retry), `FS_STALE_VERSION` (re-read, then retry) — and sandbox denials surface as the `[sandbox: file access denied under mode]` marker. Paths must be absolute; a relative path is refused with a hint.
-
------
-
-
-## Understand the implementation
-
-
-Implementation internals — click to expand
-
-This section explains the design decisions behind the editor tool and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
-
-### Design concept
-
-The tool is one schema with four commands over `ctx.fs`. Mutations never touch the provider directly with their own assumptions: each one runs the `fs/write-intent` or `fs/edit-intent` waterfall to obtain the policy plugin's guard, resolves the per-call sandbox policy when the mounted `ctx.fs` confines, and delegates enforcement to the provider. `str_replace` and `insert` additionally re-read the file and use the observed version as the compare-and-swap basis when no policy plugin supplies a guard.
-
-### Source map
-
-| File | Role |
-|---|---|
-| [`src/index.ts`](src/index.ts) | The whole tool: schema, command dispatch, view rendering, mutation policy |
-
-### How each command runs
-
-Every command resolves the absolute path first; mutations then follow one shared flow — policy guard, provider enforcement, then an `fs/observed` record on success — while `view` only stats and renders. The entire tool — schema, command dispatch, and view rendering — lives in `src/index.ts`.
-
-
-
------
-
-
-## Further Exploration
-
-Read these pages when the package-level contract is not enough. They move from the tool to the contract, policy, and backends it composes with.
-
-- [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — exhaustive provider contract, policy events, and error taxonomy.
-- [dsh-fs](../fs/README.md) — the `ctx.fs` contract this tool consumes.
-- [tool-fs](../tool-fs/README.md) — the alternative `read`/`write`/`edit` tool suite.
-- [fs-observation-policy](../fs-observation-policy/README.md) — the policy plugin that guards mutations through the `fs/*` events.
-- [fs-sandbox](../fs-sandbox/README.md) — the sandbox-enforcing backend that fences mutations.
-- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor) — the exhaustive schema this package registers.
-
------
-
-
-## Model Experience
-
-### Tool schema
-
-#### What the model sees
-
-The generated [`str_replace_editor` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor), including the configured `description`. The plugin contributes no standalone system-prompt section.
-
-#### Token effect
-
-Fixed schema cost while `str_replace_editor` is visible.
-
-#### KV Cache effect
-
-Prefix-stable while the configured description and schema remain unchanged.
-
-### Tool results
-
-#### What the model sees
-
-Views return numbered text or a shallow directory listing. Calls expose file locations, and create/replace calls expose diff cards to presentation surfaces. Mutations return concise confirmations. Long views keep their prefix and append a clipping notice.
-
-#### Token effect
-
-Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice.
-
-#### KV Cache effect
-
-Append-only tool results follow the reusable request prefix.
-
-## Known Limitations and Deferred Work
-
-
-
-
-These limits define when the editor tool is a poor fit or needs special operational care. They are current package constraints, not a general editor comparison or a task backlog.
-
-- **Operations target UTF-8 text** — binary files are unsupported.
-- **`str_replace` intentionally rejects zero or multiple matches** — it has no `replace_all` argument.
-- **Every mutation goes through the mounted policy and sandbox** — `fs/write-intent` or `fs/edit-intent` resolves the current session sandbox policy and delegates enforcement to the mounted filesystem and policy plugins, so a deployment without them gets unconditional mutations.
-
-
-### Dev Note
-
-
-Working context for maintainers — click to expand
-
-None.
-
-
-
-**Runtime invariant:** No companion is published. The tool adapter owns no independent durable state; filesystem mutation relations stay with the provider and policy plugins.
diff --git a/packages/fs/tool-str-replace-editor/README.zh.md b/packages/fs/tool-str-replace-editor/README.zh.md
deleted file mode 100644
index 60c63b25cb..0000000000
--- a/packages/fs/tool-str-replace-editor/README.zh.md
+++ /dev/null
@@ -1,149 +0,0 @@
----
-description: "基于 `ctx.fs` 的独立 str_replace_editor 工具:供组合 Claude Code 风格文件编辑能力的用户与维护者使用。"
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-tool-str-replace-editor
-
-[English](README.md) | 中文
-
-## 概述
-
-`dsh-tool-str-replace-editor` 提供基于 `ctx.fs` 的独立面向模型 `str_replace_editor` 工具:`view` 显示带行号的文件内容或浅层目录列表,`create` 创建新文件,`str_replace` 应用唯一的字面量替换,`insert` 在选定的边界处插入行。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或其他终端接口组合。修改操作遵守与 fs 家族其余部分相同的编辑前读取策略与沙箱围栏,具体由所挂载的后端与策略插件强制执行。当部署需要 Claude Code 风格、使用绝对路径的单一编辑器工具时选择它;`dsh-tool-fs` 包提供替代的 `read`/`write`/`edit` 套件。
-
-## 目录
-
-- [使用本包](#use-this-package)
-- [理解实现](#understand-the-implementation)
-- [进一步探索](#further-exploration)
-- [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
-- [开发备注](#dev-note)
-
------
-
-
-## 使用本包
-
-当模型应通过熟悉的 `view`/`create`/`str_replace`/`insert` 命令词汇在绝对路径上编辑文件时,把工具与 `ctx.fs` 后端(以及需要防护变更时的策略插件)一起挂载。
-
-### 最小组合
-
-一个后端、可选地加策略插件,然后是工具;编辑器可与任何终端接口组合。
-
-```yaml
-- name: '@deepseek-ai/dsh-fs-local'
-- name: '@deepseek-ai/dsh-fs-observation-policy'
-- name: '@deepseek-ai/dsh-tool-str-replace-editor'
-```
-
-### 配置
-
-| 键 | 默认值 | 含义 |
-|---|---|---|
-| `maxOutputChars` | `16000` | 文件和目录查看结果保留的前缀字符数 |
-| `description` | `Custom editing tool for viewing, creating and editing files`(多行) | 面向模型的工具描述 |
-
-### 命令
-
-`view` 返回从 1 开始编号的文件内容(保留制表符,因此显示的文本仍可作为有效的字面量替换输入)或忽略隐藏、依赖与 Python 缓存条目的两层目录列表。`create` 创建新文件,并拒绝覆盖现有文件。选定命令不使用的专用字段可以包含 `null` 占位符;必填字段仍保持必填,`view_range: null` 选择完整视图,`str_replace.new_str: null` 会被拒绝,因此删除内容必须省略该字段。`str_replace` 要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇;`insert` 遵循所选的零基插入边界,不会隐式补尾换行。修改操作会保留请求编辑范围之外的制表符。
-
-### 失败与恢复
-
-`view`、`str_replace` 或 `insert` 发生元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前记录确认缺失,因此后续 `create` 可以通过已挂载策略的防护创建流程恢复外部删除的路径;缺失状态绝不会授权 `str_replace` 或 `insert`。防护变更继承策略插件的错误码与恢复指令——`FS_NOT_OBSERVED`(先读取文件再重试)、`FS_STALE_VERSION`(先重新读取再重试)——沙箱拒绝则表现为 `[sandbox: file access denied under mode]` 标记。路径必须是绝对路径;相对路径会被拒绝并给出提示。
-
------
-
-
-## 理解实现
-
-
-实现细节——点击展开
-
-本节解释编辑器工具背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
-
-### 设计理念
-
-该工具是基于 `ctx.fs` 的单一 schema、四个命令。修改操作绝不带着自己的假设直接触碰提供方:每个操作都运行 `fs/write-intent` 或 `fs/edit-intent` waterfall 以取得策略插件的防护,在已挂载的 `ctx.fs` 实施沙箱限制时解析按调用沙箱策略,并把强制执行委托给提供方。`str_replace` 与 `insert` 还会重新读取文件,并在没有策略插件提供防护时把观察到的版本作为比较并交换的基础。
-
-### 源码地图
-
-| 文件 | 职责 |
-|---|---|
-| [`src/index.ts`](src/index.ts) | 整个工具:schema、命令分派、查看渲染、修改策略 |
-
-### 各命令如何运行
-
-每个命令都先解析绝对路径;修改操作随后遵循同一条共享流程——策略防护、提供方强制执行、成功后记录 `fs/observed`——而 `view` 只执行 stat 并渲染。整个工具——schema、命令分派与查看渲染——都位于 `src/index.ts`。
-
-
-
------
-
-
-## 进一步探索
-
-当包级约定不够用时阅读以下页面。它们从工具逐步进入它所组合的约定、策略与后端。
-
-- [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——穷尽式提供方约定、策略事件与错误分类体系。
-- [dsh-fs](../fs/README.zh.md)——本工具消费的 `ctx.fs` 约定。
-- [tool-fs](../tool-fs/README.zh.md)——替代的 `read`/`write`/`edit` 工具套件。
-- [fs-observation-policy](../fs-observation-policy/README.zh.md)——通过 `fs/*` 事件防护变更的策略插件。
-- [fs-sandbox](../fs-sandbox/README.zh.md)——围栏变更的沙箱强制后端。
-- [生成工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-str-replace-editor)——本包注册的穷尽式 schema。
-
------
-
-
-## 模型体验
-
-### 工具 schema
-
-#### 模型看到的内容
-
-生成的 [`str_replace_editor` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-str-replace-editor),包含配置的 `description`。本插件不贡献独立系统提示词段。
-
-#### Token 影响
-
-`str_replace_editor` 可见时产生固定的 schema 成本。
-
-#### KV Cache 影响
-
-配置的描述与 schema 不变时前缀稳定。
-
-### 工具结果
-
-#### 模型看到的内容
-
-查看操作返回带行号文本或浅层目录列表。调用会提供文件位置,创建/替换调用还会向展示层提供 diff 卡片。修改操作返回简洁确认。长查看结果保留前缀并追加截断提示。
-
-#### Token 影响
-
-随数据变化,并受 `maxOutputChars` 与固定截断提示约束。
-
-#### KV Cache 影响
-
-工具结果以追加方式位于可复用请求前缀之后。
-
-## 已知限制与延期工作
-
-
-
-
-这些限制说明编辑器工具何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用编辑器对比或任务积压。
-
-- **操作面向 UTF-8 文本**——不支持二进制文件。
-- **`str_replace` 刻意拒绝零匹配或多匹配**——它没有 `replace_all` 参数。
-- **每个修改操作都会经过已挂载的策略与沙箱**——`fs/write-intent` 或 `fs/edit-intent` 解析当前会话的沙箱策略,并把强制执行委托给已挂载的文件系统与策略插件,因此未挂载它们的部署会得到无条件变更。
-
-
-### 开发备注
-
-
-维护者的工作上下文——点击展开
-
-无。
-
-
-
-**运行时不变式:** 不发布伴生入口。tool adapter 不持有独立持久状态;filesystem mutation 关系属于 provider 与 policy 插件。
diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json
deleted file mode 100644
index d989328728..0000000000
--- a/packages/fs/tool-str-replace-editor/package.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "name": "@deepseek-ai/dsh-tool-str-replace-editor",
- "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service",
- "version": "0.1.2-rc.1",
- "publishConfig": {
- "access": "public"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
- "directory": "packages/fs/tool-str-replace-editor"
- },
- "type": "module",
- "main": "lib/index.js",
- "types": "lib/types/index.d.ts",
- "exports": {
- ".": {
- "types": "./lib/types/index.d.ts",
- "default": "./lib/index.js"
- },
- "./package.json": "./package.json"
- },
- "files": [
- "lib/index.js",
- "lib/types/**/*.d.ts"
- ],
- "license": "MIT",
- "peerDependencies": {
- "@deepseek-ai/dsh-fs": "workspace:^",
- "@deepseek-ai/dsh-sandbox": "workspace:^",
- "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
- "@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^"
- },
- "dependencies": {
- "@deepseek-ai/schemastery": "workspace:^"
- },
- "devDependencies": {
- "@deepseek-ai/dsh-agent": "workspace:^",
- "@deepseek-ai/dsh-fs": "workspace:^",
- "@deepseek-ai/dsh-fs-local": "workspace:^",
- "@deepseek-ai/dsh-fs-observation-policy": "workspace:^",
- "@deepseek-ai/dsh-fs-sandbox": "workspace:^",
- "@deepseek-ai/dsh-llm": "workspace:^",
- "@deepseek-ai/dsh-sandbox": "workspace:^",
- "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
- "@deepseek-ai/dsh-session": "workspace:^",
- "@deepseek-ai/dsh-system-prompt": "workspace:^",
- "@deepseek-ai/dsh-tools": "workspace:^",
- "@deepseek-ai/cordis": "workspace:^",
- "@deepseek-ai/dsh-session-projection": "workspace:^"
- }
-}
diff --git a/packages/fs/tool-str-replace-editor/src/index.ts b/packages/fs/tool-str-replace-editor/src/index.ts
deleted file mode 100644
index f77736685e..0000000000
--- a/packages/fs/tool-str-replace-editor/src/index.ts
+++ /dev/null
@@ -1,531 +0,0 @@
-/**
- * Model-facing `str_replace_editor` over the Harness filesystem seam.
- * @module @deepseek-ai/dsh-tool-str-replace-editor
- */
-
-import { isAbsolute } from 'node:path'
-import type { Context } from '@deepseek-ai/cordis'
-import z from '@deepseek-ai/schemastery'
-import { FsError } from '@deepseek-ai/dsh-fs'
-import type { FsInfo, FsTarget, FsWriteIntent } from '@deepseek-ai/dsh-fs'
-import { sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox'
-import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
-import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
-import { defineTool } from '@deepseek-ai/dsh-tools'
-import type { ToolCallView, ToolRunContext } from '@deepseek-ai/dsh-tools'
-
-const TRUNCATED_MESSAGE = 'To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.'
-
-const DEFAULT_DESCRIPTION = `
-Custom editing tool for viewing, creating and editing files
-* State is persistent across command calls and discussions with the user
-* If \`path\` is a file, \`view\` displays the result of applying \`cat -n\`. If \`path\` is a directory, \`view\` lists non-hidden files and directories up to 2 levels deep
-* The \`create\` command cannot be used if the specified \`path\` already exists as a file
-* If a \`command\` generates a long output, it will be truncated and marked with \`\`
-* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit \`str_replace.new_str\` rather than setting it to null when deleting a match
-
-Notes for using the \`str_replace\` command:
-* The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
-* If the \`old_str\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \`old_str\` to make it unique
-* The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\`
-`.trim()
-
-function maybeTruncate(content: string, maxOutputChars: number): string {
- return content.length <= maxOutputChars
- ? content
- : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE
-}
-
-function codepointCompare(left: string, right: string): number {
- return left < right ? -1 : left > right ? 1 : 0
-}
-
-function matchOffsets(content: string, search: string): number[] {
- const offsets: number[] = []
- let offset = 0
- while (true) {
- const match = content.indexOf(search, offset)
- if (match < 0) return offsets
- offsets.push(match)
- offset = match + search.length
- }
-}
-
-function lineNumbersAt(content: string, offsets: readonly number[]): number[] {
- let line = 1
- let cursor = 0
- return offsets.map((offset) => {
- while (cursor < offset) {
- if (content[cursor] === '\n') line += 1
- cursor += 1
- }
- return line
- })
-}
-
-class MutationPolicy {
- private readonly policy: SandboxPolicyService | undefined
-
- constructor(ctx: Context) {
- this.policy = ctx.fs.sandboxMode === undefined ? undefined : ctx.get('sandboxPolicy')
- if (ctx.fs.sandboxMode !== undefined && this.policy === undefined) {
- throw new Error('tool-str-replace-editor: the mounted filesystem confines but ctx.sandboxPolicy is missing')
- }
- }
-
- resolve(exec: ToolRunContext): SandboxExecutionPolicy | undefined {
- return this.policy?.resolve({
- ...exec.agent === undefined ? {} : { session: exec.agent.session },
- })
- }
-
- mapError(error: unknown, policy: SandboxExecutionPolicy | undefined): unknown {
- if (!(error instanceof FsError) || error.code !== 'FS_SANDBOX_DENIED') return error
- const mode = (policy as SandboxExecutionPolicy).mode
- return new FsError(sandboxDenialMarker(mode), 'FS_SANDBOX_DENIED', { cause: error })
- }
-}
-
-async function resolveTarget(
- ctx: Context,
- path: string,
- signal: AbortSignal,
-): Promise {
- if (path.trim().length === 0) throw new Error('path must be a non-empty string')
- if (!isAbsolute(path)) {
- throw new Error(`The path ${path} is not an absolute path, it should start with \`/\`. Maybe you meant /${path}?`)
- }
- return ctx.fs.resolve(path, { signal })
-}
-
-async function statExisting(
- ctx: Context,
- target: FsTarget,
- command: 'view' | 'str_replace' | 'insert',
- exec: ToolRunContext,
-): Promise {
- const info = await ctx.fs.stat(target, exec.signal)
- if (info === undefined) {
- ctx.emit('fs/observed', target, { kind: 'absent' }, exec)
- throw new FsError(
- `The path ${target.displayPath} does not exist. Please provide a valid path.`,
- 'FS_NOT_FOUND',
- )
- }
- if (info.type === 'directory' && command !== 'view') {
- throw new FsError(
- `The path ${target.displayPath} is a directory and only the \`view\` command can be used on directories`,
- 'FS_NOT_REGULAR_FILE',
- )
- }
- return info
-}
-
-function requiredForCommand(
- value: string | undefined,
- parameter: string,
- command: string,
- allowEmpty = true,
-): string {
- if (value === undefined) throw new Error(`Parameter \`${parameter}\` is required for command: ${command}`)
- if (!allowEmpty && value.length === 0) {
- throw new Error(`Parameter \`${parameter}\` is empty for command: ${command}`)
- }
- return value
-}
-
-function formatFileView(
- path: string,
- content: string,
- maxOutputChars: number,
- viewRange?: number[],
-): string {
- const allLines = content.split('\n')
- let lines = allLines
- let initialLine = 1
- let finalLine: number | undefined
- let prompt = `Here's the content of ${path} with line numbers (which has a total of ${allLines.length} lines)`
- if (viewRange !== undefined) {
- const [requestedInitialLine, requestedFinalLine] = viewRange
- if (
- viewRange.length !== 2
- || requestedInitialLine === undefined
- || requestedFinalLine === undefined
- || !viewRange.every(Number.isInteger)
- ) {
- throw new Error('Invalid `view_range`. It should be a list of two integers.')
- }
- initialLine = requestedInitialLine
- finalLine = requestedFinalLine
- if (initialLine < 1 || initialLine > allLines.length) {
- throw new Error(
- `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its first element \`${initialLine}\` should be within the range of lines of the file: [1, ${allLines.length}]`,
- )
- }
- if (finalLine > allLines.length) {
- throw new Error(
- `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its second element \`${finalLine}\` should be smaller than the number of lines in the file: \`${allLines.length}\``,
- )
- }
- if (finalLine !== -1 && finalLine < initialLine) {
- throw new Error(
- `Invalid \`view_range\`: [${viewRange.join(', ')}]. Its second element \`${finalLine}\` should be larger or equal than its first \`${initialLine}\``,
- )
- }
- lines = finalLine === -1
- ? allLines.slice(initialLine - 1)
- : allLines.slice(initialLine - 1, finalLine)
- prompt += ` with view_range=[${initialLine}, ${finalLine}]`
- }
- const numbered = lines
- .map((line, index) => `${String(initialLine + index).padStart(6, ' ')} ${line}`)
- .join('\n')
- return maybeTruncate(`${prompt}:\n${numbered}\n`, maxOutputChars)
-}
-
-async function listDirectory(
- ctx: Context,
- target: FsTarget,
- maxOutputChars: number,
- exec: ToolRunContext,
-): Promise {
- async function visit(dir: FsTarget, depth: number): Promise {
- const entries = await ctx.fs.listDir(dir, exec.signal)
- const rows: string[] = []
- for (const entry of entries.filter(candidate =>
- !candidate.name.startsWith('.')
- && candidate.name !== 'node_modules'
- && candidate.name !== '__pycache__')) {
- const type = entry.type === 'directory' ? 'd' : entry.type === 'file' ? 'f' : '?'
- rows.push(`${type}\t${entry.target.displayPath}`)
- if (entry.type === 'directory' && depth < 2) {
- rows.push(...await visit(entry.target, depth + 1))
- }
- }
- return rows
- }
- const rows = [`d\t${target.displayPath}`, ...await visit(target, 1)]
- rows.sort((left, right) => {
- const leftPath = left.slice(left.indexOf('\t') + 1)
- const rightPath = right.slice(right.indexOf('\t') + 1)
- return codepointCompare(leftPath, rightPath)
- })
- const listing = maybeTruncate(rows.join('\n') + '\n', maxOutputChars)
- return `Here're the files and directories up to 2 levels deep in ${target.displayPath}, excluding hidden items, node_modules, and Python cache directories:\n${listing}\n`
-}
-
-async function viewPath(
- ctx: Context,
- path: string,
- viewRange: number[] | undefined,
- maxOutputChars: number,
- exec: ToolRunContext,
-): Promise {
- const target = await resolveTarget(ctx, path, exec.signal)
- const info = await statExisting(ctx, target, 'view', exec)
- if (info.type === 'directory') {
- if (viewRange !== undefined) {
- throw new Error('The `view_range` parameter is not allowed when `path` points to a directory.')
- }
- return listDirectory(ctx, target, maxOutputChars, exec)
- }
- if (info.type !== 'file') {
- throw new FsError(`cannot view "${target.displayPath}": not a regular file or directory`, 'FS_NOT_REGULAR_FILE')
- }
- const content = await ctx.fs.readText(target, exec.signal)
- ctx.emit('fs/observed', target, { kind: 'present', version: info.version }, exec)
- return formatFileView(target.displayPath, content, maxOutputChars, viewRange)
-}
-
-async function createFile(
- ctx: Context,
- policy: MutationPolicy,
- path: string,
- fileText: string | undefined,
- exec: ToolRunContext,
-): Promise {
- const content = requiredForCommand(fileText, 'file_text', 'create')
- const sandboxPolicy = policy.resolve(exec)
- const target = await resolveTarget(ctx, path, exec.signal)
- if (await ctx.fs.stat(target, exec.signal) !== undefined) {
- throw new Error(`File already exists at: ${target.displayPath}. Cannot overwrite files using command \`create\`.`)
- }
- const intent = await ctx.waterfall(
- 'fs/write-intent',
- target,
- exec,
- () => ({ kind: 'createIfAbsent' } as const),
- )
- let outcome
- try {
- outcome = await ctx.fs.writeText(
- target,
- content,
- intent,
- exec.signal,
- sandboxPolicy,
- )
- } catch (error: unknown) {
- throw policy.mapError(error, sandboxPolicy)
- }
- ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec)
- return `New file created successfully at: ${target.displayPath}`
-}
-
-async function replaceInFile(
- ctx: Context,
- policy: MutationPolicy,
- path: string,
- oldStr: string | undefined,
- newStr: string | null | undefined,
- exec: ToolRunContext,
-): Promise {
- if (newStr === null) {
- throw new Error('Parameter `new_str` must be omitted or contain a string for command: str_replace')
- }
- const sandboxPolicy = policy.resolve(exec)
- const target = await resolveTarget(ctx, path, exec.signal)
- const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)
- const oldValue = requiredForCommand(oldStr, 'old_str', 'str_replace', false)
- const newValue = newStr ?? ''
- const info = await statExisting(ctx, target, 'str_replace', exec)
- if (info.type !== 'file') {
- throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
- }
- const before = await ctx.fs.readText(target, exec.signal)
- const offsets = matchOffsets(before, oldValue)
- const offset = offsets[0]
- if (offset === undefined) {
- throw new FsError(
- `No replacement was performed, old_str \`${oldValue}\` did not appear verbatim in ${target.displayPath}.`,
- 'FS_EDIT_NOT_FOUND',
- )
- }
- if (offsets.length > 1) {
- const lines = lineNumbersAt(before, offsets)
- throw new FsError(
- `No replacement was performed. Multiple occurrences of old_str \`${oldValue}\` in lines [${lines.join(', ')}]. Please ensure it is unique`,
- 'FS_AMBIGUOUS_EDIT',
- )
- }
- let outcome
- try {
- outcome = await ctx.fs.writeText(
- target,
- before.slice(0, offset) + newValue + before.slice(offset + oldValue.length),
- intent === undefined
- ? { kind: 'replaceIfVersion', version: info.version }
- : { kind: 'replaceIfVersion', version: intent.version },
- exec.signal,
- sandboxPolicy,
- )
- } catch (error: unknown) {
- throw policy.mapError(error, sandboxPolicy)
- }
- ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec)
- return `The file ${target.displayPath} has been edited successfully.`
-}
-
-async function insertInFile(
- ctx: Context,
- policy: MutationPolicy,
- path: string,
- insertLine: number | undefined,
- newStr: string | undefined,
- exec: ToolRunContext,
-): Promise {
- if (insertLine === undefined) throw new Error('Parameter `insert_line` is required for command: insert')
- const value = requiredForCommand(newStr, 'new_str', 'insert')
- const sandboxPolicy = policy.resolve(exec)
- const target = await resolveTarget(ctx, path, exec.signal)
- const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)
- const info = await statExisting(ctx, target, 'insert', exec)
- if (info.type !== 'file') {
- throw new FsError(`cannot insert into "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
- }
- const before = await ctx.fs.readText(target, exec.signal)
- const lines = before.split('\n')
- if (!Number.isInteger(insertLine) || insertLine < 0 || insertLine > lines.length) {
- throw new Error(
- `Invalid \`insert_line\` parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${lines.length}]`,
- )
- }
- const after = [
- ...lines.slice(0, insertLine),
- ...value.split('\n'),
- ...lines.slice(insertLine),
- ].join('\n')
- const expected: FsWriteIntent = intent === undefined
- ? { kind: 'replaceIfVersion', version: info.version }
- : { kind: 'replaceIfVersion', version: intent.version }
- let outcome
- try {
- outcome = await ctx.fs.writeText(target, after, expected, exec.signal, sandboxPolicy)
- } catch (error: unknown) {
- throw policy.mapError(error, sandboxPolicy)
- }
- ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec)
- return `The file ${target.displayPath} has been edited successfully.`
-}
-
-interface ResolvedConfig {
- maxOutputChars: number
- description: string
-}
-
-function presentEditorCall(args: {
- command: 'view' | 'create' | 'str_replace' | 'insert'
- path: string
- file_text?: string | null
- insert_line?: number | null
- new_str?: string | null
- old_str?: string | null
-}): ToolCallView {
- switch (args.command) {
- case 'view':
- return {
- card: 'generic',
- title: `view ${args.path}`,
- kind: 'read',
- locations: [{ path: args.path }],
- }
- case 'create':
- return {
- card: 'diff',
- title: `create ${args.path}`,
- diffs: [{ path: args.path, oldText: null, newText: args.file_text ?? '' }],
- locations: [{ path: args.path }],
- }
- case 'str_replace':
- return {
- card: 'diff',
- title: `str_replace ${args.path}`,
- diffs: [{
- path: args.path,
- oldText: args.old_str ?? null,
- newText: args.new_str ?? '',
- }],
- locations: [{ path: args.path }],
- }
- case 'insert':
- return {
- card: 'generic',
- title: `insert ${args.path}`,
- kind: 'edit',
- locations: [{
- path: args.path,
- ...args.insert_line === undefined || args.insert_line === null
- ? {}
- : { line: Math.max(1, args.insert_line + 1) },
- }],
- }
- }
-}
-
-/** Register the model-facing `str_replace_editor` tool. */
-function registerStrReplaceEditor(ctx: Context, config: ResolvedConfig): void {
- const policy = new MutationPolicy(ctx)
- ctx.tools.register(defineTool({
- name: 'str_replace_editor',
- description: config.description,
- parameters: {
- command: {
- type: 'string',
- required: true,
- enum: ['view', 'create', 'str_replace', 'insert'],
- description: 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.',
- },
- path: {
- type: 'string',
- required: true,
- description: 'Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.',
- },
- file_text: {
- oneOf: [{ type: 'string' }, { type: 'null' }],
- description: 'Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter.',
- },
- insert_line: {
- oneOf: [{ type: 'integer' }, { type: 'null' }],
- description: 'Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter.',
- },
- new_str: {
- oneOf: [{ type: 'string' }, { type: 'null' }],
- description: 'Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter.',
- },
- old_str: {
- oneOf: [{ type: 'string' }, { type: 'null' }],
- description: 'Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter.',
- },
- view_range: {
- oneOf: [
- { type: 'array', items: { type: 'integer' } },
- { type: 'null' },
- ],
- description: 'Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
- },
- },
- output: {
- schema: { type: 'string' },
- render: (_args, value) => [{ type: 'text', text: value }],
- },
- async execute(args, exec) {
- switch (args.command) {
- case 'view':
- return viewPath(ctx, args.path, args.view_range ?? undefined, config.maxOutputChars, exec)
- case 'create':
- return createFile(ctx, policy, args.path, args.file_text ?? undefined, exec)
- case 'str_replace':
- return replaceInFile(
- ctx,
- policy,
- args.path,
- args.old_str ?? undefined,
- args.new_str,
- exec,
- )
- case 'insert':
- return insertInFile(
- ctx,
- policy,
- args.path,
- args.insert_line ?? undefined,
- args.new_str ?? undefined,
- exec,
- )
- }
- },
- presentCall: presentEditorCall,
- }))
-}
-
-export const name = 'tool-str-replace-editor'
-export const inject = ['tools', 'fs']
-
-/** Configuration for the string-replacement editor tool. */
-export interface Config {
- /** Maximum returned view characters before clipping (default 16000). */
- maxOutputChars?: number
- /** Model-facing tool description. */
- description?: string
-}
-
-/** Runtime configuration schema for the string-replacement editor tool. */
-export const Config: z = z.object({
- maxOutputChars: z.number().default(16_000),
- description: z.string().default(DEFAULT_DESCRIPTION),
-})
-
-/** Register one `str_replace_editor` tool over `ctx.fs`. */
-export function apply(ctx: Context, config: Config): void {
- const resolved: ResolvedConfig = {
- maxOutputChars: config.maxOutputChars ?? 16_000,
- description: config.description ?? DEFAULT_DESCRIPTION,
- }
- if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) {
- throw new Error('tool-str-replace-editor: maxOutputChars must be a positive safe integer')
- }
- if (resolved.description.trim().length === 0) {
- throw new Error('tool-str-replace-editor: description must be non-empty')
- }
- registerStrReplaceEditor(ctx, resolved)
-}
diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
deleted file mode 100644
index 0965db21d2..0000000000
--- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
+++ /dev/null
@@ -1,643 +0,0 @@
-import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
-import { join } from 'node:path'
-import { afterEach, describe, expect, it } from 'vitest'
-import { Context } from '@deepseek-ai/cordis'
-import { FsVersion } from '@deepseek-ai/dsh-fs'
-import { ToolCallId } from '@deepseek-ai/dsh-llm'
-import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
-import type { Agent } from '@deepseek-ai/dsh-agent'
-import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
-import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
-import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
-import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
-import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
-import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
-import ToolRuntime from '@deepseek-ai/dsh-tools'
-import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor'
-
-const contexts: Context[] = []
-const roots: string[] = []
-let callNumber = 0
-
-afterEach(async () => {
- for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
- for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
-})
-
-function agent(ctx: Context, cwd: string): Agent {
- const id = SessionId(`str-replace-editor-owner-${callNumber}`)
- const scope = ctx.plugin(() => {})
- const session = Session.create(id, [], { version: 0, id, createdAt: 0, cwd, isSeeded: false })
- const value: Agent = {
- id,
- options: {},
- session,
- inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
- status: 'idle',
- ctx: scope.ctx,
- send: () => {},
- followup: () => {},
- steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
- inject: () => {},
- cancel() {},
- runMaintenance: task => task(new AbortController().signal),
- whenIdle: () => Promise.resolve(),
- }
- ctx.agents.register(value)
- return value
-}
-
-function text(result: { content: { type: string; text?: string }[] }): string {
- return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
-}
-
-function call(ctx: Context, owner: Agent | undefined, args: unknown) {
- return ctx.tools.execute({
- signal: new AbortController().signal,
- callId: ToolCallId(`str-replace-editor-${++callNumber}`),
- name: 'str_replace_editor',
- arguments: args,
- ...owner === undefined ? {} : { agent: owner },
- })
-}
-
-async function setup(
- config: ToolStrReplaceEditor.Config = {},
- options: { fsPolicy?: boolean; sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access' } = {},
-) {
- const root = await mkdtemp(join(tmpdir(), 'dsh-tool-str-replace-editor-'))
- roots.push(root)
- const ctx = new Context()
- contexts.push(ctx)
- await ctx.plugin(SystemPrompt)
- await ctx.plugin(ToolRuntime)
- await ctx.plugin(AgentRegistry)
- if (options.sandboxMode === undefined) {
- await ctx.plugin(LocalFileSystem, { cwd: root })
- } else {
- // SandboxPolicy declares the registry as a required injection; mount it
- // before the policy activates.
- await ctx.plugin(SessionProjectionRegistry)
- await ctx.plugin(SandboxPolicy, { mode: options.sandboxMode, workspaceRoot: root })
- await ctx.plugin(SandboxedFileSystem, { cwd: root })
- }
- if (options.fsPolicy === true) await ctx.plugin(FsPolicy)
- const fiber = await ctx.plugin(ToolStrReplaceEditor, config)
- return { ctx, root, fiber, owner: agent(ctx, root) }
-}
-
-describe('tool-str-replace-editor', () => {
- it('registers the standalone schema and configurable description', async () => {
- const { ctx, fiber } = await setup({ description: 'custom editor description' })
- const schema = ctx.tools.schemas()[0]
- expect(ctx.tools.schemas().map(item => item.name)).toEqual(['str_replace_editor'])
- expect(schema?.description).toBe('custom editor description')
- const properties = (schema?.parameters as {
- properties: Record
- }).properties
- expect(properties).not.toHaveProperty('replace_all')
- expect(properties.file_text?.oneOf?.map(option => option.type)).toEqual(['string', 'null'])
- expect(properties.insert_line?.oneOf?.map(option => option.type)).toEqual(['integer', 'null'])
- expect(properties.new_str?.oneOf?.map(option => option.type)).toEqual(['string', 'null'])
- expect(properties.old_str?.oneOf?.map(option => option.type)).toEqual(['string', 'null'])
- expect(properties.view_range?.oneOf?.map(option => option.type)).toEqual(['array', 'null'])
- expect(properties.view_range?.oneOf?.[0]?.items?.type).toBe('integer')
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'view',
- path: '/workspace/a.txt',
- file_text: null,
- insert_line: null,
- new_str: null,
- old_str: null,
- view_range: null,
- })).toMatchObject({
- card: 'generic',
- kind: 'read',
- locations: [{ path: '/workspace/a.txt' }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'create',
- path: '/workspace/a.txt',
- file_text: 'hello',
- insert_line: null,
- new_str: null,
- old_str: null,
- view_range: null,
- })).toMatchObject({
- card: 'diff',
- diffs: [{ path: '/workspace/a.txt', oldText: null, newText: 'hello' }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'str_replace',
- path: '/workspace/a.txt',
- old_str: 'old',
- new_str: 'new',
- file_text: null,
- insert_line: null,
- view_range: null,
- })).toMatchObject({
- card: 'diff',
- diffs: [{ path: '/workspace/a.txt', oldText: 'old', newText: 'new' }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'insert',
- path: '/workspace/a.txt',
- insert_line: null,
- new_str: 'x',
- })).toMatchObject({
- card: 'generic',
- kind: 'edit',
- locations: [{ path: '/workspace/a.txt' }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'insert',
- path: '/workspace/a.txt',
- insert_line: 0,
- new_str: 'x',
- file_text: null,
- old_str: null,
- view_range: null,
- })).toMatchObject({
- card: 'generic',
- kind: 'edit',
- locations: [{ path: '/workspace/a.txt', line: 1 }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'create',
- path: '/workspace/empty.txt',
- })).toMatchObject({
- diffs: [{ path: '/workspace/empty.txt', oldText: null, newText: '' }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'str_replace',
- path: '/workspace/a.txt',
- })).toMatchObject({
- diffs: [{ path: '/workspace/a.txt', oldText: null, newText: '' }],
- })
- expect(ctx.tools.get('str_replace_editor')?.presentCall?.({
- command: 'insert',
- path: '/workspace/a.txt',
- })).toMatchObject({
- locations: [{ path: '/workspace/a.txt' }],
- })
-
- await fiber.dispose()
- expect(ctx.tools.schemas()).toEqual([])
- expect(ctx.tools.get('str_replace_editor')).toBeUndefined()
- })
-
- it('creates, views, replaces, and inserts with the canonical model-facing output', async () => {
- const { ctx, root, owner } = await setup()
- const sample = join(root, 'sample.txt')
- expect(text(await call(ctx, owner, {
- command: 'create',
- path: sample,
- file_text: 'one\ntwo\nthree\n',
- insert_line: null,
- new_str: null,
- old_str: null,
- view_range: null,
- }))).toBe(`New file created successfully at: ${sample}`)
-
- expect(text(await call(ctx, owner, {
- command: 'view',
- path: sample,
- file_text: null,
- insert_line: null,
- new_str: null,
- old_str: null,
- view_range: null,
- }))).toContain(' 2 two')
-
- expect(text(await call(ctx, owner, {
- command: 'view',
- path: sample,
- view_range: [2, -1],
- }))).toBe([
- `Here's the content of ${sample} with line numbers (which has a total of 4 lines) with view_range=[2, -1]:`,
- ' 2 two',
- ' 3 three',
- ' 4 ',
- '',
- ].join('\n'))
-
- expect(text(await call(ctx, owner, {
- command: 'str_replace',
- path: sample,
- old_str: 'two',
- new_str: 'TWO',
- file_text: null,
- insert_line: null,
- view_range: null,
- }))).toBe(`The file ${sample} has been edited successfully.`)
- expect(text(await call(ctx, owner, {
- command: 'str_replace',
- path: sample,
- old_str: 'TWO',
- }))).toBe(`The file ${sample} has been edited successfully.`)
- expect(text(await call(ctx, owner, {
- command: 'insert',
- path: sample,
- insert_line: 1,
- new_str: 'between',
- file_text: null,
- old_str: null,
- view_range: null,
- }))).toBe(`The file ${sample} has been edited successfully.`)
- expect(await readFile(sample, 'utf8')).toBe('one\nbetween\n\nthree\n')
- })
-
- it('a failed view records absence so create can recover after external deletion', async () => {
- const { ctx, root, owner } = await setup({}, { fsPolicy: true })
- const sample = join(root, 'deleted.txt')
- await writeFile(sample, 'original')
- expect((await call(ctx, owner, { command: 'view', path: sample })).isError).toBe(false)
- await rm(sample)
-
- const missing = await call(ctx, owner, { command: 'view', path: sample })
- expect(missing.isError).toBe(true)
- expect(missing.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
-
- const edit = await call(ctx, owner, {
- command: 'str_replace',
- path: sample,
- old_str: 'original',
- new_str: 'edited',
- })
- expect(edit.isError).toBe(true)
- expect(edit.error).toMatchObject({ info: { code: 'FS_NOT_FOUND' } })
-
- const created = await call(ctx, owner, {
- command: 'create',
- path: sample,
- file_text: 'fresh',
- })
- expect(created.isError).toBe(false)
- expect(await readFile(sample, 'utf8')).toBe('fresh')
- })
-
- it('writes replacement text literally', async () => {
- const { ctx, root, owner } = await setup()
- const sample = join(root, 'literal.txt')
- const replacement = "$&|$`|$'|$$"
- await writeFile(sample, 'before OLD after')
-
- expect((await call(ctx, owner, {
- command: 'str_replace',
- path: sample,
- old_str: 'OLD',
- new_str: replacement,
- })).isError).toBe(false)
- expect(await readFile(sample, 'utf8')).toBe(`before ${replacement} after`)
- })
-
- it('lists visible entries to depth two and clips at the configured view limit', async () => {
- const { ctx, root, owner } = await setup({ maxOutputChars: 10_000 })
- await mkdir(join(root, 'dir', 'nested', 'third'), { recursive: true })
- await mkdir(join(root, 'dir', 'node_modules', 'pkg'), { recursive: true })
- await mkdir(join(root, 'dir', 'node_modules_old'), { recursive: true })
- await mkdir(join(root, 'dir', '__pycache__'), { recursive: true })
- await mkdir(join(root, 'dir', '__pycache__backup'), { recursive: true })
- await writeFile(join(root, 'dir', 'visible.txt'), 'ok')
- await writeFile(join(root, 'dir', '.hidden'), 'hidden')
- await writeFile(join(root, 'dir', 'nested', 'child.txt'), 'child')
- await writeFile(join(root, 'dir', 'nested', 'third', 'too-deep.txt'), 'deep')
- await writeFile(join(root, 'dir', 'node_modules', 'pkg', 'index.js'), 'hidden dependency')
- await writeFile(join(root, 'dir', 'node_modules_old', 'kept.js'), 'visible source')
- await writeFile(join(root, 'dir', '__pycache__', 'module.pyc'), 'cache')
- await writeFile(join(root, 'dir', '__pycache__backup', 'kept.py'), 'visible source')
- const listDir = ctx.fs.listDir.bind(ctx.fs)
- const otherTarget = await ctx.fs.resolve(join(root, 'dir', 'other'))
- ctx.fs.listDir = async (target, signal) => {
- const entries = await listDir(target, signal)
- return target.displayPath === join(root, 'dir')
- ? [
- { name: 'same-target', type: 'other', target: otherTarget },
- { name: 'other', type: 'other', target: otherTarget },
- ...entries.toReversed(),
- ]
- : entries
- }
-
- const listing = text(await call(ctx, owner, { command: 'view', path: join(root, 'dir') }))
- expect(listing).not.toContain('.hidden')
- expect(listing).not.toContain('too-deep.txt')
- expect(listing).not.toContain('index.js')
- expect(listing).not.toContain('module.pyc')
- // The listing carries absolute display paths; the POSIX-style substrings
- // only match on Linux, so assert with platform separators.
- expect(listing).toContain(join('node_modules_old', 'kept.js'))
- expect(listing).toContain(join('__pycache__backup', 'kept.py'))
-
- const clipped = await setup({ maxOutputChars: 10 })
- await writeFile(join(clipped.root, 'large.txt'), 'x'.repeat(100))
- expect(text(await call(clipped.ctx, clipped.owner, {
- command: 'view',
- path: join(clipped.root, 'large.txt'),
- })))
- .toContain('')
- })
-
- it('matches canonical empty-line, range, and end-insert behavior', async () => {
- const { ctx, root, owner } = await setup()
- const empty = join(root, 'empty.txt')
- const newline = join(root, 'newline.txt')
- const plain = join(root, 'plain.txt')
- await writeFile(empty, '')
- await writeFile(newline, '\n')
- await writeFile(plain, 'one\ntwo')
-
- expect(text(await call(ctx, owner, { command: 'view', path: empty })))
- .toContain('(which has a total of 1 lines):\n 1 \n')
- expect(text(await call(ctx, owner, { command: 'view', path: newline })))
- .toContain('(which has a total of 2 lines):\n 1 \n 2 \n')
- expect(text(await call(ctx, owner, {
- command: 'view',
- path: plain,
- view_range: [1, 2],
- }))).toContain(' 2 two')
- expect(text(await call(ctx, undefined, {
- command: 'view',
- path: plain,
- }))).toContain(' 1 one')
- expect((await call(ctx, undefined, {
- command: 'create',
- path: join(root, 'ownerless.txt'),
- file_text: 'ownerless',
- })).isError).toBe(false)
-
- await call(ctx, owner, {
- command: 'insert',
- path: plain,
- insert_line: 2,
- new_str: 'three',
- })
- expect(await readFile(plain, 'utf8')).toBe('one\ntwo\nthree')
-
- await writeFile(newline, 'one\n')
- await call(ctx, owner, {
- command: 'insert',
- path: newline,
- insert_line: 2,
- new_str: 'three',
- })
- expect(await readFile(newline, 'utf8')).toBe('one\n\nthree')
- })
-
- it('uses old_str-only replacement failures and rejects relative paths', async () => {
- const { ctx, root, owner } = await setup()
- const ambiguous = join(root, 'ambiguous.txt')
- await writeFile(ambiguous, 'same\nother\nsame')
-
- const missing = await call(ctx, owner, {
- command: 'str_replace',
- path: ambiguous,
- old_str: 'absent',
- new_str: 'x',
- })
- expect(missing.isError).toBe(true)
- expect(text(missing)).toContain(`old_str \`absent\` did not appear verbatim in ${ambiguous}`)
- expect(text(missing)).not.toContain('old_string')
-
- const repeated = await call(ctx, owner, {
- command: 'str_replace',
- path: ambiguous,
- old_str: 'same',
- new_str: 'x',
- })
- expect(repeated.isError).toBe(true)
- expect(text(repeated)).toContain('Multiple occurrences of old_str `same` in lines [1, 3]')
- expect(text(repeated)).not.toContain('replace_all')
-
- await writeFile(ambiguous, 'alpha\nbeta\nmiddle\nalpha\nbeta')
- const repeatedMultiline = await call(ctx, owner, {
- command: 'str_replace',
- path: ambiguous,
- old_str: 'alpha\nbeta',
- new_str: 'x',
- })
- expect(text(repeatedMultiline))
- .toContain('Multiple occurrences of old_str `alpha\nbeta` in lines [1, 4]')
-
- const mixedEol = join(root, 'mixed-eol.txt')
- await writeFile(mixedEol, 'alpha\r\nbeta\nmiddle\nalpha\nbeta')
- expect((await call(ctx, owner, {
- command: 'str_replace',
- path: mixedEol,
- old_str: 'alpha\r\nbeta',
- new_str: 'replaced',
- })).isError).toBe(false)
- expect(await readFile(mixedEol, 'utf8')).toBe('replaced\nmiddle\nalpha\nbeta')
-
- const relative = await call(ctx, owner, { command: 'view', path: 'ambiguous.txt' })
- expect(relative.isError).toBe(true)
- expect(text(relative)).toContain('is not an absolute path')
- expect(await readFile(ambiguous, 'utf8')).toBe('alpha\nbeta\nmiddle\nalpha\nbeta')
- })
-
- it('reports invalid commands or arguments without mutating files', async () => {
- const { ctx, root, owner } = await setup()
- const ambiguous = join(root, 'ambiguous.txt')
- const empty = join(root, 'empty.txt')
- const trailingNewline = join(root, 'trailing-newline.txt')
- const threeLines = join(root, 'three-lines.txt')
- const directory = join(root, 'directory')
- await writeFile(ambiguous, 'same same')
- await writeFile(empty, '')
- await writeFile(trailingNewline, 'one\n')
- await writeFile(threeLines, 'one\ntwo\nthree')
- await mkdir(directory)
-
- const cases = [
- { command: null, path: ambiguous },
- { command: 'view', path: null },
- { command: 'view', path: '' },
- { command: 'view', path: join(root, 'missing.txt') },
- { command: 'view', path: ambiguous, view_range: [1] },
- { command: 'view', path: ambiguous, view_range: [0, 1] },
- { command: 'view', path: ambiguous, view_range: [1.5, 2] },
- { command: 'view', path: threeLines, view_range: [1, 99] },
- { command: 'view', path: threeLines, view_range: [2, 1] },
- { command: 'view', path: directory, view_range: [1, 1] },
- { command: 'create', path: join(root, 'new.txt') },
- { command: 'create', path: join(root, 'new.txt'), file_text: null },
- { command: 'create', path: ambiguous, file_text: 'overwrite' },
- { command: 'str_replace', path: ambiguous, new_str: 'x' },
- { command: 'str_replace', path: ambiguous, old_str: null, new_str: 'x' },
- { command: 'str_replace', path: ambiguous, old_str: 'same same', new_str: null },
- { command: 'str_replace', path: ambiguous, old_str: '', new_str: 'x' },
- { command: 'insert', path: ambiguous, new_str: 'x' },
- { command: 'insert', path: ambiguous, insert_line: null, new_str: 'x' },
- { command: 'insert', path: ambiguous, insert_line: 0, new_str: null },
- { command: 'insert', path: ambiguous, insert_line: -1, new_str: 'x' },
- { command: 'insert', path: ambiguous, insert_line: 1.5, new_str: 'x' },
- { command: 'insert', path: ambiguous, insert_line: 99, new_str: 'x' },
- { command: 'insert', path: empty, insert_line: 2, new_str: 'x' },
- { command: 'insert', path: directory, insert_line: 0, new_str: 'x' },
- ]
- for (const args of cases) {
- expect((await call(ctx, owner, args)).isError).toBe(true)
- }
- expect(await readFile(ambiguous, 'utf8')).toBe('same same')
-
- ctx.fs.stat = async () => ({ version: FsVersion('special'), type: 'other' })
- const special = await call(ctx, owner, { command: 'view', path: join(root, 'special') })
- expect(special.isError).toBe(true)
- expect(special.error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } })
- expect((await call(ctx, owner, {
- command: 'str_replace',
- path: join(root, 'special'),
- old_str: 'x',
- new_str: 'y',
- })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } })
- expect((await call(ctx, owner, {
- command: 'insert',
- path: join(root, 'special'),
- insert_line: 0,
- new_str: 'x',
- })).error).toMatchObject({ info: { code: 'FS_NOT_REGULAR_FILE' } })
- })
-
- it('delegates read-before-edit decisions to fs-observation-policy', async () => {
- const { ctx, root, owner } = await setup({}, { fsPolicy: true })
- const existing = join(root, 'existing.txt')
- const created = join(root, 'created.txt')
- await writeFile(existing, 'before')
-
- const blindEdit = await call(ctx, owner, {
- command: 'str_replace',
- path: existing,
- old_str: 'before',
- new_str: 'after',
- })
- expect(blindEdit.error).toMatchObject({ info: { code: 'FS_NOT_OBSERVED' } })
- expect(await readFile(existing, 'utf8')).toBe('before')
-
- await call(ctx, owner, { command: 'view', path: existing })
- expect((await call(ctx, owner, {
- command: 'str_replace',
- path: existing,
- old_str: 'before',
- new_str: 'after',
- })).isError).toBe(false)
- expect(await readFile(existing, 'utf8')).toBe('after')
-
- expect((await call(ctx, owner, {
- command: 'insert',
- path: existing,
- insert_line: 1,
- new_str: 'tail',
- })).isError).toBe(false)
- expect(await readFile(existing, 'utf8')).toBe('after\ntail')
-
- expect((await call(ctx, owner, {
- command: 'create',
- path: created,
- file_text: 'new',
- })).isError).toBe(false)
- expect(await readFile(created, 'utf8')).toBe('new')
- })
-
- it('passes the session sandbox policy to every mutation', async () => {
- const { ctx, root, owner } = await setup({}, { sandboxMode: 'read-only' })
- const path = join(root, 'blocked.txt')
- const result = await call(ctx, owner, {
- command: 'create',
- path,
- file_text: 'blocked',
- })
- expect(result.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } })
- expect(text(result)).toContain('[sandbox: file access denied under read-only mode]')
-
- const ownerless = await call(ctx, undefined, {
- command: 'create',
- path: join(root, 'ownerless-blocked.txt'),
- file_text: 'blocked',
- })
- expect(ownerless.error).toMatchObject({ info: { code: 'FS_SANDBOX_DENIED' } })
- })
-
- it('preserves tabs outside the edited region', async () => {
- const { ctx, root, owner } = await setup()
- const path = join(root, 'Makefile')
- await writeFile(path, 'target:\n\told\nremove\n')
- expect(text(await call(ctx, owner, { command: 'view', path })))
- .toContain(' 2 \told')
- await call(ctx, owner, {
- command: 'str_replace',
- path,
- old_str: '\told',
- new_str: '\tnew',
- })
- await call(ctx, owner, {
- command: 'str_replace',
- path,
- old_str: 'remove\n',
- })
- await call(ctx, owner, {
- command: 'insert',
- path,
- insert_line: 1,
- new_str: '\tkept',
- })
- expect(await readFile(path, 'utf8')).toBe('target:\n\tkept\n\tnew\n')
- })
-
- it('reports missing sandbox-policy composition during plugin startup', async () => {
- const root = await mkdtemp(join(tmpdir(), 'dsh-tool-str-replace-editor-missing-policy-'))
- roots.push(root)
- const ctx = new Context()
- contexts.push(ctx)
- await ctx.plugin(SystemPrompt)
- await ctx.plugin(ToolRuntime)
- await ctx.plugin(AgentRegistry)
- await ctx.plugin(LocalFileSystem, { cwd: root })
- Object.defineProperty(ctx.fs, 'sandboxMode', { value: 'read-only' })
-
- await expect(ctx.plugin(ToolStrReplaceEditor))
- .rejects.toThrow('the mounted filesystem confines but ctx.sandboxPolicy is missing')
- })
-
- it('maps unexpected backend write failures for replace and insert', async () => {
- const { ctx, root, owner } = await setup()
- const path = join(root, 'backend-error.txt')
- await writeFile(path, 'old\n')
- const failWrite = async (): Promise => {
- throw new Error('backend write failed')
- }
- ctx.fs.writeText = failWrite
-
- const replace = await call(ctx, owner, {
- command: 'str_replace',
- path,
- old_str: 'old',
- new_str: 'new',
- })
- expect(replace.isError).toBe(true)
- expect(text(replace)).toContain('backend write failed')
-
- const insert = await call(ctx, owner, {
- command: 'insert',
- path,
- insert_line: 1,
- new_str: 'new',
- })
- expect(insert.isError).toBe(true)
- expect(text(insert)).toContain('backend write failed')
- })
-
- it('rejects invalid plugin config', () => {
- expect(() => {
- ToolStrReplaceEditor.apply(new Context(), { maxOutputChars: 0 })
- }).toThrow('maxOutputChars must be a positive safe integer')
- expect(() => {
- ToolStrReplaceEditor.apply(new Context(), { description: ' ' })
- }).toThrow('description must be non-empty')
- })
-})
diff --git a/packages/fs/tool-str-replace-editor/tsconfig.json b/packages/fs/tool-str-replace-editor/tsconfig.json
deleted file mode 100644
index b9dba81d5d..0000000000
--- a/packages/fs/tool-str-replace-editor/tsconfig.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "extends": "../../../tsconfig.base.json",
- "compilerOptions": {
- "rootDir": "src",
- "outDir": "lib/types"
- },
- "include": ["src"],
- "references": [
- { "path": "../../../vendor/cordis" },
- { "path": "../../../vendor/schemastery" },
- { "path": "../../core/tools" },
- { "path": "../fs" },
- { "path": "../../sandbox/sandbox" },
- { "path": "../../sandbox/sandbox-policy" }
- ]
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4388263d21..08cdf72672 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -298,9 +298,6 @@ importers:
'@deepseek-ai/dsh-tool-skill':
specifier: workspace:^
version: link:../../packages/skill/tool-skill
- '@deepseek-ai/dsh-tool-str-replace-editor':
- specifier: workspace:^
- version: link:../../packages/fs/tool-str-replace-editor
'@deepseek-ai/dsh-tool-subagent':
specifier: workspace:^
version: link:../../packages/subagent/tool-subagent
@@ -1242,9 +1239,6 @@ importers:
'@deepseek-ai/dsh-tool-skill':
specifier: workspace:^
version: link:../../skill/tool-skill
- '@deepseek-ai/dsh-tool-str-replace-editor':
- specifier: workspace:^
- version: link:../../fs/tool-str-replace-editor
'@deepseek-ai/dsh-tool-subagent':
specifier: workspace:^
version: link:../../subagent/tool-subagent
@@ -5324,52 +5318,6 @@ importers:
specifier: workspace:^
version: link:../../core/tools
- packages/fs/tool-str-replace-editor:
- dependencies:
- '@deepseek-ai/schemastery':
- specifier: link:../../../vendor/schemastery
- version: link:../../../vendor/schemastery
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
- '@deepseek-ai/dsh-agent':
- specifier: workspace:^
- version: link:../../core/agent
- '@deepseek-ai/dsh-fs':
- specifier: workspace:^
- version: link:../fs
- '@deepseek-ai/dsh-fs-local':
- specifier: workspace:^
- version: link:../fs-local
- '@deepseek-ai/dsh-fs-observation-policy':
- specifier: workspace:^
- version: link:../fs-observation-policy
- '@deepseek-ai/dsh-fs-sandbox':
- specifier: workspace:^
- version: link:../fs-sandbox
- '@deepseek-ai/dsh-llm':
- specifier: workspace:^
- version: link:../../llm/llm
- '@deepseek-ai/dsh-sandbox':
- specifier: workspace:^
- version: link:../../sandbox/sandbox
- '@deepseek-ai/dsh-sandbox-policy':
- specifier: workspace:^
- version: link:../../sandbox/sandbox-policy
- '@deepseek-ai/dsh-session':
- specifier: workspace:^
- version: link:../../core/session
- '@deepseek-ai/dsh-session-projection':
- specifier: workspace:^
- version: link:../../session/session-projection
- '@deepseek-ai/dsh-system-prompt':
- specifier: workspace:^
- version: link:../../core/system-prompt
- '@deepseek-ai/dsh-tools':
- specifier: workspace:^
- version: link:../../core/tools
-
packages/goal/command-goal:
devDependencies:
'@deepseek-ai/cordis':
@@ -10037,9 +9985,6 @@ importers:
'@deepseek-ai/dsh-tool-skill':
specifier: workspace:^
version: link:../../packages/skill/tool-skill
- '@deepseek-ai/dsh-tool-str-replace-editor':
- specifier: workspace:^
- version: link:../../packages/fs/tool-str-replace-editor
'@deepseek-ai/dsh-tool-subagent':
specifier: workspace:^
version: link:../../packages/subagent/tool-subagent
diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json
index 97c1a6bc68..762619c5d1 100644
--- a/python/sdk-runtime/package.json
+++ b/python/sdk-runtime/package.json
@@ -110,7 +110,6 @@
"@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^",
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
"@deepseek-ai/dsh-tool-skill": "workspace:^",
- "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tool-todo": "workspace:^",
diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts
index 972eadc1b8..4535e3fef6 100644
--- a/scripts/gen-tool-catalog.ts
+++ b/scripts/gen-tool-catalog.ts
@@ -48,7 +48,6 @@ import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
-import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor'
import TerminalSessionService from '@deepseek-ai/dsh-terminal'
import * as ToolPty from '@deepseek-ai/dsh-tool-terminal'
import * as ToolGoal from '@deepseek-ai/dsh-tool-goal'
@@ -298,19 +297,6 @@ const TOOL_PACKAGES: ToolPackage[] = [
note:
'One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description.',
},
- {
- pkg: '@deepseek-ai/dsh-tool-str-replace-editor',
- dir: 'tool-str-replace-editor',
- source: 'packages/fs/tool-str-replace-editor/src/index.ts',
- requires: ['ctx.tools', 'ctx.fs'],
- writes: ['tool/call', 'fs/observed after view presence/absence, edit absence, or successful mutation', 'tool/result'],
- async mount(ctx) {
- await ctx.plugin(LocalFileSystem)
- await ctx.plugin(ToolStrReplaceEditor)
- },
- note:
- 'Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.',
- },
{
pkg: '@deepseek-ai/dsh-tool-fs',
dir: 'tool-fs',
diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py
index b1969ab5b9..66e9221cae 100644
--- a/scripts/smoke-python-runtime.py
+++ b/scripts/smoke-python-runtime.py
@@ -79,7 +79,6 @@ LEGACY_CUSTOM_DISABLED_ROWS = (
"tool-goal",
"tool-ralph",
"tool-skill",
- "tool-str-replace-editor",
"tool-subagent-control",
"tool-subagent-list-agents",
"tool-subagent-fork",
diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json
index 716b08cd1d..dd7a2da87e 100644
--- a/snapshots/sdk/bash-tool/tool-schemas.expected.json
+++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.",
diff --git a/snapshots/sdk/persistent-tools/cordis.snapshot.yml b/snapshots/sdk/persistent-tools/cordis.snapshot.yml
deleted file mode 100644
index 0579e3881d..0000000000
--- a/snapshots/sdk/persistent-tools/cordis.snapshot.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# Replay layer for the TypeScript SDK minimal customization patch.
-
-- id: llm-deepseek
- name: '@deepseek-ai/dsh-llm-deepseek'
- disabled: true
-
-- insert:
- - id: llm-replay
- name: '@deepseek-ai/dsh-llm-replay'
- config:
- providers:
- - id: deepseek-official
- name: DeepSeek
- models:
- - id: deepseek-v4-flash
diff --git a/snapshots/sdk/persistent-tools/cordis.yml b/snapshots/sdk/persistent-tools/cordis.yml
deleted file mode 100644
index c78756cb69..0000000000
--- a/snapshots/sdk/persistent-tools/cordis.yml
+++ /dev/null
@@ -1,158 +0,0 @@
-# Per-launch SDK customization: retain only persistent Bash and the string
-# editor as model-facing tools, with process-local providers and an explicit
-# danger-full-access / never-ask policy.
-
-- id: llm-deepseek
- name: '@deepseek-ai/dsh-llm-deepseek'
- config:
- apiKeyEnv: DEEPSEEK_API_KEY
- streamIdleTimeoutMs: 172800000
- models:
- - id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash'
- contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000)
-
-- id: sandbox-policy
- name: '@deepseek-ai/dsh-sandbox-policy'
- config:
- mode: danger-full-access
- workspaceRoot: !!js process.cwd()
-
-- id: approval
- name: '@deepseek-ai/dsh-user-approval'
- config:
- policy: never
-
-- id: permission
- name: '@deepseek-ai/dsh-permission-presets'
- disabled: true
-
-- id: bash-sandbox
- name: '@deepseek-ai/dsh-bash-sandbox'
- disabled: true
-
-- id: pwsh-sandbox
- name: '@deepseek-ai/dsh-pwsh-sandbox'
- disabled: true
-
-- id: fs-sandbox
- name: '@deepseek-ai/dsh-fs-sandbox'
- disabled: true
-
-- id: tool-bash
- name: '@deepseek-ai/dsh-tool-bash'
- disabled: true
-
-- id: tool-pwsh
- name: '@deepseek-ai/dsh-tool-pwsh'
- disabled: true
-
-- id: agent-instructions
- name: '@deepseek-ai/dsh-agent-instructions'
- disabled: true
-
-- id: skill
- name: '@deepseek-ai/dsh-skill'
- disabled: true
-
-- id: skill-filesystem
- name: '@deepseek-ai/dsh-skill-filesystem'
- disabled: true
-
-- id: tool-skill
- name: '@deepseek-ai/dsh-tool-skill'
- disabled: true
-
-- id: tool-jobs
- name: '@deepseek-ai/dsh-tool-jobs'
- disabled: true
-
-- id: tool-fs
- name: '@deepseek-ai/dsh-tool-fs'
- disabled: true
-
-- id: tool-fs-search
- name: '@deepseek-ai/dsh-tool-fs-search'
- disabled: true
-
-- id: tool-subagent-control
- name: '@deepseek-ai/dsh-tool-subagent-control'
- disabled: true
-
-- id: tool-subagent-list-agents
- name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
- disabled: true
-
-- id: tool-subagent
- name: '@deepseek-ai/dsh-tool-subagent'
- disabled: true
-
-- id: tool-subagent-fork
- name: '@deepseek-ai/dsh-tool-subagent'
- disabled: true
-
-- id: tool-workflow
- name: '@deepseek-ai/dsh-tool-workflow'
- disabled: true
-
-- id: tool-todo
- name: '@deepseek-ai/dsh-tool-todo'
- disabled: true
-
-- id: tool-goal
- name: '@deepseek-ai/dsh-tool-goal'
- disabled: true
-
-- id: tool-ralph
- name: '@deepseek-ai/dsh-tool-ralph'
- disabled: true
-
-- id: tool-web
- name: '@deepseek-ai/dsh-tool-web'
- disabled: true
-
-- id: plan-mode
- name: '@deepseek-ai/dsh-plan-mode'
- disabled: true
-
-- id: system-prompt
- name: '@deepseek-ai/dsh-system-prompt'
- config:
- includeHarnessIdentity: false
- persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'
-
-- id: session-persistence-jsonl
- name: '@deepseek-ai/dsh-session-persistence-jsonl'
- config:
- root: !!js dshHomePath('sessions')
- compression: none
-
-- insert:
- - id: bash-local
- name: '@deepseek-ai/dsh-bash-local'
-
- - id: fs-local
- name: '@deepseek-ai/dsh-fs-local'
- config:
- cwd: !!js process.cwd()
-
- - id: terminal
- name: '@deepseek-ai/dsh-terminal'
-
- - id: terminal-bash
- name: '@deepseek-ai/dsh-terminal-bash'
- config:
- timeoutMs: 300000
-
- - id: persistent-bash
- name: '@deepseek-ai/dsh-tool-bash-persistent'
- config:
- timeoutMs: 300000
- description: |-
- Run commands in a bash shell
- * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
- * You don't have access to the internet via this tool.
- * You do have access to a mirror of common linux and python packages via apt and pip.
- * State is persistent across command calls and discussions with the user.
- * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
- * Please avoid commands that may produce a very large amount of output.
- * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
diff --git a/snapshots/sdk/persistent-tools/notifications.expected.jsonl b/snapshots/sdk/persistent-tools/notifications.expected.jsonl
deleted file mode 100644
index fe4872b440..0000000000
--- a/snapshots/sdk/persistent-tools/notifications.expected.jsonl
+++ /dev/null
@@ -1,89 +0,0 @@
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}}
-{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":6,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":26,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":1,"step":2}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":28,"time":0,"data":{"turn":1,"step":3}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-delayed-pipeline","name":"bash","argumentsDelta":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":37,"time":0,"data":{"turn":1,"step":3}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":38,"time":0,"data":{"turn":1,"step":4}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":44,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":45,"time":0,"data":{"turn":1,"step":4,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":47,"time":0,"data":{"turn":1,"step":4}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":48,"time":0,"data":{"turn":1,"step":5}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":53,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":54,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":55,"time":0,"data":{"turn":1,"step":5,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":56,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":57,"time":0,"data":{"turn":1,"step":5}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":58,"time":0,"data":{"turn":1,"step":6}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":65,"time":0,"data":{"turn":1,"step":6,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":66,"time":0,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":67,"time":0,"data":{"turn":1,"step":6}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":68,"time":0,"data":{"turn":1,"step":7}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":70,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":71,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":72,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":73,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":74,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":75,"time":0,"data":{"turn":1,"step":7,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":76,"time":0,"data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[75],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":77,"time":0,"data":{"turn":1,"step":7}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":78,"time":0,"data":{"turn":1,"step":8}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":79,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":80,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":81,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":82,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":83,"time":0,"data":{"turn":1,"step":8,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":84,"time":0,"data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[79,80,81,82,83],"surfaceOp":"append"}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":85,"time":0,"data":{"turn":1,"step":8}}}}
-{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":86,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}}
-{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}}
diff --git a/snapshots/sdk/persistent-tools/result.expected.json b/snapshots/sdk/persistent-tools/result.expected.json
deleted file mode 100644
index 53a1342f7d..0000000000
--- a/snapshots/sdk/persistent-tools/result.expected.json
+++ /dev/null
@@ -1 +0,0 @@
-{"sessionId":"{{sessionId}}","finalResponse":"PERSISTENT_TOOLS_OK"}
diff --git a/snapshots/sdk/persistent-tools/session.jsonl b/snapshots/sdk/persistent-tools/session.jsonl
deleted file mode 100644
index b382309331..0000000000
--- a/snapshots/sdk/persistent-tools/session.jsonl
+++ /dev/null
@@ -1,88 +0,0 @@
-{"type":"session","version":0,"id":"{{session:1}}","createdAt":1785331618309,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
-{"type":"turn/start","data":{"turn":1}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","data":{"turn":1,"step":1}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Prove that bash state persists. After the persistence checks, run exactly `{ sleep 0.1; echo delayed; } | cat` as its own bash call and observe `delayed`. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
-{"type":"session/title","data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}
-{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
-{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":1}}
-{"type":"step/start","data":{"turn":1,"step":2}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":2}}
-{"type":"step/start","data":{"turn":1,"step":3}}
-{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-delayed-pipeline","name":"bash","argumentsDelta":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":3,"callId":"bash-delayed-pipeline","name":"bash","arguments":"{\"command\":\"{ sleep 0.1; echo delayed; } | cat\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"bash-delayed-pipeline"},"content":[{"type":"tool-result","toolCallId":"bash-delayed-pipeline","content":[{"type":"text","text":"delayed"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":3}}
-{"type":"step/start","data":{"turn":1,"step":4}}
-{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":4,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\",\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}
-{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{message:10}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":4}}
-{"type":"step/start","data":{"turn":1,"step":5}}
-{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":5,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":null,\"insert_line\":null,\"new_str\":null,\"old_str\":null,\"view_range\":null}"}}
-{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{message:12}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":5}}
-{"type":"step/start","data":{"turn":1,"step":6}}
-{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":6,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\",\"file_text\":null,\"insert_line\":null,\"view_range\":null}"}}
-{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{message:14}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":6}}
-{"type":"step/start","data":{"turn":1,"step":7}}
-{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:15}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"}
-{"type":"tool/call","data":{"turn":1,"step":7,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{message:16}}"}},"sourceEventSeqs":[75],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":7}}
-{"type":"step/start","data":{"turn":1,"step":8}}
-{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","data":{"turn":1,"step":8,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:17}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[79,80,81,82,83],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":1,"step":8}}
-{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/snapshots/sdk/persistent-tools/snapshot.yml b/snapshots/sdk/persistent-tools/snapshot.yml
deleted file mode 100644
index b4876f0b17..0000000000
--- a/snapshots/sdk/persistent-tools/snapshot.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-version: 1
-scenario: persistent-tools
-profile: sdk
-composition: sdk-minimal
-recording: live
-header:
- class: sdk-minimal
- pin: true
-workspace:
- final: true
diff --git a/snapshots/sdk/persistent-tools/system-prompt.expected.md b/snapshots/sdk/persistent-tools/system-prompt.expected.md
deleted file mode 100644
index 3066e65687..0000000000
--- a/snapshots/sdk/persistent-tools/system-prompt.expected.md
+++ /dev/null
@@ -1 +0,0 @@
-You are the environment-selected minimal software engineer.
diff --git a/snapshots/sdk/persistent-tools/tool-schemas.expected.json b/snapshots/sdk/persistent-tools/tool-schemas.expected.json
deleted file mode 100644
index 73234c5e49..0000000000
--- a/snapshots/sdk/persistent-tools/tool-schemas.expected.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "initial": [
- {
- "name": "bash",
- "description": "Run commands in a bash shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* You do have access to a mirror of common linux and python packages via apt and pip.\n* State is persistent across command calls and discussions with the user.\n* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The bash command to run. Relative path is preferred in the command."
- }
- },
- "required": [
- "command"
- ]
- }
- },
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- }
- ],
- "changes": []
-}
diff --git a/snapshots/sdk/persistent-tools/workspace.expected/note.txt b/snapshots/sdk/persistent-tools/workspace.expected/note.txt
deleted file mode 100644
index 86b943efe7..0000000000
--- a/snapshots/sdk/persistent-tools/workspace.expected/note.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-target:
- new
diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts
index 50ececf76a..3095c36575 100644
--- a/snapshots/sdk/sdk.snapshot.ts
+++ b/snapshots/sdk/sdk.snapshot.ts
@@ -54,16 +54,6 @@ import {
const corpusRoot = fileURLToPath(new URL('../', import.meta.url))
-const MINIMAL_SYSTEM_PROMPT = 'You are the environment-selected minimal software engineer.'
-const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
-* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
-* You don't have access to the internet via this tool.
-* You do have access to a mirror of common linux and python packages via apt and pip.
-* State is persistent across command calls and discussions with the user.
-* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
-* Please avoid commands that may produce a very large amount of output.
-* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.`
-
const mode = process.env.DSH_SNAPSHOT ?? 'replay'
const recording = mode === 'record'
const refreshing = mode === 'refresh'
@@ -112,16 +102,6 @@ const SDK_ASSERTIONS: Readonly> = {
'subagent-dsh-sdk-diagnostic': {
environment: { DSH_TEST_CHILD_PATCH: dshSdkDiagnosticChildPatch },
},
- 'persistent-tools': {
- environment: { DSH_SYSTEM_PROMPT: MINIMAL_SYSTEM_PROMPT },
- expectedTools: { bash: ['command'], str_replace_editor: ['command', 'path'] },
- expectedSystem: MINIMAL_SYSTEM_PROMPT,
- expectedToolDescriptions: { bash: MINIMAL_BASH_DESCRIPTION },
- runtimeContext: {
- includes: ['Current DSH file policy: danger-full-access', 'Approval prompts are disabled in this session'],
- excludes: ['workspace-write'],
- },
- },
'subagent-dsh-sdk-dynamic-route': {
environment: { DSH_TEST_PARENT_PROVIDER: 'deepseek-official' },
dshSdkChild: {
diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json
index 87cbb941a1..1531215c7f 100644
--- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json
+++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json
index 87cbb941a1..1531215c7f 100644
--- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json
+++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json
index ee4ad0e6b6..ba63548517 100644
--- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json
+++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json
index c21ad8a0f5..e7899283b4 100644
--- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json
+++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json
index 9cc82c6fb5..73c4ae509a 100644
--- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json
+++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json
@@ -374,91 +374,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.",
diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json
index 87cbb941a1..1531215c7f 100644
--- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json
+++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json
index 87cbb941a1..1531215c7f 100644
--- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json
+++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json
index 716b08cd1d..dd7a2da87e 100644
--- a/snapshots/sdk/text-turn/tool-schemas.expected.json
+++ b/snapshots/sdk/text-turn/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.",
diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json
index a3e1ee5238..40661b4472 100644
--- a/snapshots/session/agent-instructions/tool-schemas.expected.json
+++ b/snapshots/session/agent-instructions/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
@@ -1074,91 +989,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md
index 22f4a66ba2..5c3c43a2e7 100644
--- a/snapshots/session/both-mode-turn/system-prompt.expected.md
+++ b/snapshots/session/both-mode-turn/system-prompt.expected.md
@@ -173,23 +173,6 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record;
- /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */
- str_replace_editor: {
- /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */
- command: "view" | "create" | "str_replace" | "insert";
- /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */
- path: string;
- /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */
- file_text?: string | null;
- /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */
- insert_line?: number | null;
- /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */
- new_str?: string | null;
- /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */
- old_str?: string | null;
- /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */
- view_range?: number[] | null;
- } & Record;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
@@ -459,7 +442,6 @@ interface ToolOutputMap {
};
content: string;
};
- str_replace_editor: string;
subagent: {
kind: "background";
jobId: string;
diff --git a/snapshots/session/both-mode-turn/tool-schemas.expected.json b/snapshots/session/both-mode-turn/tool-schemas.expected.json
index b1a1179d18..fcaa91628e 100644
--- a/snapshots/session/both-mode-turn/tool-schemas.expected.json
+++ b/snapshots/session/both-mode-turn/tool-schemas.expected.json
@@ -378,91 +378,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json
index a3e1ee5238..40661b4472 100644
--- a/snapshots/session/compaction-recovery/tool-schemas.expected.json
+++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
@@ -1074,91 +989,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md
index 8f0f468911..6bd67f58ab 100644
--- a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md
+++ b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md
@@ -340,23 +340,6 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record;
- /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */
- str_replace_editor: {
- /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */
- command: "view" | "create" | "str_replace" | "insert";
- /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */
- path: string;
- /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */
- file_text?: string | null;
- /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */
- insert_line?: number | null;
- /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */
- new_str?: string | null;
- /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */
- old_str?: string | null;
- /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */
- view_range?: number[] | null;
- } & Record;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
@@ -645,7 +628,6 @@ interface ToolOutputMap {
};
content: string;
};
- str_replace_editor: string;
subagent: {
kind: "background";
jobId: string;
diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json
index fbb01942ab..fe1f6ed30a 100644
--- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json
+++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json
@@ -575,91 +575,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json
index 0dd86f7f77..5d3cd77ac0 100644
--- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json
+++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json
@@ -261,91 +261,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json
index def66aefbd..393fe158eb 100644
--- a/snapshots/session/lsp-definition/tool-schemas.expected.json
+++ b/snapshots/session/lsp-definition/tool-schemas.expected.json
@@ -394,91 +394,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json
index 232c2778f2..a20d2a132f 100644
--- a/snapshots/session/product-subagent-both/tool-schemas.expected.json
+++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json
index e8877690ec..f28699d048 100644
--- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json
+++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json
index 8303bc5845..9ba36a6336 100644
--- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json
+++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md
index 375b87b4e0..f9eaff62cb 100644
--- a/snapshots/session/ptc-python-turn/system-prompt.expected.md
+++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md
@@ -366,23 +366,6 @@ class SkillOutput(TypedDict):
resourceBase: NotRequired[SkillOutputResourceBase1 | SkillOutputResourceBase2 | SkillOutputResourceBase3]
content: str
-class StrReplaceEditorArgs(TypedDict):
- # The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.
- command: Literal["view", "create", "str_replace", "insert"]
- # Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.
- path: str
- # Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter.
- file_text: NotRequired[str | None]
- # Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter.
- insert_line: NotRequired[int | None]
- # Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter.
- new_str: NotRequired[str | None]
- # Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter.
- old_str: NotRequired[str | None]
- # Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.
- view_range: NotRequired[list[int] | None]
- # Additional keys beyond those declared are allowed.
-
class SubagentArgs(TypedDict):
# A short (3-5 word) description of the delegated task, for display.
description: str
@@ -607,8 +590,6 @@ class Tools(Protocol):
"""Send a message to a direct continuable child by its agent id. If you are a resident continuable child, you may also target your direct parent. If the target is still working, the message steers its nearest step; if it is idle, the message starts a turn. This call returns no answer from the agent — only confirmation that the message was delivered. A failure means the message was NOT delivered."""
async def skill(self, args: SkillArgs) -> SkillOutput:
"""Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill."""
- async def str_replace_editor(self, args: StrReplaceEditorArgs) -> str:
- """Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str`"""
async def subagent(self, args: SubagentArgs) -> SubagentOutput1 | SubagentOutput2 | SubagentOutput3:
"""Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result."""
async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput1 | SubagentForkOutput2 | SubagentForkOutput3:
diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md
index 004a681296..672242d92e 100644
--- a/snapshots/session/ptc-read-image/system-prompt.expected.md
+++ b/snapshots/session/ptc-read-image/system-prompt.expected.md
@@ -175,23 +175,6 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record;
- /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */
- str_replace_editor: {
- /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */
- command: "view" | "create" | "str_replace" | "insert";
- /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */
- path: string;
- /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */
- file_text?: string | null;
- /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */
- insert_line?: number | null;
- /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */
- new_str?: string | null;
- /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */
- old_str?: string | null;
- /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */
- view_range?: number[] | null;
- } & Record;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
@@ -461,7 +444,6 @@ interface ToolOutputMap {
};
content: string;
};
- str_replace_editor: string;
subagent: {
kind: "background";
jobId: string;
diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md
index 5c43f1c4db..dd648445e1 100644
--- a/snapshots/session/ptc-turn/system-prompt.expected.md
+++ b/snapshots/session/ptc-turn/system-prompt.expected.md
@@ -175,23 +175,6 @@ interface ToolArgsMap {
/** The exact skill name from the available skills list. */
name: string;
} & Record;
- /** Custom editing tool for viewing, creating and editing files * State is persistent across command calls and discussions with the user * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep * The `create` command cannot be used if the specified `path` already exists as a file * If a `command` generates a long output, it will be truncated and marked with `` * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match Notes for using the `str_replace` command: * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique * The `new_str` parameter should contain the edited lines that should replace the `old_str` */
- str_replace_editor: {
- /** The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`. */
- command: "view" | "create" | "str_replace" | "insert";
- /** Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. */
- path: string;
- /** Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter. */
- file_text?: string | null;
- /** Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter. */
- insert_line?: number | null;
- /** Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter. */
- new_str?: string | null;
- /** Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter. */
- old_str?: string | null;
- /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */
- view_range?: number[] | null;
- } & Record;
/** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result. */
subagent: {
/** A short (3-5 word) description of the delegated task, for display. */
@@ -461,7 +444,6 @@ interface ToolOutputMap {
};
content: string;
};
- str_replace_editor: string;
subagent: {
kind: "background";
jobId: string;
diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json
index fd18c5805a..3c9ef95371 100644
--- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json
+++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json
index 08dce4ee40..1feee5af52 100644
--- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json
+++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "structured_output",
"description": "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.",
diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json
index 08dce4ee40..1feee5af52 100644
--- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json
+++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "structured_output",
"description": "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.",
diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json
index 5d8b78fc63..e59600ed46 100644
--- a/snapshots/session/session-query-spill/tool-schemas.expected.json
+++ b/snapshots/session/session-query-spill/tool-schemas.expected.json
@@ -561,91 +561,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json
index cec2b16ef8..cb0c104048 100644
--- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json
+++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json
index ed2d4fa46f..0cbbda81fe 100644
--- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json
+++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json
@@ -420,91 +420,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json
index 87cbb941a1..1531215c7f 100644
--- a/snapshots/session/text-turn/tool-schemas.expected.json
+++ b/snapshots/session/text-turn/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json
index aa3f6cabd5..ed38c64b2b 100644
--- a/snapshots/session/web-fetch/tool-schemas.expected.json
+++ b/snapshots/session/web-fetch/tool-schemas.expected.json
@@ -357,91 +357,6 @@
]
}
},
- {
- "name": "str_replace_editor",
- "description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with ``\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
- "enum": [
- "view",
- "create",
- "str_replace",
- "insert"
- ]
- },
- "path": {
- "type": "string",
- "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
- },
- "file_text": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "insert_line": {
- "oneOf": [
- {
- "type": "integer"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "new_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
- },
- "old_str": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
- },
- "view_range": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "integer"
- }
- },
- {
- "type": "null"
- }
- ],
- "description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
- }
- },
- "required": [
- "command",
- "path"
- ]
- }
- },
{
"name": "subagent",
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` steers the child's nearest step while it is running and starts a turn while it is idle. Set `run_in_background: false` only when your next action depends on receiving the result.",
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 68aab92144..ea2868ce93 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -394,7 +394,6 @@
"@deepseek-ai/dsh-tool-ralph": ["./packages/workflow/tool-ralph/src"],
"@deepseek-ai/dsh-tool-session-query": ["./packages/session-query/tool-session-query/src"],
"@deepseek-ai/dsh-tool-skill": ["./packages/skill/tool-skill/src"],
- "@deepseek-ai/dsh-tool-str-replace-editor": ["./packages/fs/tool-str-replace-editor/src"],
"@deepseek-ai/dsh-tool-subagent": ["./packages/subagent/tool-subagent/src"],
"@deepseek-ai/dsh-tool-subagent/invariant": ["./packages/subagent/tool-subagent/src/invariant.ts"],
"@deepseek-ai/dsh-tool-subagent-control": ["./packages/subagent/tool-subagent-control/src"],
diff --git a/tsconfig.host.json b/tsconfig.host.json
index d2a033266d..7f79fe8c98 100644
--- a/tsconfig.host.json
+++ b/tsconfig.host.json
@@ -248,7 +248,6 @@
{ "path": "./packages/fs/fs-sandbox" },
{ "path": "./packages/fs/tool-fs" },
{ "path": "./packages/fs/tool-fs-search" },
- { "path": "./packages/fs/tool-str-replace-editor" },
{ "path": "./packages/compaction/compaction" },
{ "path": "./packages/compaction/compaction-basic" },
{ "path": "./packages/compaction/command-compact" },