From b56042399506a4d26c0774ed21b900d3cbfa2edf Mon Sep 17 00:00:00 2001 From: LogosVeritas Date: Tue, 13 Oct 2026 11:25:18 +0800 Subject: [PATCH] fix(minimal): configure tools and complete image offload recovery --- ...st-plane-ownership-after-presets.i18n.yaml | 4 +- ...8-10-host-plane-ownership-after-presets.md | 2 +- ...0-host-plane-ownership-after-presets.zh.md | 2 +- ...-24-preset-scoped-schedule-tools.i18n.yaml | 4 +- ...2026-09-24-preset-scoped-schedule-tools.md | 2 +- ...6-09-24-preset-scoped-schedule-tools.zh.md | 2 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 4 +- apps/cli/reference/README.zh.md | 4 +- apps/cli/tests/built-bin.e2e.ts | 1 + .../tests/profiles/sdk/keyless-smoke.e2e.ts | 37 +++++++++++---- apps/cli/tests/web-agent-presets.e2e.ts | 2 - .../section.expected.md | 2 +- docs/config-catalog.md | 10 +++- docs/config-catalog.zh.md | 10 +++- .../guide.i18n.yaml | 4 +- .../sdk-minimal-image-compaction/guide.md | 8 +++- .../sdk-minimal-image-compaction/guide.zh.md | 8 +++- packages/bundle/sdk-minimal/README.i18n.yaml | 8 ++-- packages/bundle/sdk-minimal/README.md | 4 +- packages/bundle/sdk-minimal/README.zh.md | 4 +- packages/bundle/sdk-minimal/cordis.patch.yml | 11 +++-- packages/bundle/sdk-minimal/package.json | 1 + .../sdk-minimal/tests/sdk-minimal.spec.ts | 5 +- .../bundle/web-app/presets/minimal.patch.yml | 8 ++-- .../compaction-image-offload/README.i18n.yaml | 4 +- .../compaction-image-offload/README.md | 2 +- .../compaction-image-offload/README.zh.md | 2 +- .../compaction-image-offload/src/index.ts | 4 +- .../tests/image-offload.spec.ts | 42 +++++++++++++++++ packages/fs/tool-fs/README.i18n.yaml | 4 +- packages/fs/tool-fs/README.md | 9 +++- packages/fs/tool-fs/README.zh.md | 9 +++- packages/fs/tool-fs/src/index.ts | 28 ++++++----- packages/fs/tool-fs/tests/integration.spec.ts | 15 ++++++ packages/fs/tool-fs/tests/read-image.spec.ts | 23 ++++++--- packages/fs/tool-fs/tests/tools.spec.ts | 30 ++++-------- pnpm-lock.yaml | 3 ++ python/sdk/README.i18n.yaml | 8 ++-- python/sdk/README.md | 4 +- python/sdk/README.zh.md | 4 +- .../notifications.expected.jsonl | 9 ++-- .../minimal-image-compaction/session.v4.jsonl | 3 ++ .../tool-schemas.expected.json | 47 +++++++++++++++++++ snapshots/sdk/sdk.snapshot.ts | 4 ++ 45 files changed, 288 insertions(+), 117 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml index 6512532f4a..6401383a39 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.i18n.yaml @@ -18,5 +18,5 @@ en: ca7c9088d0793e1e zh: ea648bac9e726c64 /agent-note-what-stays-host-plane-once-presets-own-the-agent-plane/consequences: - en: de26cde94adda770 - zh: 7f39760b77958966 + en: 0a426923c2a1f774 + zh: 29626804b99b35ff diff --git a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md index 546423f32e..620692ee9f 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md +++ b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md @@ -38,6 +38,6 @@ Projection key presence is not a per-session capability signal ([session project ## Consequences -The context meter becomes a per-session fact instead of a function of mount history. A preset can no longer opt out of token accounting; no shipped preset did, and `minimal` now says it drops auto-compaction rather than the accounting. +The context meter is a per-session fact independent of mount history. Presets resolve the host meter for token accounting while selecting their own compaction backend; `minimal` supports both automatic compaction and the human `/compact` command. Production entry points must compose Agents explicitly; installing the registry alone does not bind them. An unjoined Agent receives no tools, prompt sections, or skills and reports no error. diff --git a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md index 59d318b410..325758c769 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.zh.md @@ -38,6 +38,6 @@ Status: implemented ## 后果 -context meter 成为逐会话的事实,而不再是挂载历史的函数。代价是 preset 不能再选择不做 token 记账;随附的 preset 没有一个这么做,`minimal` 现在也写明它放弃的是自动压缩而非记账。 +context meter 是独立于挂载历史的逐会话事实。Preset 使用宿主 meter 进行 token 记账,并选择自己的压缩后端;`minimal` 同时支持自动压缩和用户 `/compact` 命令。 生产入口必须显式组装 Agent;仅安装注册表不会自动绑定。未加入的 Agent 不会获得工具、提示词段落或 skill,也不会报告错误。 diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml index 7c2ed58b01..a642f015e9 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.i18n.yaml @@ -18,5 +18,5 @@ en: 546d83d28487d1c1 zh: 438922cd4f3ff9ec /agent-note-preset-scoped-schedule-tools/testing: - en: 1c5f34cf30a24fd7 - zh: 6f7a372de52d0ecd + en: 10c37e2219b5f7d2 + zh: 6e9901a8957feaa4 diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md index c0b1b34412..af0f94f867 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md +++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.md @@ -31,4 +31,4 @@ English | [中文](2026-09-24-preset-scoped-schedule-tools.zh.md) ## Testing -`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` pins the four definitions and their error mapping, including `refuses every tool for a delegated child caller`. `apps/cli/tests/web-agent-presets.e2e.ts` asserts the `minimal` preset's tool list, and `snapshots/web/minimal-preset/tool-schemas.expected.json` records its bash-only tool table. `packages/schedule/schedule/tests/subagent-ownership.spec.ts` pins the delegation-depth refusal, and `scripts/optional-bundles.spec.ts` asserts that both delegation rows in the three presets deny all four tool names. +`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` pins the four definitions and their error mapping, including `refuses every tool for a delegated child caller`. `apps/cli/tests/web-agent-presets.e2e.ts` asserts the `minimal` preset's tool list, and `snapshots/web/minimal-preset/tool-schemas.expected.json` records its tool table without reminder schemas. `packages/schedule/schedule/tests/subagent-ownership.spec.ts` pins the delegation-depth refusal, and `scripts/optional-bundles.spec.ts` asserts that both delegation rows in the three presets deny all four tool names. diff --git a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md index bb9a8c0894..31543e13dd 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-preset-scoped-schedule-tools.zh.md @@ -31,4 +31,4 @@ Status: implemented ## Testing -`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` 固定这四个定义及其错误映射,包括 `refuses every tool for a delegated child caller`。`apps/cli/tests/web-agent-presets.e2e.ts` 断言 `minimal` preset 的工具列表,`snapshots/web/minimal-preset/tool-schemas.expected.json` 记录其只含 `bash` 的工具表。`packages/schedule/schedule/tests/subagent-ownership.spec.ts` 固定委派深度拒绝,`scripts/optional-bundles.spec.ts` 断言三个 preset 的两行委派行都拒绝全部四个工具名。 +`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` 固定这四个定义及其错误映射,包括 `refuses every tool for a delegated child caller`。`apps/cli/tests/web-agent-presets.e2e.ts` 断言 `minimal` preset 的工具列表,`snapshots/web/minimal-preset/tool-schemas.expected.json` 记录其不含提醒工具 schema 的工具表。`packages/schedule/schedule/tests/subagent-ownership.spec.ts` 固定委派深度拒绝,`scripts/optional-bundles.spec.ts` 断言三个 preset 的两行委派行都拒绝全部四个工具名。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 69b8cfee07..7cc7f2a17a 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -21,8 +21,8 @@ en: 8d2cfff9bab7327f zh: 21515ab6e6798504 /dsh-cli-behavior-reference/web-profile: - en: ee6c00e55a2f0a10 - zh: 404942e397e213ab + en: d768b6213e75a890 + zh: 731791b496ce14ef /dsh-cli-behavior-reference/shared-deployment-behavior: en: 6a7abc9339e8f37f zh: 133adb834c8e195d diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 3123b7898e..8195b3e5c5 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -126,11 +126,11 @@ The production Web runner needs built package and frontend artifacts (`pnpm run Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed. -The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its sandbox-policy root but intentionally omits filesystem tools, instruction discovery, and SQLite. An enabled HMR plugin watches both `cordis.patch.yml` layers (profile and home) and reapplies valid edits transactionally; without HMR, the process applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. +The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its sandbox-policy root and includes `read_image`, but omits text filesystem tools, instruction discovery, and SQLite. An enabled HMR plugin watches both `cordis.patch.yml` layers (profile and home) and reapplies valid edits transactionally; without HMR, the process applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers. New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service. -`DSH_TOOLS_MODE` selects `native` or `ptc` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only the platform-selected persistent shell. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place. +`DSH_TOOLS_MODE` selects `native` or `ptc` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and provides the platform-selected persistent shell, `read_image`, and `working_directory`. It supports automatic context compaction and the human `/compact` command. Select 极简模式 when creating a Web session; the shared browser, workspace, persistence, sandbox, and permission host remains in place. ## Shared deployment behavior diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 9a43ca2858..e1dbf1ffe8 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -128,11 +128,11 @@ dsh web --help 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。 -基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录,但刻意省略文件系统工具、指令发现与 SQLite。启用的 HMR 插件会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;未启用 HMR 时只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose 所有实时监视器。 +基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为沙箱策略根目录并包含 `read_image`,但省略文本文件系统工具、指令发现与 SQLite。启用的 HMR 插件会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;未启用 HMR 时只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose 所有实时监视器。 基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。 -`DSH_TOOLS_MODE` 为进程选择 `native` 或 `ptc`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合按平台选择的持久 shell。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 +`DSH_TOOLS_MODE` 为进程选择 `native` 或 `ptc`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并提供按平台选择的持久 shell、`read_image` 和 `working_directory`。它支持自动上下文压缩和用户 `/compact` 命令。创建 Web 会话时请选择极简模式;共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。 ## 共享部署行为 diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index f791cf0c55..0b92215c86 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -1256,6 +1256,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', ['session-projection', '@deepseek-ai/dsh-session-projection'], ['token-meter', '@deepseek-ai/dsh-token-meter'], ['compaction-basic', '@deepseek-ai/dsh-compaction-basic'], + ['image-offload', '@deepseek-ai/dsh-compaction-image-offload'], ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], ['subprocess', '@deepseek-ai/dsh-subprocess-local'], ['pty', '@deepseek-ai/dsh-terminal'], diff --git a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts index e30b17eab0..5c51bd36ae 100644 --- a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts +++ b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts @@ -202,11 +202,12 @@ describe('Python SDK dsh profile keyless smoke', () => { }, 40_000) it.each([ - { label: 'boots the standalone minimal profile through its generated manifest', editorEnabled: false, imageEnabled: false, optOut: false }, - { label: 'executes the documented editor opt-in patch with sdk-minimal', editorEnabled: true, imageEnabled: false, optOut: false }, - { label: 'sends a read_image result to the model with sdk-minimal', editorEnabled: false, imageEnabled: true, optOut: false }, - { label: 'retains the previous tools through the documented opt-out patch', editorEnabled: false, imageEnabled: false, optOut: true }, - ])('$label', async ({ editorEnabled, imageEnabled, optOut }) => { + { label: 'boots the standalone minimal profile through its generated manifest', editorEnabled: false, imageEnabled: false, optOut: false, offloadEnabled: false }, + { label: 'executes the documented editor opt-in patch with sdk-minimal', editorEnabled: true, imageEnabled: false, optOut: false, offloadEnabled: false }, + { label: 'sends a read_image result to the model with sdk-minimal', editorEnabled: false, imageEnabled: true, optOut: false, offloadEnabled: false }, + { label: 'offloads an over-budget image and continues with sdk-minimal', editorEnabled: false, imageEnabled: true, optOut: false, offloadEnabled: true }, + { label: 'retains the previous tools through the documented opt-out patch', editorEnabled: false, imageEnabled: false, optOut: true, offloadEnabled: false }, + ])('$label', async ({ editorEnabled, imageEnabled, optOut, offloadEnabled }) => { const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-')) onTestFinished(() => rm(root, { recursive: true, force: true })) const editorPatch = join(root, 'editor.patch.yml') @@ -223,6 +224,14 @@ describe('Python SDK dsh profile keyless smoke', () => { expect(yaml).toBeDefined() await writeFile(editorPatch, yaml!) } + if (offloadEnabled) await writeFile(editorPatch, [ + '- id: llm-deepseek', + ' config:', + ' apiKeyEnv: DEEPSEEK_API_KEY', + ' maxRequestFilesBytes: 1', + ' imageOffloadByteQuantum: 1', + '', + ].join('\n')) const editorFile = join(root, 'editor.txt') if (imageEnabled) await writeFile(join(root, 'red.png'), Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64', @@ -268,7 +277,7 @@ describe('Python SDK dsh profile keyless smoke', () => { ...launch.args, '--profile', 'sdk-minimal', - ...(editorEnabled || optOut ? ['--patch', editorPatch] : []), + ...(editorEnabled || optOut || offloadEnabled ? ['--patch', editorPatch] : []), ], { cwd: repoRoot, env: { @@ -307,9 +316,11 @@ describe('Python SDK dsh profile keyless smoke', () => { method: 'session/prompt', params: { sessionId: 'minimal', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, })}\n`) + const offloads: Record[] = [] const turnEnd = await waitForLine(lines, (value) => { const params = value.params as Record | undefined const event = params?.event as Record | undefined + if (params?.sessionId === 'minimal' && event?.type === 'image/offload') offloads.push(event) return params?.sessionId === 'minimal' && event?.type === 'turn/end' }, () => stderr) expect(turnEnd, `${JSON.stringify(turnEnd)}\n${stderr}`).toMatchObject({ @@ -337,9 +348,17 @@ describe('Python SDK dsh profile keyless smoke', () => { }[] const imageResult = messages.flatMap(message => message.content) .find(block => block.type === 'tool_result' && block.tool_use_id === 'minimal-image-read') - expect(imageResult?.content).toEqual(expect.arrayContaining([ - { type: 'image', source: { type: 'file', file_id: 'file-minimal-image' } }, - ])) + if (offloadEnabled) { + expect(offloads).toHaveLength(1) + expect(offloads[0]).toMatchObject({ data: { targets: [{ imageIndexes: [0] }] } }) + expect(imageResult?.content?.some(block => block.type === 'image')).toBe(false) + expect(JSON.stringify(imageResult?.content)).toContain('red.png') + } else { + expect(offloads).toEqual([]) + expect(imageResult?.content).toEqual(expect.arrayContaining([ + { type: 'image', source: { type: 'file', file_id: 'file-minimal-image' } }, + ])) + } const stored = await readdir(join(root, '.dsh', 'attachments', 'v1', 'objects'), { recursive: true }) expect(stored.some(file => /[a-f0-9]{64}$/u.test(file))).toBe(true) } diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 7de1c131ab..201decc2bb 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -434,7 +434,6 @@ describe('the shipped Web composition', () => { expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'read_image', 'working_directory']) } finally { await handle.dispose() - unregister() } }) @@ -463,7 +462,6 @@ describe('the shipped Web composition', () => { expect(adapter.requests.some(request => request.purpose === 'compaction')).toBe(true) } finally { await handle.dispose() - unregister() } }) diff --git a/apps/web/tests/expected/agent-preset-authoring/section.expected.md b/apps/web/tests/expected/agent-preset-authoring/section.expected.md index a7d47106e1..dd8628cd57 100644 --- a/apps/web/tests/expected/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/expected/agent-preset-authoring/section.expected.md @@ -31,7 +31,7 @@ - 'button "设为新任务默认: 极简模式"': - text: 极简模式 内置 - code: minimal - - text: Agent 仅使用终端工具完成任务,适合测试和对比其基础表现。 + - text: Agent 使用终端和图片读取工具,并支持上下文压缩,适合测试和对比其基础表现。 - 'button "模式说明: 极简模式"': 模式说明 - 'button "如何使用: 极简模式"': 如何使用 - 'button "查看配置: 极简模式"': 查看配置 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 7bba9d8f3e..a6a871b1e8 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3714,8 +3714,14 @@ export interface Config { ```ts config-catalog /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Tools to register, in canonical order; omitted means all four. Names must be unique and the list non-empty. */ - tools?: ('read' | 'read_image' | 'write' | 'edit')[] + /** Register `read` and its guidance. Defaults to true. */ + read?: boolean + /** Register `read_image` while an attachment store is mounted. Defaults to true. */ + readImage?: boolean + /** Register `write` and its guidance. Defaults to true. */ + write?: boolean + /** Register `edit` and its guidance. Defaults to true. */ + edit?: boolean /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 933b5d6db5..b9a3a4174e 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3716,8 +3716,14 @@ export interface Config { ```ts config-catalog /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Tools to register, in canonical order; omitted means all four. Names must be unique and the list non-empty. */ - tools?: ('read' | 'read_image' | 'write' | 'edit')[] + /** Register `read` and its guidance. Defaults to true. */ + read?: boolean + /** Register `read_image` while an attachment store is mounted. Defaults to true. */ + readImage?: boolean + /** Register `write` and its guidance. Defaults to true. */ + write?: boolean + /** Register `edit` and its guidance. Defaults to true. */ + edit?: boolean /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.i18n.yaml index 38023522ae..7855124a09 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.i18n.yaml @@ -9,8 +9,8 @@ en: 1b0161d44c885284 zh: 5babee2ad7dc2571 /sdk-minimal-image-reading-and-automatic-compaction/change: - en: b71b37ed705e0387 - zh: 9cabd8cb8cf59882 + en: 42d90c20c9549611 + zh: f904303bea2a60a8 /sdk-minimal-image-reading-and-automatic-compaction/migration: en: e944f28ce8c2ef8d zh: 3bd5fd84a02e5866 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.md index 5a851ddf08..a1cd0795fa 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.md @@ -9,7 +9,7 @@ English | [中文](guide.zh.md) ## Change -`sdk-minimal` advertises `read_image` alongside its persistent shell and `working_directory`. Automatic compaction sends an additional model request to summarize older history when context pressure reaches the configured threshold. The complete Session log remains stored as uncompressed JSONL. SDK prompt text does not execute `/compact`. +`sdk-minimal` advertises `read_image` alongside its persistent shell and `working_directory`. Automatic compaction sends an additional model request to summarize older history when context pressure reaches the configured threshold. The complete Session log remains stored as uncompressed JSONL. Images that exceed the route budget are permanently replaced by attachment/path placeholders so the request can retry. SDK prompt text does not execute `/compact`. Clients that pin tool names, benchmark prompt size, or count model requests must account for these defaults. Image reading requires an adapter route that declares image input. See the [bundle reference](../../../../packages/bundle/sdk-minimal/README.md) for configuration and model effects. @@ -19,7 +19,11 @@ Clients that pin tool names, benchmark prompt size, or count model requests must ```yaml - id: tool-fs - disabled: true + config: + read: false + readImage: false + write: false + edit: false - id: compaction-basic config: auto: false diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.zh.md index b4d2f42b81..6d4d43a100 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/sdk-minimal-image-compaction/guide.zh.md @@ -9,7 +9,7 @@ description: "sdk-minimal 默认工具清单新增 read_image,并自动生成 ## 变更 -`sdk-minimal` 在持久 shell 和 `working_directory` 之外公布 `read_image`。上下文压力达到配置阈值时,自动压缩会额外发起模型请求,生成较早历史的摘要。完整 Session 日志仍存储为未压缩 JSONL。SDK 提示词文本不会执行 `/compact`。 +`sdk-minimal` 在持久 shell 和 `working_directory` 之外公布 `read_image`。上下文压力达到配置阈值时,自动压缩会额外发起模型请求,生成较早历史的摘要。完整 Session 日志仍存储为未压缩 JSONL。图片超出路由预算时会被永久替换为包含附件与路径的占位文本,以便重试请求。SDK 提示词文本不会执行 `/compact`。 固定工具名称、基准提示词大小或统计模型请求数的客户端必须考虑这些默认行为。图片读取需要适配器路由声明支持图片输入。配置与模型影响见[组合包参考](../../../../packages/bundle/sdk-minimal/README.zh.md)。 @@ -19,7 +19,11 @@ description: "sdk-minimal 默认工具清单新增 read_image,并自动生成 ```yaml - id: tool-fs - disabled: true + config: + read: false + readImage: false + write: false + edit: false - id: compaction-basic config: auto: false diff --git a/packages/bundle/sdk-minimal/README.i18n.yaml b/packages/bundle/sdk-minimal/README.i18n.yaml index 8af69ec313..b93af7197d 100644 --- a/packages/bundle/sdk-minimal/README.i18n.yaml +++ b/packages/bundle/sdk-minimal/README.i18n.yaml @@ -36,11 +36,11 @@ en: df40d9529b566cfa zh: 4678769b4f85120b /deepseek-ai-dsh-sdk-minimal/model-experience/minimal-coding-agent-composition/token-effect: - en: d84b54486d1c66b0 - zh: 01a22b8947cb9a49 + en: 9b7a23c7a447eb22 + zh: e01bfd06d19b7d18 /deepseek-ai-dsh-sdk-minimal/model-experience/minimal-coding-agent-composition/kv-cache-effect: - en: e4ed99b28a68b765 - zh: fa39a06a5a3a5156 + en: dd887b6519f40ab3 + zh: a98e56411fdc79df /deepseek-ai-dsh-sdk-minimal/known-limitations-and-deferred-work: en: 00c4192d6af9f621 zh: 460451ab6dc07c3a diff --git a/packages/bundle/sdk-minimal/README.md b/packages/bundle/sdk-minimal/README.md index 4c17bca2e0..9accd1fec2 100644 --- a/packages/bundle/sdk-minimal/README.md +++ b/packages/bundle/sdk-minimal/README.md @@ -82,11 +82,11 @@ The system prompt is `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer #### Token effect -By default, one stable persona, three tool schemas, and user context for the initial directory and subsequent changes. Configured MCP servers add their tools, shared resource tools, and available server instructions. Images consume model input tokens. Automatic compaction sends an additional summarization request when context pressure reaches the configured threshold and replaces older model-visible history with its summary; the durable JSONL log retains those events. +By default, one stable persona, three tool schemas, and user context for the initial directory and subsequent changes. Configured MCP servers add their tools, shared resource tools, and available server instructions. Images consume model input tokens. When images exceed the route budget, [image offload](../../compaction/compaction-image-offload/README.md) permanently replaces older image occurrences with attachment/path placeholders and retries the request or compaction summary. Automatic compaction sends an additional summarization request when context pressure reaches the configured threshold and replaces older model-visible history with its summary; the durable JSONL log retains those events. #### KV Cache effect -Stable for a fixed persona, platform, provider, model, and bundle patch stack. Compaction replaces part of the request history and can reduce cache reuse. Profile changes take effect on the next process. +Stable for a fixed persona, platform, provider, model, and bundle patch stack. Compaction replaces part of the request history and can reduce cache reuse. Image offload also ends cache reuse at the first changed image. Profile changes take effect on the next process. ## Known Limitations and Deferred Work diff --git a/packages/bundle/sdk-minimal/README.zh.md b/packages/bundle/sdk-minimal/README.zh.md index 0db43af8a7..d35655df20 100644 --- a/packages/bundle/sdk-minimal/README.zh.md +++ b/packages/bundle/sdk-minimal/README.zh.md @@ -82,11 +82,11 @@ dsh --profile sdk-minimal #### Token 影响 -默认是一个稳定 persona、三个工具 schema,以及初始目录与后续变更的用户上下文。已配置 MCP 服务器会添加自己的工具、共享资源工具及可用的服务器指令。图片消耗模型输入 token。上下文压力达到配置阈值时,自动压缩会额外发起摘要请求,并用摘要替换较早的模型可见历史;持久 JSONL 日志保留这些事件。 +默认是一个稳定 persona、三个工具 schema,以及初始目录与后续变更的用户上下文。已配置 MCP 服务器会添加自己的工具、共享资源工具及可用的服务器指令。图片消耗模型输入 token。图片超过路由预算时,[图片省略](../../compaction/compaction-image-offload/README.zh.md)会永久将较早的图片位置替换为包含附件与路径的占位文本,并重试请求或压缩摘要。上下文压力达到配置阈值时,自动压缩会额外发起摘要请求,并用摘要替换较早的模型可见历史;持久 JSONL 日志保留这些事件。 #### KV Cache 影响 -当 persona、平台、提供方、模型与 bundle patch 栈固定时保持稳定。压缩会替换部分请求历史,可能降低缓存复用率。Profile 变更在下一个进程生效。 +当 persona、平台、提供方、模型与 bundle patch 栈固定时保持稳定。压缩会替换部分请求历史,可能降低缓存复用率。图片省略还会使缓存复用止于第一张被修改的图片。Profile 变更在下一个进程生效。 ## 已知限制与延期工作 diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index 4c35a7ef8d..79922e7113 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -44,8 +44,10 @@ - id: tool-fs name: '@deepseek-ai/dsh-tool-fs' config: - tools: - - read_image + read: false + readImage: true + write: false + edit: false - id: working-directory name: '@deepseek-ai/dsh-working-directory' @@ -61,8 +63,9 @@ - id: compaction-basic name: '@deepseek-ai/dsh-compaction-basic' - config: - auto: true + + - id: image-offload + name: '@deepseek-ai/dsh-compaction-image-offload' - id: sandbox-policy name: '@deepseek-ai/dsh-sandbox-policy' diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index 96e7728c5b..04095beabb 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -39,6 +39,7 @@ "@deepseek-ai/dsh-agent-loop": "workspace:*", "@deepseek-ai/dsh-attachment-local": "workspace:*", "@deepseek-ai/dsh-compaction-basic": "workspace:*", + "@deepseek-ai/dsh-compaction-image-offload": "workspace:*", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*", "@deepseek-ai/dsh-jobs-local": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index c85bf3e121..f4e24e5bc5 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -41,6 +41,7 @@ describe('dsh-sdk-minimal bundle', () => { ['session-projection', '@deepseek-ai/dsh-session-projection'], ['token-meter', '@deepseek-ai/dsh-token-meter'], ['compaction-basic', '@deepseek-ai/dsh-compaction-basic'], + ['image-offload', '@deepseek-ai/dsh-compaction-image-offload'], ['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'], ['subprocess', '@deepseek-ai/dsh-subprocess-local'], ['pty', '@deepseek-ai/dsh-terminal'], @@ -77,8 +78,8 @@ describe('dsh-sdk-minimal bundle', () => { personaPrefix: { __jsExpr: "process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'" }, }) expect(rows.find(row => row.id === 'agent-loop')?.config).toEqual({ agents: [] }) - expect(rows.find(row => row.id === 'tool-fs')?.config).toEqual({ tools: ['read_image'] }) - expect(rows.find(row => row.id === 'compaction-basic')?.config).toEqual({ auto: true }) + expect(rows.find(row => row.id === 'tool-fs')?.config).toEqual({ read: false, readImage: true, write: false, edit: false }) + expect(rows.find(row => row.id === 'compaction-basic')?.config).toBeUndefined() expect(rows.find(row => row.id === 'terminal-bash')).toMatchObject({ disabled: { __jsExpr: "process.platform === 'win32'" }, }) diff --git a/packages/bundle/web-app/presets/minimal.patch.yml b/packages/bundle/web-app/presets/minimal.patch.yml index 21b98f6158..043e34cb01 100644 --- a/packages/bundle/web-app/presets/minimal.patch.yml +++ b/packages/bundle/web-app/presets/minimal.patch.yml @@ -17,8 +17,10 @@ - id: tool-fs name: '@deepseek-ai/dsh-tool-fs' config: - tools: - - read_image + read: false + readImage: true + write: false + edit: false - id: compaction name: cordis:group group: true @@ -27,8 +29,6 @@ config: - id: compaction-basic name: '@deepseek-ai/dsh-compaction-basic' - config: - auto: true - id: command-compact name: '@deepseek-ai/dsh-command-compact' - id: persistent-shell diff --git a/packages/compaction/compaction-image-offload/README.i18n.yaml b/packages/compaction/compaction-image-offload/README.i18n.yaml index c23237b66e..4a87529c58 100644 --- a/packages/compaction/compaction-image-offload/README.i18n.yaml +++ b/packages/compaction/compaction-image-offload/README.i18n.yaml @@ -24,8 +24,8 @@ en: 6a156ba4e44b520a zh: a61fc63697f85095 /deepseek-ai-dsh-compaction-image-offload/use-this-package/failures-and-recovery: - en: 5e4ac1980eec905a - zh: 0284e597c82cb179 + en: 453802a8e8338955 + zh: c49c3d1871cca863 /deepseek-ai-dsh-compaction-image-offload/understand-the-implementation: en: 61d7de1373f1c420 zh: bec696992dfa774a diff --git a/packages/compaction/compaction-image-offload/README.md b/packages/compaction/compaction-image-offload/README.md index d1d3f60687..e417c8d1ec 100644 --- a/packages/compaction/compaction-image-offload/README.md +++ b/packages/compaction/compaction-image-offload/README.md @@ -39,7 +39,7 @@ Each decision appends one `image/offload` event identifying the selected occurre ### Failures and recovery -The plugin acts only on `IMAGE_OFFLOAD_REQUIRED` failures that carry `offloadImages`. It walks the surface in model request order, skips assistant nodes and images already marked, and marks that many occurrences. When nothing remains to offload it delegates on the `agent/request-error` waterfall, so downstream recovery or the ordinary turn error applies. The retry spends no provider retry budget and logs no `llm/retry` event. +The plugin acts only on `IMAGE_OFFLOAD_REQUIRED` failures that carry `offloadImages`. It walks the surface in model request order, skips assistant nodes and images already marked, and marks that many occurrences. When nothing remains to offload it delegates on the `agent/request-error` waterfall, so downstream recovery or the ordinary turn error applies. The retry spends no provider retry budget and logs no `llm/retry` event. Cancellation before recording a selection delegates without adding an `image/offload` event; cancellation after a committed selection stops retry but preserves the omission. ----- diff --git a/packages/compaction/compaction-image-offload/README.zh.md b/packages/compaction/compaction-image-offload/README.zh.md index ed00a4dfd6..4e6f35d016 100644 --- a/packages/compaction/compaction-image-offload/README.zh.md +++ b/packages/compaction/compaction-image-offload/README.zh.md @@ -39,7 +39,7 @@ kind: "package-reference" ### 失败与恢复 -本插件只处理带 `offloadImages` 的 `IMAGE_OFFLOAD_REQUIRED` 失败。它按模型请求顺序遍历表层,跳过 assistant 节点和已经标记的图片,给指定数量的出现位置打标记。没有可省略的图片时在 `agent/request-error` waterfall 上委派下游,由下游恢复或普通的轮次错误处理。这次重试不占提供方重试预算,也不追加 `llm/retry` 事件。 +本插件只处理带 `offloadImages` 的 `IMAGE_OFFLOAD_REQUIRED` 失败。它按模型请求顺序遍历表层,跳过 assistant 节点和已经标记的图片,给指定数量的出现位置打标记。没有可省略的图片时在 `agent/request-error` waterfall 上委派下游,由下游恢复或普通的轮次错误处理。这次重试不占提供方重试预算,也不追加 `llm/retry` 事件。记录选择前发生取消时会委派后续处理,不新增 `image/offload` 事件;选择提交后发生取消时停止重试,但保留省略记录。 ----- diff --git a/packages/compaction/compaction-image-offload/src/index.ts b/packages/compaction/compaction-image-offload/src/index.ts index 5415cd5c4c..f6f68eaff5 100644 --- a/packages/compaction/compaction-image-offload/src/index.ts +++ b/packages/compaction/compaction-image-offload/src/index.ts @@ -24,8 +24,8 @@ export const inject = ['agents', 'sessions'] */ export function apply(ctx: Context): void { ctx.sessions.registerMessageProjection(imageOffloadProjection) - ctx.on('agent/request-error', ({ agent, failure }, next): Promise => { - if (failure.code !== IMAGE_OFFLOAD_REQUIRED_CODE || failure.offloadImages === undefined) return next() + ctx.on('agent/request-error', ({ agent, failure, signal }, next): Promise => { + if (failure.code !== IMAGE_OFFLOAD_REQUIRED_CODE || failure.offloadImages === undefined || signal.aborted) return next() // A durable surface repair, not a provider retry: it spends no retry budget and logs no retry event. if (!offloadOldestImages(agent.session, agent.session.surface.nodes, failure.offloadImages)) return next() return Promise.resolve({ kind: 'retry' }) diff --git a/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts b/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts index 001b307a65..b001da9a94 100644 --- a/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts +++ b/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts @@ -223,6 +223,48 @@ describe('summary image offload', () => { }) describe('compaction-image-offload', () => { + it('cancelling at failed-attempt settlement preserves images for later turns', async () => { + const adapter = new ScriptedAdapter([offloadRequired(1), textResponse('continued')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-cancel-before-record'), { provider: 'mock', model: 'mock' }) + ctx.on('session/event', (session, event) => { + if (session === agent.session && event.type === 'assistant/attempt') agent.cancel({ kind: 'user' }) + }) + agent.followup(createUserMessage({ content: [image('original')], source: { kind: 'user' } })) + await agent.whenIdle() + expect(agent.session.snapshotEvents().find(event => event.type === 'turn/end')) + .toMatchObject({ data: { reason: { kind: 'aborted' } } }) + expect(adapter.requests).toHaveLength(1) + expect(decisions(agent.session)).toEqual([]) + + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } })) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(2) + expect(offloadedNames(adapter.requests[1]!)).toEqual([]) + expect(agent.session.deriveMessages().flatMap(message => message.content)).toContainEqual(image('original')) + }) + + it('cancelling after an image omission stops retry and retains the committed selection', async () => { + const adapter = new ScriptedAdapter([offloadRequired(1), textResponse('continued')]) + const ctx = await harness(adapter) + const agent = await ctx.agentLoop.create(SessionId('offload-cancel-after-record'), { provider: 'mock', model: 'mock' }) + ctx.on('session/event', (session, event) => { + if (session === agent.session && event.type === 'image/offload') agent.cancel({ kind: 'user' }) + }) + agent.followup(createUserMessage({ content: [image('original')], source: { kind: 'user' } })) + await agent.whenIdle() + expect(agent.session.snapshotEvents().find(event => event.type === 'turn/end')) + .toMatchObject({ data: { reason: { kind: 'aborted' } } }) + expect(adapter.requests).toHaveLength(1) + expect(decisions(agent.session)).toHaveLength(1) + + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } })) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(2) + expect(offloadedNames(adapter.requests[1]!)).toEqual(['original']) + expect(decisions(agent.session)).toHaveLength(1) + }) + it('logs one exact image selection and retries without replacing the message', async () => { const adapter = new ScriptedAdapter([offloadRequired(2), textResponse('sent')]) const ctx = await harness(adapter) diff --git a/packages/fs/tool-fs/README.i18n.yaml b/packages/fs/tool-fs/README.i18n.yaml index 21ded3b6c9..d3ba5a1f48 100644 --- a/packages/fs/tool-fs/README.i18n.yaml +++ b/packages/fs/tool-fs/README.i18n.yaml @@ -24,8 +24,8 @@ en: 29a1d47e9677f2a3 zh: 0ae3c794cc7c54db /deepseek-ai-dsh-tool-fs/use-this-package/configuration: - en: 8b2564d19a3ce635 - zh: 284d0b8aabc6ce37 + en: 6903a9ca39cb9a71 + zh: a078377d02c3ab6f /deepseek-ai-dsh-tool-fs/use-this-package/policy-and-sandbox-behavior: en: 53d4ac2db089ef39 zh: 214ea051ed56f500 diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index 61f780819b..787e88f916 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -54,11 +54,14 @@ Field names are snake_case to match Claude Code and existing harness tool schema ### Configuration -All keys are optional. Omit `tools` to register the full suite, or select a non-empty list of unique tool names, such as `tools: [read_image]`. Selection controls registration and guidance; it does not bypass filesystem authorization or the image tool's attachment and model requirements. Tools register in canonical order regardless of list order. Read caps remain validated even when `read` is excluded. +All keys are optional. The `read`, `readImage`, `write`, and `edit` switches default to `true`; set a switch to `false` to omit that tool and its guidance. All four may be disabled. Enabling `readImage` still requires an attachment store and an image-capable model. Read caps remain validated when `read` is disabled. | Key | Default | Meaning | |---|---|---| -| `tools` | `[read, read_image, write, edit]` | Tools to register; unknown names, duplicates, and empty lists reject configuration | +| `read` | `true` | Register `read` and its guidance | +| `readImage` | `true` | Register `read_image` while an attachment store is mounted | +| `write` | `true` | Register `write` and its guidance | +| `edit` | `true` | Register `edit` and its guidance | | `readLimit` | `2000` | Default and maximum lines returned by one `read` call | | `readMaxLineLength` | `2000` | Characters kept per line before truncation | | `readMaxBytes` | `51200` | Byte cap on one `read` call's selected lines; overflow ends the window with a capped footer | @@ -66,6 +69,8 @@ All keys are optional. Omit `tools` to register the full suite, or select a non- The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-fs) is the exhaustive source for every accepted field and its JSDoc. +With `fs-observation-policy` mounted and `read` disabled, `write` can still create files, and successful writes or edits record the versions needed for later mutations in that session. Existing unobserved files remain protected; disabling `read` does not relax the policy. Another filesystem reader must emit `fs/observed` for an existing file to become observed. + ### Policy and sandbox behavior Path authorization for `read` and `read_image` belongs entirely to `ctx.fs`; media-type declarations and file signatures only decide whether `read_image` accepts the bytes returned by that backend. diff --git a/packages/fs/tool-fs/README.zh.md b/packages/fs/tool-fs/README.zh.md index c863b71d21..acee934f80 100644 --- a/packages/fs/tool-fs/README.zh.md +++ b/packages/fs/tool-fs/README.zh.md @@ -54,11 +54,14 @@ kind: "package-reference" ### 配置 -所有键均为可选。省略 `tools` 时注册完整工具集,也可选择非空且名称不重复的列表,例如 `tools: [read_image]`。选择项决定工具注册和指导文本,不会绕过文件系统授权或图片工具的附件、模型要求。工具按固定顺序注册,与列表顺序无关。即使未选择 `read`,读取上限配置仍会校验。 +所有键均为可选。`read`、`readImage`、`write` 和 `edit` 开关默认为 `true`;将开关设为 `false` 可省略对应工具及其指导文本。允许关闭全部四个工具。启用 `readImage` 仍需要附件存储和支持图片输入的模型。关闭 `read` 时仍会校验读取上限。 | 键 | 默认值 | 含义 | |---|---|---| -| `tools` | `[read, read_image, write, edit]` | 要注册的工具;未知名称、重复项和空列表会使配置校验失败 | +| `read` | `true` | 注册 `read` 及其指导文本 | +| `readImage` | `true` | 附件存储挂载时注册 `read_image` | +| `write` | `true` | 注册 `write` 及其指导文本 | +| `edit` | `true` | 注册 `edit` 及其指导文本 | | `readLimit` | `2000` | 一次 `read` 调用返回的默认和最大行数 | | `readMaxLineLength` | `2000` | 每行截断前保留的字符数 | | `readMaxBytes` | `51200` | 一次 `read` 调用所选行的字节上限;溢出时以「已达上限」footer 结束窗口 | @@ -66,6 +69,8 @@ kind: "package-reference" 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-fs)是每个受支持字段及其 JSDoc 的穷尽式真源。 +挂载 `fs-observation-policy` 且关闭 `read` 时,`write` 仍可创建文件,成功写入或编辑会记录本会话后续修改所需的版本。未观察过的已有文件仍受保护;关闭 `read` 不会放宽策略。其他文件系统读取方必须发出 `fs/observed`,已有文件才会成为已观察状态。 + ### 策略与沙箱行为 `read` 与 `read_image` 的路径授权完全由 `ctx.fs` 负责;媒体类型声明和文件签名只决定 `read_image` 是否接受该后端返回的字节。 diff --git a/packages/fs/tool-fs/src/index.ts b/packages/fs/tool-fs/src/index.ts index 0705813fec..5c7a78d01b 100644 --- a/packages/fs/tool-fs/src/index.ts +++ b/packages/fs/tool-fs/src/index.ts @@ -24,8 +24,14 @@ export const inject = ['tools', 'fs', 'systemPrompt', 'workingDirectory'] /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Tools to register, in canonical order; omitted means all four. Names must be unique and the list non-empty. */ - tools?: ('read' | 'read_image' | 'write' | 'edit')[] + /** Register `read` and its guidance. Defaults to true. */ + read?: boolean + /** Register `read_image` while an attachment store is mounted. Defaults to true. */ + readImage?: boolean + /** Register `write` and its guidance. Defaults to true. */ + write?: boolean + /** Register `edit` and its guidance. Defaults to true. */ + edit?: boolean /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ @@ -37,8 +43,10 @@ export interface Config { } export const Config: z = z.object({ - tools: z.array(z.union(['read', 'read_image', 'write', 'edit'] as const)).min(1) - .default(['read', 'read_image', 'write', 'edit']), + read: z.boolean().default(true), + readImage: z.boolean().default(true), + write: z.boolean().default(true), + edit: z.boolean().default(true), readLimit: z.number().default(READ_LIMIT), readMaxLineLength: z.number().default(READ_MAX_LINE_LENGTH), readMaxBytes: z.number().default(READ_MAX_BYTES), @@ -59,13 +67,11 @@ function assertPositiveInteger(name: string, value: number): void { export function apply(ctx: Context, config: Config): void { // schemastery (Config) has already filled every defaulted field. const resolved = config as ResolvedConfig - const tools = new Set(resolved.tools) - if (tools.size !== resolved.tools.length) throw new Error('tool-fs: tools must contain unique names') assertPositiveInteger('readLimit', resolved.readLimit) assertPositiveInteger('readMaxLineLength', resolved.readMaxLineLength) assertPositiveInteger('readMaxBytes', resolved.readMaxBytes) assertPositiveInteger('readStreamMinSize', resolved.readStreamMinSize) - if (tools.has('read')) { + if (resolved.read) { applyReadTool(ctx, { limit: resolved.readLimit, maxLineLength: resolved.readMaxLineLength, @@ -76,7 +82,7 @@ export function apply(ctx: Context, config: Config): void { // read_image is composition-conditional: without a mounted attachment store // the deployment cannot durably commit image bytes, so the tool never // registers; the execute body keeps a defensive re-check for direct callers. - if (tools.has('read_image')) { + if (resolved.readImage) { ctx.inject(['attachments'], (imageCtx) => { applyReadImageTool(imageCtx) }) @@ -84,9 +90,9 @@ export function apply(ctx: Context, config: Config): void { // One escalation API shared by both mutating tools: advertisement gating, // per-call policy resolution, and denial-marker mapping, all keyed off whether // the mounted ctx.fs confines (ctx.fs.sandboxMode). - if (tools.has('write') || tools.has('edit')) { + if (resolved.write || resolved.edit) { const sandbox = new FsSandboxController(ctx) - if (tools.has('write')) applyWriteTool(ctx, sandbox) - if (tools.has('edit')) applyEditTool(ctx, sandbox) + if (resolved.write) applyWriteTool(ctx, sandbox) + if (resolved.edit) applyEditTool(ctx, sandbox) } } diff --git a/packages/fs/tool-fs/tests/integration.spec.ts b/packages/fs/tool-fs/tests/integration.spec.ts index 1c0dfd32c2..1b51503a74 100644 --- a/packages/fs/tool-fs/tests/integration.spec.ts +++ b/packages/fs/tool-fs/tests/integration.spec.ts @@ -71,6 +71,21 @@ describe('default deployment (with dsh-fs-observation-policy)', () => { }) describe('write → disk', () => { + it('without read, creates and edits a new file while protecting unobserved existing files', async () => { + await fiber.dispose() + fiber = await ctx.plugin(ToolFs, { read: false }) + await writeFile(join(dir, 'existing.txt'), 'original') + expect((await call('write', { file_path: 'new.txt', content: 'created' })).isError).toBe(false) + expect((await call('edit', { file_path: 'new.txt', old_string: 'created', new_string: 'edited' })).isError).toBe(false) + expect(await readFile(join(dir, 'new.txt'), 'utf8')).toBe('edited') + expect(await call('write', { file_path: 'existing.txt', content: 'overwrite' })) + .toMatchObject({ isError: true, error: { info: { code: 'FS_NOT_OBSERVED' } } }) + expect(await call('edit', { file_path: 'existing.txt', old_string: 'original', new_string: 'overwrite' })) + .toMatchObject({ isError: true, error: { info: { code: 'FS_NOT_OBSERVED' } } }) + expect(await readFile(join(dir, 'existing.txt'), 'utf8')).toBe('original') + expect(ctx.tools.get('read')).toBeUndefined() + }) + it('creates a file with exactly the requested bytes', async () => { const result = await call('write', { file_path: 'new.txt', content: 'line one\nline two\n' }) expect(result.isError).toBe(false) diff --git a/packages/fs/tool-fs/tests/read-image.spec.ts b/packages/fs/tool-fs/tests/read-image.spec.ts index 1e14a92ef1..b85db7be49 100644 --- a/packages/fs/tool-fs/tests/read-image.spec.ts +++ b/packages/fs/tool-fs/tests/read-image.spec.ts @@ -96,7 +96,7 @@ afterEach(async () => { }) interface SetupOptions { - tools?: ToolFs.Config['tools'] + toolFs?: ToolFs.Config models?: LlmModelInfo[] resolvedModels?: LlmModelInfo[] attachments?: boolean @@ -127,7 +127,7 @@ async function setup(options: SetupOptions = {}) { { provider: 'visual', id: 'legacy-model', name: 'Legacy' }, ], options.resolvedModels)) } - await ctx.plugin(ToolFs, options.tools === undefined ? {} : { tools: options.tools }) + await ctx.plugin(ToolFs, options.toolFs ?? {}) return ctx } @@ -514,7 +514,7 @@ describe('argument and service preconditions', () => { expect(text(nonImage)).toContain('the .txt extension does not declare a supported image format') }) - it.each([{}, { tools: ['read_image'] }] satisfies SetupOptions[]) + it.each([{}, { toolFs: { read: false, write: false, edit: false } }] satisfies SetupOptions[]) ('refuses when no attachment service is mounted: %j', async (config) => { await writeFile(join(dir, 'red.png'), PNG_1X1) const ctx = await setup({ ...config, attachments: false }) @@ -762,7 +762,7 @@ describe('image admission failures', () => { describe('registration surface', () => { it('registers only read_image without text guidance', async () => { - const ctx = await setup({ tools: ['read_image'] }) + const ctx = await setup({ toolFs: { read: false, write: false, edit: false } }) try { expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['read_image']) expect((await ctx.systemPrompt.assemble()).sections.some(section => section.name.startsWith('tool:'))).toBe(false) @@ -775,8 +775,17 @@ describe('registration surface', () => { } }) - it.each([{ tools: ['read_image'] }, { tools: ['read', 'read_image', 'write', 'edit'] }] as const) - ('withdraws and restores selected image registration with the attachment store: $tools', async ({ tools }) => { + it('omits read_image while an attachment store is mounted when readImage is false', async () => { + const ctx = await setup({ toolFs: { readImage: false } }) + expect(ctx.tools.schemas().map(tool => tool.name).sort()).toEqual(['edit', 'read', 'write']) + expect(ctx.tools.get('read_image')).toBeUndefined() + expect((await readImage(ctx, { file_path: 'red.png' }, agentOn('vision-model'))).isError).toBe(true) + }) + + it.each([ + { config: { read: false, write: false, edit: false }, tools: ['read_image'] }, + { config: {}, tools: ['read', 'read_image', 'write', 'edit'] }, + ])('withdraws and restores enabled image registration with the attachment store: $config', async ({ config, tools }) => { const ctx = new Context() try { provideWorkingDirectoryFixture(ctx) @@ -784,7 +793,7 @@ describe('registration surface', () => { await ctx.plugin(ToolRuntime) await ctx.plugin(LocalFileSystem, { cwd: dir }) const attachment = await ctx.plugin(LocalAttachmentStore, { dshHome: home }) - const plugin = await ctx.plugin(ToolFs, { tools: [...tools] }) + const plugin = await ctx.plugin(ToolFs, config) const names = () => ctx.tools.schemas().map(tool => tool.name).sort() expect(names()).toEqual([...tools].sort()) await attachment.dispose() diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index 47d46c01cf..83d22de89d 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -149,14 +149,14 @@ describe('current directory resolution', () => { describe('registration', () => { it.each([ - { tools: ['read'] as const, sections: ['tool:read'] }, - { tools: ['write'] as const, sections: ['tool:write'] }, - { tools: ['edit'] as const, sections: ['tool:edit'] }, - { tools: ['edit', 'read'] as const, sections: ['tool:edit', 'tool:read'] }, - ])('registers only selected tools and their guidance: $tools', async ({ tools, sections }) => { - const { ctx } = await setup({ tools: [...tools] }) + { config: { read: false }, tools: ['edit', 'write'], sections: ['tool:edit', 'tool:write'] }, + { config: { write: false }, tools: ['edit', 'read'], sections: ['tool:edit', 'tool:read'] }, + { config: { edit: false }, tools: ['read', 'write'], sections: ['tool:read', 'tool:write'] }, + { config: { read: false, readImage: false, write: false, edit: false }, tools: [], sections: [] }, + ])('omits disabled tools and their guidance: $config', async ({ config, tools, sections }) => { + const { ctx } = await setup(config) try { - expect(ctx.tools.schemas().map(tool => tool.name).sort()).toEqual([...tools].sort()) + expect(ctx.tools.schemas().map(tool => tool.name).sort()).toEqual(tools) expect((await ctx.systemPrompt.assemble()).sections.map(section => section.name) .filter(name => name.startsWith('tool:')).sort()).toEqual(sections) const excluded = ['read', 'write', 'edit'].find(name => !tools.some(tool => tool === name))! @@ -166,24 +166,10 @@ describe('registration', () => { } }) - it.each([{ tools: [] }, { tools: ['missing'] }])('rejects invalid tool selection at configuration parsing: %j', async (config) => { + it.each([{ read: 'false' }, { readImage: 1 }, { write: [] }, { edit: {} }])('rejects non-boolean switches at configuration parsing: %j', async (config) => { expect((await ToolFs.Config['~standard'].validate(config)).issues).toBeDefined() }) - it('rejects duplicate selection before registering tools', async () => { - const ctx = new Context() - try { - provideWorkingDirectoryFixture(ctx) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - await ctx.plugin(FakeFs) - await expect(ctx.plugin(ToolFs, { tools: ['read', 'read'] })).rejects.toThrow('tools must contain unique names') - expect(ctx.tools.schemas()).toEqual([]) - } finally { - await ctx.fiber.dispose() - } - }) - it('registers read, write, and edit', async () => { const { ctx } = await setup() expect(ctx.tools.schemas().map(s => s.name).sort()).toEqual(['edit', 'read', 'write']) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52cac25f41..d178775191 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2420,6 +2420,9 @@ importers: '@deepseek-ai/dsh-compaction-basic': specifier: workspace:* version: link:../../compaction/compaction-basic + '@deepseek-ai/dsh-compaction-image-offload': + specifier: workspace:* + version: link:../../compaction/compaction-image-offload '@deepseek-ai/dsh-deepseek-llm-api-extensions': specifier: workspace:* version: link:../../llm/deepseek-llm-api-extensions diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 4f3ea7ad34..b0f9b305c7 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -12,8 +12,8 @@ en: 5fceaffcc35039c6 zh: 1d87e3defe7e875b /deepseek-harness-python-sdk/customize-plugins: - en: d32ee602f121e6af - zh: fdea4487688e5774 + en: 69a6a6745c6e89c1 + zh: 85e82a4cc31e16e5 /deepseek-harness-python-sdk/results-and-notifications: - en: 173d2dc1ce63fe2d - zh: 5354e18c59f20878 + en: d4f7366c2d260639 + zh: 7de99aed2b1bdd0a diff --git a/python/sdk/README.md b/python/sdk/README.md index c92b061717..9ed3f8d281 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -85,7 +85,7 @@ with DeepSeekHarness( `provider` selects a provider route registered by the chosen Cordis composition; `model` is the model id resolved by that adapter. `reasoning_effort` is an optional non-empty adapter-owned identifier for that exact route; omission preserves the model's own default. `max_tokens` is an optional positive per-request output-token cap for the root agent and its in-process descendants; omission leaves the provider default in control. Initialization rejects a missing adapter, unavailable model, or unsupported effort before a prompt runs. Compaction summaries keep the separate limit configured by their compaction plugin. The bundled default composition registers `deepseek-official`. A custom composition can mount `llm-pi-ai`, configure provider-specific credentials/endpoints there, and select any provider/model present in pi-ai's installed catalog. -The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides only a platform-selected persistent shell, local execution, and JSONL sessions; filesystem tools, settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. +The shipped `sdk-minimal` profile is a standalone explicit tree rather than an overlay on `dsh-base`. Select it with `profile="sdk-minimal"`; the ordinary `model` argument is the sole runtime model selection, including for model ids outside the adapter's advisory catalog. It provides a platform-selected persistent shell, `read_image`, `working_directory`, local execution, JSONL sessions, and automatic compaction. Text file tools, settings, managed credentials, telemetry, Web tools, and the full default tool roster remain available through the separate full `sdk` and `web` profiles. SDK prompt text does not execute `/compact`. ## Results and notifications @@ -93,7 +93,7 @@ The shipped `sdk-minimal` profile is a standalone explicit tree rather than an o `HarnessClient` retains discovered subagent ancestry for the runtime process lifetime. During `Session.run()`, `RunResult.notifications` and `on_notification` receive the root session and known descendants in wire order. `RunResult.events` contains root-session events only, so descendant output cannot replace the root response. The low-level `session_prompt()` returns the queued message id immediately; callers that bypass `Session.run()` own the later activity boundary. -The selected home stores profiles, plugins, and every profile-owned durable resource. The full `sdk` profile uses its credentials, settings, and session stores; `sdk-minimal` uses only its JSONL session store. Use a fresh home when those resources must be isolated, and a fresh session id for independent work. Reusing both a harness and session id continues the durable conversation and session-owned resources. +The selected home stores profiles, plugins, and every profile-owned durable resource. The full `sdk` profile uses its credentials, settings, and session stores; `sdk-minimal` uses JSONL session storage and stores images under `$DSH_HOME/attachments/v1`, with request-image cache under `$DSH_HOME/cache/attachments`. Use a fresh home when those resources must be isolated, and a fresh session id for independent work. Reusing both a harness and session id continues the durable conversation and session-owned resources. See the [Python tutorial](../../docs/user/guide/python-sdk.md), [runnable example](examples/README.md), and [runtime wheel reference](../sdk-runtime/README.md). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 3e58512d87..7adf0decbd 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -85,7 +85,7 @@ with DeepSeekHarness( `provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`reasoning_effort` 是该确切路由可选的非空适配器自有标识符;省略时保留模型自身的默认值。`max_tokens` 是一个可选的正整数,用于限制根 agent 及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。缺少适配器、模型不可用或推理强度不受支持时,初始化会在提示词运行前拒绝。压缩(compaction)摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 -随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 ID。它只提供按平台选择的持久 shell、本地执行与 JSONL 会话;文件系统工具、设置、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。 +随附的 `sdk-minimal` profile 是独立显式配置树,而不是 `dsh-base` 上的 overlay。使用 `profile="sdk-minimal"` 选择它;普通 `model` 参数是唯一运行时模型选择,也适用于不在适配器建议目录中的模型 ID。它提供按平台选择的持久 shell、`read_image`、`working_directory`、本地执行、JSONL 会话和自动压缩。文本文件工具、设置、托管凭据、遥测、Web 工具与完整默认工具清单仍由独立的完整 `sdk` 与 `web` profile 提供。SDK 提示词文本不会执行 `/compact`。 ## 结果与通知 @@ -93,7 +93,7 @@ with DeepSeekHarness( `HarnessClient` 会在运行时进程的整个生命周期内保留已发现的 subagent 谱系。在 `Session.run()` 期间,`RunResult.notifications` 与 `on_notification` 按协议顺序接收根会话和已知后代的通知。`RunResult.events` 只包含根会话事件,因此后代输出不会替换根响应。底层 `session_prompt()` 会立即返回已排队消息的 id;绕过 `Session.run()` 的调用方自行负责后续活动边界。 -所选 home 保存 profile、插件与每个 profile 自有的持久资源。完整 `sdk` profile 使用其中的凭据、设置与会话存储;`sdk-minimal` 只使用自己的 JSONL 会话存储。需要隔离这些资源时应使用新的 home;独立工作应使用新的会话 ID。同时复用 harness 与会话 ID 会延续持久对话和会话资源。 +所选 home 保存 profile、插件与每个 profile 自有的持久资源。完整 `sdk` profile 使用其中的凭据、设置与会话存储;`sdk-minimal` 保存 JSONL 会话,将图片存储在 `$DSH_HOME/attachments/v1` 下,请求图片缓存位于 `$DSH_HOME/cache/attachments` 下。需要隔离这些资源时应使用新的 home;独立工作应使用新的会话 ID。同时复用 harness 与会话 ID 会延续持久对话和会话资源。 另见 [Python 教程](../../docs/user/guide/python-sdk.zh.md)、[可运行示例](examples/README.zh.md) 和 [运行时 wheel 包参考](../sdk-runtime/README.zh.md)。 diff --git a/snapshots/sdk/minimal-image-compaction/notifications.expected.jsonl b/snapshots/sdk/minimal-image-compaction/notifications.expected.jsonl index c1bfc3eaf7..87e133697e 100644 --- a/snapshots/sdk/minimal-image-compaction/notifications.expected.jsonl +++ b/snapshots/sdk/minimal-image-compaction/notifications.expected.jsonl @@ -20,7 +20,10 @@ {"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":"user/message","seq":19,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":20,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"series"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_IMAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["MINIMAL_IMAGE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_IMAGE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":23,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":21,"time":0,"data":{"turn":1,"step":2,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"request images exceed the route budget","code":"IMAGE_OFFLOAD_REQUIRED","offloadImages":1}}}}]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"image/offload","seq":22,"time":0,"data":{"targets":[{"seq":12,"imageIndexes":[0]}]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":23,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"series"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_IMAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["MINIMAL_IMAGE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_IMAGE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":25,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":26,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/minimal-image-compaction/session.v4.jsonl b/snapshots/sdk/minimal-image-compaction/session.v4.jsonl index aadf65b1b7..65412952e8 100644 --- a/snapshots/sdk/minimal-image-compaction/session.v4.jsonl +++ b/snapshots/sdk/minimal-image-compaction/session.v4.jsonl @@ -20,6 +20,9 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\"."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."}]},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"series"}} +{"type":"assistant/attempt","data":{"turn":1,"step":2,"stream":[{"type":"chunk","time":1791776020002,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"request images exceed the route budget","code":"IMAGE_OFFLOAD_REQUIRED","offloadImages":1}}}}]}} +{"type":"image/offload","data":{"targets":[{"seq":12,"imageIndexes":[0]}]}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-flash"},"tools":"{{tools}}"},"reason":"series"}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_IMAGE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1791776020002,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1791776020002,"index":0,"dt":[],"texts":["MINIMAL_IMAGE_OK"]},{"type":"chunk","time":1791776020002,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_IMAGE_OK"}}},{"type":"chunk","time":1791776020002,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1791776020002,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/minimal-image-compaction/tool-schemas.expected.json b/snapshots/sdk/minimal-image-compaction/tool-schemas.expected.json index 5ed83c5ec3..ded9dfa451 100644 --- a/snapshots/sdk/minimal-image-compaction/tool-schemas.expected.json +++ b/snapshots/sdk/minimal-image-compaction/tool-schemas.expected.json @@ -47,6 +47,53 @@ } ], "changes": [ + [ + { + "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* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.\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": "read_image", + "description": "Read a PNG/JPEG/WebP/GIF file and return the image itself. Large images are downscaled automatically; do not install image libraries or create thumbnails to inspect an image.", + "parameters": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to the image file, resolved by the filesystem backend." + } + }, + "required": [ + "file_path" + ] + } + }, + { + "name": "working_directory", + "description": "Read the current working directory, or change it with cd. Relative paths use the current directory. Existing shells and running processes keep their own directories.", + "parameters": { + "type": "object", + "properties": { + "cd": { + "type": "string", + "description": "Existing directory to enter. Omit to read the current directory." + } + } + } + } + ], [ { "name": "bash", diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 0590dcc624..788293bc9e 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -962,6 +962,10 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { expect(summaries[0]?.data).not.toHaveProperty('sourceCommandId') expect(events.filter(event => event.type === 'compaction/end')).toHaveLength(1) expect(events.filter(event => event.type === 'compaction/error')).toHaveLength(0) + const offloads = events.filter(event => event.type === 'image/offload') + expect(offloads).toHaveLength(1) + const imageSeq = events.find(event => event.type === 'tool/result')?.seq + expect(offloads[0]).toMatchObject({ data: { targets: [{ seq: imageSeq, imageIndexes: [0] }] } }) const subscribed = notifications.flatMap(notification => { const event = notificationEvent(notification) return event?.type === 'compaction/summary' ? [event] : []