Procházet zdrojové kódy

feat(sdk): disable str_replace_editor by default

fz před 1 týdnem
rodič
revize
965adbb5cf

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.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-22-single-dsh-application-launcher.md
-2026-08-22-single-dsh-application-launcher.md: 5729ed770b1d26969079e6341d2d2da4b17d0b90
-2026-08-22-single-dsh-application-launcher.zh.md: 45d5b9758fd1f01057de0f34e580249b63f9877e
+2026-08-22-single-dsh-application-launcher.md: 7fcb33d8d5c0a9979f3cc5724c2211bf9a1e55a1
+2026-08-22-single-dsh-application-launcher.zh.md: ac91a7b854042b5f974931aa69fffc91fd91319d

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md

@@ -34,7 +34,7 @@ Profile manifests own patch reload:
 
 Custom profiles default to `live`. A startup profile still applies its bundle, profile, home-level, and invocation `--patch` layers, but it does not watch them after boot. `dsh-base` inserts the module-HMR row disabled; a profile with a tested source-module reload lifecycle must enable it explicitly. None of the shipped profiles enable server module HMR: `patchReload: live` uses the launcher's config-only watcher while the startup profiles install no watcher. SDK and ACP cannot safely replace their server, agents, persistence, or tool registry inside one owned stdio connection.
 
-The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Full-profile model-visible tool and persistence defaults come from `dsh-base`; `sdk-minimal` owns its explicit defaults. Runnable snapshots own the assembled application outputs.
+The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Full-profile model-visible tool and persistence defaults start from `dsh-base`; the SDK application applies its [default editor selection](../simplification/2026-09-05-sdk-default-file-editor.md), while `sdk-minimal` owns its explicit defaults. Runnable snapshots own the assembled application outputs.
 
 ### TypeScript SDK customization
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md

@@ -34,7 +34,7 @@ Profile manifest 负责 patch 重载:
 
 自定义 profile 默认为 `live`。`startup` profile 仍会应用组合包、profile、home 级与调用时 `--patch` 各层,但启动后不会监视这些文件。`dsh-base` 插入的模块 HMR(热模块替换)配置项默认禁用;具有经过验证的源码模块重载生命周期的 profile 必须显式启用它。随附 profile 均不启用服务器模块 HMR:`patchReload: live` 使用启动器的仅配置 watcher,`startup` profile 则不安装 watcher。SDK 与 ACP 无法在一个自有 stdio 连接内安全替换其服务器、agent、持久化或工具注册表。
 
-随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。完整 profile 的模型可见工具与持久化默认值来自 `dsh-base`;`sdk-minimal` 拥有自己的显式默认值。可运行快照负责固定已组装的应用输出。
+随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。完整 profile 的模型可见工具与持久化默认值以 `dsh-base` 为基础;SDK 应用采用其[默认编辑器选择](../simplification/2026-09-05-sdk-default-file-editor.zh.md),而 `sdk-minimal` 拥有自己的显式默认值。可运行快照负责固定已组装的应用输出。
 
 ### TypeScript SDK 自定义
 

+ 6 - 0
.agents/notes/implemented/simplification/2026-09-05-sdk-default-file-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-05-sdk-default-file-editor.md
+2026-09-05-sdk-default-file-editor.md: ca1d321fb3562f0ea06fca6e4a4a146f922996d3
+2026-09-05-sdk-default-file-editor.zh.md: 9d54e839953f430826622681ab17bc56664e7dff

+ 29 - 0
.agents/notes/implemented/simplification/2026-09-05-sdk-default-file-editor.md

@@ -0,0 +1,29 @@
+# Agent Note: SDK default file editor selection
+
+Status: implemented
+
+English | [中文](2026-09-05-sdk-default-file-editor.zh.md)
+
+## Problem
+
+The standard SDK profile inherits both `read`/`write`/`edit` and `str_replace_editor` from the shared base. These offer overlapping file editing interfaces, while the Web standard preset selects the former. [Issue #3599](https://github.com/deepseek-harness/deepseek-harness/issues/3599) requests the same editor choice for the standard SDK without making all SDK and Web behavior identical.
+
+## Decision
+
+The [SDK application patch](../../../../packages/bundle/sdk-app/cordis.patch.yml) disables the inherited `tool-str-replace-editor` row. The SDK retains `read`, `write`, and `edit`; the shared base, reusable editor package, and standalone `sdk-minimal` composition retain their own defaults. A trusted profile, home, or invocation patch can explicitly enable the row.
+
+This narrows the SDK tool default described by [one dsh launcher](../architecture/2026-08-22-single-dsh-application-launcher.md). That decision remains active for launch ownership, shared services, and patch precedence; no active note is fully superseded.
+
+## Alternatives considered
+
+**Keep both editors enabled.** Shared base composition explains their availability, but the standard SDK does not need two default interfaces for the same file editing operations. Callers requiring this editor can select it explicitly.
+
+**Remove the shared registration or reuse the entire Web preset.** Either changes more than the requested SDK default. Keeping the change in the SDK bundle preserves other profiles and the existing application architecture.
+
+## Consequences
+
+Default SDK requests omit the editor schema. Callers that select `str_replace_editor` by name need an explicit patch or their existing dedicated composition. This decision makes no claim of complete SDK/Web tool parity.
+
+## Verification
+
+The [SDK profile process test](../../../../apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts) captures actual model requests for the default roster, explicit editor enablement, and standalone minimal roster. [SDK recorded sessions](../../../../snapshots/sdk/sdk.snapshot.ts) pin the resulting schemas; shared cross-profile and persistent-editor recordings explicitly retain their required editor.

+ 29 - 0
.agents/notes/implemented/simplification/2026-09-05-sdk-default-file-editor.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: SDK 默认文件编辑器选择
+
+Status: implemented
+
+[English](2026-09-05-sdk-default-file-editor.md) | 中文
+
+## Problem
+
+标准 SDK profile 从共享 base 同时继承 `read`/`write`/`edit` 和 `str_replace_editor`。这些工具提供重叠的文件编辑接口,而 Web standard preset 选择前者。[Issue #3599](https://github.com/deepseek-harness/deepseek-harness/issues/3599) 要求标准 SDK 采用相同的编辑器选择,不要求 SDK 与 Web 的全部行为一致。
+
+## Decision
+
+[SDK 应用 patch](../../../../packages/bundle/sdk-app/cordis.patch.yml) 禁用继承的 `tool-str-replace-editor` 配置项。SDK 保留 `read`、`write` 和 `edit`;共享 base、可复用编辑器包及独立 `sdk-minimal` 组合保留各自的默认值。受信任的 profile、home 或逐次调用 patch 可以显式启用该配置项。
+
+本决策收窄了[统一 dsh 启动器](../architecture/2026-08-22-single-dsh-application-launcher.zh.md)所述的 SDK 工具默认值。该决策对启动所有权、共享服务和 patch 优先级仍然有效;没有被完全取代的活跃 Agent Note。
+
+## Alternatives considered
+
+**继续默认启用两个编辑器。** 共享 base 组合解释了它们为何可用,但标准 SDK 无需为相同的文件编辑操作默认提供两套接口。需要此编辑器的调用方可以显式选择它。
+
+**移除共享注册或复用整个 Web preset。** 两者都会扩大 SDK 默认值调整的范围。将改动放在 SDK bundle 中可以保留其他 profile 和现有应用架构。
+
+## Consequences
+
+默认 SDK 请求省略该编辑器 schema。按名称选择 `str_replace_editor` 的调用方需要显式 patch 或其现有专用组合。本决策不承诺 SDK/Web 的全部工具一致。
+
+## Verification
+
+[SDK profile 进程测试](../../../../apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts) 捕获默认工具清单、显式启用编辑器和独立极简工具清单的实际模型请求。[SDK 录制会话](../../../../snapshots/sdk/sdk.snapshot.ts) 固定最终 schema;跨 profile 共享录制与持久编辑器录制显式保留其所需的编辑器。

+ 20 - 7
apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts

@@ -1,5 +1,5 @@
 import { createServer } from 'node:http'
-import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
+import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
@@ -46,11 +46,14 @@ function waitForLine(
 
 describe('Python SDK dsh profile keyless smoke', () => {
   it.each([
-    { label: 'reports max-token turns with the default mapping config', envValue: undefined },
-    { label: 'reports max-token turns with mapping enabled through env', envValue: 'true' },
-    { label: 'reports max-token turns with mapping disabled through env', envValue: 'false' },
-  ])('$label', async ({ envValue }) => {
+    { label: 'reports max-token turns with the default mapping config', envValue: undefined, editorEnabled: false },
+    { label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
+    { label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
+    { label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
+  ])('$label', async ({ envValue, editorEnabled }) => {
     const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
+    const editorPatch = join(root, 'editor.patch.yml')
+    if (editorEnabled) await writeFile(editorPatch, '- id: tool-str-replace-editor\n  disabled: false\n')
     const modelRequests: Record<string, unknown>[] = []
     const modelServer = createServer((request, response) => {
       let body = ''
@@ -76,6 +79,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
       binScript,
       '--profile',
       'sdk',
+      ...(editorEnabled ? ['--patch', editorPatch] : []),
     ], {
       cwd: repoRoot,
       env: {
@@ -154,7 +158,8 @@ describe('Python SDK dsh profile keyless smoke', () => {
       const toolNames = tools.map(tool => tool.function?.name)
       expect(modelRequests[0]?.reasoning_effort).toBe('max')
       expect(modelRequests[0]?.max_tokens).toBe(1234)
-      expect(toolNames).toEqual(expect.arrayContaining(['web_fetch', 'web_search']))
+      expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search']))
+      expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
       expect(toolNames).not.toContain('list_subagent_models')
 
       child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
@@ -180,9 +185,13 @@ describe('Python SDK dsh profile keyless smoke', () => {
 
   it('boots the standalone minimal profile through its generated manifest', async () => {
     const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-'))
+    const modelRequests: Record<string, unknown>[] = []
     const modelServer = createServer((request, response) => {
-      request.resume()
+      let body = ''
+      request.setEncoding('utf8')
+      request.on('data', (chunk: string) => { body += chunk })
       request.on('end', () => {
+        modelRequests.push(JSON.parse(body) as Record<string, unknown>)
         response.writeHead(200, { 'content-type': 'text/event-stream' })
         response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n')
         response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
@@ -249,6 +258,10 @@ describe('Python SDK dsh profile keyless smoke', () => {
         bundles: ['@deepseek-ai/dsh-sdk-minimal'],
         patchReload: 'startup',
       })
+      const tools = modelRequests[0]?.tools as { function?: { name?: string } }[]
+      expect(tools.map(tool => tool.function?.name).sort()).toEqual(
+        [process.platform === 'win32' ? 'pwsh' : 'bash', 'str_replace_editor'].sort(),
+      )
 
       child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
       await waitForLine(lines, value => value.id === 3, () => stderr)

+ 2 - 2
packages/bundle/sdk-app/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/bundle/sdk-app/README.md
-README.md: c83ff2178ebc3dd059c926270564306af0828b72
-README.zh.md: 9b25cbfde70f23309b2dc78a237043d0e8101528
+README.md: c00409984ca627897a54bd68d249a29a7856b91d
+README.zh.md: 5d5713d8e452c0a1ee550306f62967711c5fcdcc

+ 3 - 1
packages/bundle/sdk-app/README.md

@@ -31,6 +31,8 @@ The startup provider binds stdin EOF to the launcher's bounded successful shutdo
 
 `DSH_MAX_TOKENS_AS_SUCCESS` retains the SDK deployment mapping: unset or JSON `true` reports token-limited subagent completion as accepted, while JSON `false` reports it as an error. Provider/model and workspace cwd arrive through the SDK initialization request; the base profile owns adapters, tools, persistence, policy, settings, and credentials.
 
+A profile, home, or invocation patch can set `tool-str-replace-editor` to `disabled: false` to enable it explicitly. The standalone `sdk-minimal` profile owns its separate tool selection.
+
 -----
 
 <a id="model-experience"></a>
@@ -40,7 +42,7 @@ The startup provider binds stdin EOF to the launcher's bounded successful shutdo
 
 #### What the model sees
 
-The profile supplies `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.` before the base tool and context contributions. The exact SDK initialization route and session cwd resolve the placeholders.
+The profile supplies `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.` before the base tool and context contributions. The exact SDK initialization route and session cwd resolve the placeholders. Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`.
 
 #### Token effect
 

+ 3 - 1
packages/bundle/sdk-app/README.zh.md

@@ -31,6 +31,8 @@ kind: "package-bundle"
 
 `DSH_MAX_TOKENS_AS_SUCCESS` 保留 SDK 部署映射:未设置或 JSON `true` 把 token 达限的 subagent 完成报告为已接受,JSON `false` 则报告为错误。模型提供方/模型与工作区 cwd 通过 SDK 初始化请求传入;base profile 拥有适配器、工具、持久化、策略、settings 与 credentials。
 
+profile、home 或逐次调用 patch 可以把 `tool-str-replace-editor` 设置为 `disabled: false`,显式启用它。独立的 `sdk-minimal` profile 自行决定其工具选择。
+
 -----
 
 <a id="model-experience"></a>
@@ -40,7 +42,7 @@ kind: "package-bundle"
 
 #### 模型看到什么
 
-profile 会在 base 工具与上下文贡献之前提供 `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.`。确切的 SDK 初始化路由与会话 cwd 会解析其中的占位符。
+profile 会在 base 工具与上下文贡献之前提供 `You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.`。确切的 SDK 初始化路由与会话 cwd 会解析其中的占位符。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。
 
 #### Token 影响
 

+ 3 - 0
packages/bundle/sdk-app/cordis.patch.yml

@@ -8,6 +8,9 @@
 - id: session-title-llm
   disabled: true
 
+- id: tool-str-replace-editor
+  disabled: true
+
 - insert:
     - id: sdk-app-startup
       name: '@deepseek-ai/dsh-sdk-app'

+ 0 - 85
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 `<response clipped>`\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.",

+ 3 - 0
snapshots/sdk/persistent-tools/cordis.yml

@@ -2,6 +2,9 @@
 # editor as model-facing tools, with process-local providers and an explicit
 # danger-full-access / never-ask policy.
 
+- id: tool-str-replace-editor
+  disabled: false
+
 - id: llm-deepseek
   name: '@deepseek-ai/dsh-llm-deepseek'
   config:

+ 0 - 85
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 `<response clipped>`\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.",

+ 0 - 85
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 `<response clipped>`\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.",

+ 0 - 85
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 `<response clipped>`\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.",

+ 0 - 85
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 `<response clipped>`\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.",

+ 4 - 0
snapshots/session/text-turn/cordis.yml

@@ -2,6 +2,10 @@
 # The shipped profile owns application startup; this patch fixes only the
 # deterministic test composition and raw persistence needed for recording.
 
+# Shared recordings keep their editor across headless and SDK profile defaults.
+- id: tool-str-replace-editor
+  disabled: false
+
 - id: llm-deepseek
   name: '@deepseek-ai/dsh-llm-deepseek'
   config: