From 2b9badbc379637f45eac7f79ae1fbe30c27bc899 Mon Sep 17 00:00:00 2001 From: LogosVeritas Date: Mon, 26 Oct 2026 14:41:20 +0800 Subject: [PATCH] docs(sdk): clarify native command input and ordering Record empty-input migration and canonical-name limits. Verify strict alias rejection and command notifications preceding query responses. --- .../sdk-run-commands/guide.i18n.yaml | 8 +-- .../v0.2.1-rc.3/sdk-run-commands/guide.md | 13 ++-- .../v0.2.1-rc.3/sdk-run-commands/guide.zh.md | 13 ++-- packages/sdk/client/README.i18n.yaml | 4 +- packages/sdk/client/README.md | 2 +- packages/sdk/client/README.zh.md | 2 +- packages/sdk/protocol/README.i18n.yaml | 4 +- packages/sdk/protocol/README.md | 4 +- packages/sdk/protocol/README.zh.md | 4 +- packages/sdk/protocol/src/types.ts | 7 +- packages/sdk/server/README.i18n.yaml | 4 +- packages/sdk/server/README.md | 6 +- packages/sdk/server/README.zh.md | 6 +- packages/sdk/server/src/query-input.ts | 6 +- packages/sdk/server/src/server.ts | 1 + packages/sdk/server/tests/commands.spec.ts | 67 ++++++++++++++++++- python/sdk/README.i18n.yaml | 4 +- python/sdk/README.md | 2 +- python/sdk/README.zh.md | 2 +- 19 files changed, 120 insertions(+), 39 deletions(-) diff --git a/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.i18n.yaml index 4aaa66c58f..34756be6b3 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.i18n.yaml @@ -9,8 +9,8 @@ en: 3976928648fffa99 zh: 924afd63d5b211e0 /sdk-run-accepts-native-commands/change: - en: cd1f928f8a49692c - zh: 9d9a503298c5deb1 + en: 606b70efa209e059 + zh: 6c207a97b8342af0 /sdk-run-accepts-native-commands/migration: - en: 9d502135ae26478b - zh: acc2bc451c021c45 + en: 85e131a8ffa11589 + zh: 66866ac12ee34cf0 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.md index 86df9f3ef6..19657f5c1b 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.md +++ b/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.md @@ -13,10 +13,13 @@ Python `DeepSeekHarness.run()` and `Session.run()`, and their TypeScript equival Content-block arrays no longer bypass native slash-command routing. For example, `run([{"type": "text", "text": "/compact"}])` executes compaction. Arrays containing command text and encoded raster images follow Desktop's native attachment rules; `/plan off` with an image returns a command error without changing plan state. Command completion does not mean planning, review, or automatic goal rounds have finished. Handler refusals return a command error result; routing and transport failures raise or reject. +`run("")`, `run([])`, and inputs containing only blank text previously entered the prompt queue. They now fail with a JSON-RPC error before Session creation or prompt enqueue. Image-only input remains accepted. + ## Migration -1. Check the result before reading `final_response` in Python or `finalResponse` in TypeScript. In Python, use `isinstance(result, RunResult)`; in TypeScript, use `'finalResponse' in result`. A command result carries `command_id` in Python or `commandId` in TypeScript, plus `result.kind` and command-owned details. -2. To bypass native command routing, move the blocks to Python `client.session_prompt(session_id, blocks)` or TypeScript `client.prompt(sessionId, blocks)`. Subscribe before sending and manage subsequent notifications and `session/wait` yourself; these methods return an enqueue receipt, not `RunResult`. Runtime skill recognition still applies. -3. Select a model that supports image input. Submit command text and images together through `run([text_block, image_block])`; an encoded image contains `type: "image"`, base64 `data`, and `mimeType`. Place the complete command in the first text block; a recognized command rejects extra text blocks or non-encoded-image blocks instead of falling back to a prompt. Images may precede the text block. No additional input option is required. -4. Custom `session/query` clients must send `{ sessionId, contentBlocks, delivery }`; convert text into a `{ type: "text", text }` block. The text-only `session/command` and queued `session/prompt` methods retain their request fields. -5. Keep `on_notification` / `onNotification` for activity notifications. Follow the [SDK command completion rules](../../../../packages/sdk/server/README.md#command-input) for subsequent plan or goal work. Verify equivalent routing for strings and arrays, ordinary prompt delivery, `/plan off` image rejection, and low-level prompt bypass. +1. Filter blank strings, empty arrays, and arrays containing only blank text before calling `run()`. If raw queued input is required, use the low-level prompt methods below; enqueue acceptance does not guarantee the model accepts empty input. +2. Check the result before reading `final_response` in Python or `finalResponse` in TypeScript. In Python, use `isinstance(result, RunResult)`; in TypeScript, use `'finalResponse' in result`. A command result carries `command_id` in Python or `commandId` in TypeScript, plus `result.kind` and command-owned details. +3. To bypass native command routing, move the blocks to Python `client.session_prompt(session_id, blocks)` or TypeScript `client.prompt(sessionId, blocks)`. Subscribe before sending and manage subsequent notifications and `session/wait` yourself; these methods return an enqueue receipt, not `RunResult`. Runtime skill recognition still applies. +4. Select a model that supports image input. Submit command text and images together through `run([text_block, image_block])`; an encoded image contains `type: "image"`, base64 `data`, and `mimeType`. Place the complete command in the first text block; a recognized command rejects extra text blocks or non-encoded-image blocks instead of falling back to a prompt. Images may precede the text block. No additional input option is required. +5. Custom `session/query` clients must send `{ sessionId, contentBlocks, delivery }`; convert text into a `{ type: "text", text }` block. The text-only `session/command` and queued `session/prompt` methods retain their request fields. +6. Keep `on_notification` / `onNotification` for activity notifications. Follow the [SDK command completion rules](../../../../packages/sdk/server/README.md#command-input) for subsequent plan or goal work. Verify equivalent routing for strings and arrays, ordinary prompt delivery, `/plan off` image rejection, and low-level prompt bypass. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.zh.md index 7f23ccf89d..e16d2817d8 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-rc.3/sdk-run-commands/guide.zh.md @@ -13,10 +13,13 @@ Python 的 `DeepSeekHarness.run()`、`Session.run()` 及其 TypeScript 对应方 内容块数组不再绕过原生 slash command 路由。例如,`run([{"type": "text", "text": "/compact"}])` 会执行压缩。包含命令文本和编码栅格图片的数组遵循 Desktop 的原生附件规则;`/plan off` 携图返回命令错误,不修改计划状态。命令完成不表示计划生成、审阅或自动目标轮次已经结束。处理器拒绝操作时返回命令错误结果;路由和传输失败会抛出异常或拒绝 Promise。 +`run("")`、`run([])` 和仅包含空白文本的输入,原先会进入提示词队列。现在这些输入会在创建 Session 或提示词入队前,以 JSON-RPC 错误拒绝。仅图片输入仍然接受。 + ## 迁移 -1. 在 Python 中读取 `final_response`、在 TypeScript 中读取 `finalResponse` 前检查结果类型。Python 使用 `isinstance(result, RunResult)`,TypeScript 使用 `'finalResponse' in result`。命令结果在 Python 中携带 `command_id`,在 TypeScript 中携带 `commandId`,并包含 `result.kind` 和命令自身的结果字段。 -2. 要绕过原生命令路由,将内容块改为传入 Python `client.session_prompt(session_id, blocks)` 或 TypeScript `client.prompt(sessionId, blocks)`。发送前先订阅,自行管理后续通知与 `session/wait`;这些方法返回入队回执,而非 `RunResult`。运行时 skill(技能)识别仍然适用。 -3. 先选择支持图片输入的模型。通过 `run([text_block, image_block])` 同时提交命令文本和图片;编码图片包含 `type: "image"`、base64 `data` 和 `mimeType`。将完整命令放在第一个文本块中;已识别命令会拒绝额外文本块或非编码图片块,不回退为普通提示词。图片可位于文本块前。不需要额外的输入选项。 -4. 自定义 `session/query` 客户端必须发送 `{ sessionId, contentBlocks, delivery }`;将文本转换为 `{ type: "text", text }` 块。仅接受文本的 `session/command` 和排队输入的 `session/prompt` 保留原有请求字段。 -5. 继续使用 `on_notification` / `onNotification` 观察活动通知。plan 或 goal 的后续工作遵循 [SDK 命令完成规则](../../../../packages/sdk/server/README.zh.md#command-input)。验证字符串和数组路由一致、普通提示词投递、`/plan off` 携图拒绝,以及低层 prompt 绕过行为。 +1. 调用 `run()` 前过滤空白字符串、空数组和仅包含空白文本的数组。如果确实需要原始排队输入,使用下述低层 prompt 方法;入队成功不保证模型接受空输入。 +2. 在 Python 中读取 `final_response`、在 TypeScript 中读取 `finalResponse` 前检查结果类型。Python 使用 `isinstance(result, RunResult)`,TypeScript 使用 `'finalResponse' in result`。命令结果在 Python 中携带 `command_id`,在 TypeScript 中携带 `commandId`,并包含 `result.kind` 和命令自身的结果字段。 +3. 要绕过原生命令路由,将内容块改为传入 Python `client.session_prompt(session_id, blocks)` 或 TypeScript `client.prompt(sessionId, blocks)`。发送前先订阅,自行管理后续通知与 `session/wait`;这些方法返回入队回执,而非 `RunResult`。运行时 skill(技能)识别仍然适用。 +4. 先选择支持图片输入的模型。通过 `run([text_block, image_block])` 同时提交命令文本和图片;编码图片包含 `type: "image"`、base64 `data` 和 `mimeType`。将完整命令放在第一个文本块中;已识别命令会拒绝额外文本块或非编码图片块,不回退为普通提示词。图片可位于文本块前。不需要额外的输入选项。 +5. 自定义 `session/query` 客户端必须发送 `{ sessionId, contentBlocks, delivery }`;将文本转换为 `{ type: "text", text }` 块。仅接受文本的 `session/command` 和排队输入的 `session/prompt` 保留原有请求字段。 +6. 继续使用 `on_notification` / `onNotification` 观察活动通知。plan 或 goal 的后续工作遵循 [SDK 命令完成规则](../../../../packages/sdk/server/README.zh.md#command-input)。验证字符串和数组路由一致、普通提示词投递、`/plan off` 携图拒绝,以及低层 prompt 绕过行为。 diff --git a/packages/sdk/client/README.i18n.yaml b/packages/sdk/client/README.i18n.yaml index e0e0dac9b8..ca9b84828d 100644 --- a/packages/sdk/client/README.i18n.yaml +++ b/packages/sdk/client/README.i18n.yaml @@ -24,8 +24,8 @@ en: 026ad56ba7eb2d28 zh: 4adbe9b508bcb86e /deepseek-ai-dsh-sdk-client/use-this-package/lower-level-control-with-harnessclient: - en: 5ca96faa4c35e649 - zh: 77132086deca8519 + en: 2bbc630be84b1908 + zh: 211a53acc48b4582 /deepseek-ai-dsh-sdk-client/use-this-package/native-user-questions: en: f5b9d6a7363bd9e5 zh: f53b3786d22a729d diff --git a/packages/sdk/client/README.md b/packages/sdk/client/README.md index 4412855aee..854d42aca9 100644 --- a/packages/sdk/client/README.md +++ b/packages/sdk/client/README.md @@ -80,7 +80,7 @@ Launch options supply initial values. A permission command changes only its targ ### Lower-level control with HarnessClient -`client.query(sessionId, input, delivery)` accepts a string or content-block array and waits for a validated prompt receipt or command result. `client.executeCommand(sessionId, line)` executes supported command names without prompt fallback or attachments; it requires a non-blank permission preset. A non-blank unknown preset reaches the native handler and returns `result.kind === "error"`. Subscribe before these calls when notifications are needed. A request timeout abandons only the local wait; runtime work can continue afterward. +`client.query(sessionId, input, delivery)` accepts a string or content-block array and waits for a validated prompt receipt or command result. `client.executeCommand(sessionId, line)` accepts only supported canonical English command names, without prompt fallback or attachments; it requires a non-blank permission preset. Use `run()` or `query()` for Chinese aliases. A non-blank unknown preset reaches the native handler and returns `result.kind === "error"`. Subscribe before these calls when notifications are needed. A request timeout abandons only the local wait; runtime work can continue afterward. `/goal` uses the native goal handler and can start automatic rounds after its `CommandExecution`. Subscribe before sending; observe a terminal `goal/change` operation before using `session/wait` for the closing reply. Idle between rounds is not goal completion. See the [server semantics](../server/README.md) for pause, clear, restoration, and permission behavior. diff --git a/packages/sdk/client/README.zh.md b/packages/sdk/client/README.zh.md index 65fca58fb3..d360b80dd3 100644 --- a/packages/sdk/client/README.zh.md +++ b/packages/sdk/client/README.zh.md @@ -80,7 +80,7 @@ else console.log(result.finalResponse) ### 用 HarnessClient 做低层控制 -`client.query(sessionId, input, delivery)` 接受字符串或内容块数组,等待经过校验的提示词回执或命令结果。`client.executeCommand(sessionId, line)` 执行支持的命令名,不回退为提示词,也不接受附件;permission preset 必须非空。非空的未知 preset 会进入原生 handler,返回 `result.kind === "error"`。需要通知时,应在调用前订阅。请求超时仅放弃本地等待,运行时工作仍可继续。 +`client.query(sessionId, input, delivery)` 接受字符串或内容块数组,等待经过校验的提示词回执或命令结果。`client.executeCommand(sessionId, line)` 只接受受支持的英文规范命令名,不回退为提示词,也不接受附件;permission preset 必须非空。中文别名使用 `run()` 或 `query()`。非空的未知 preset 会进入原生 handler,返回 `result.kind === "error"`。需要通知时,应在调用前订阅。请求超时仅放弃本地等待,运行时工作仍可继续。 `/goal` 使用原生目标 handler,可在 `CommandExecution` 返回后启动自动轮次。发送前应订阅;先观察终态 `goal/change` 操作,再用 `session/wait` 等待收尾回复。轮次之间的 idle 不代表目标完成。暂停、清除、恢复和权限行为见[服务端语义](../server/README.zh.md)。 diff --git a/packages/sdk/protocol/README.i18n.yaml b/packages/sdk/protocol/README.i18n.yaml index 4f6e39765f..b68f1c14bd 100644 --- a/packages/sdk/protocol/README.i18n.yaml +++ b/packages/sdk/protocol/README.i18n.yaml @@ -21,8 +21,8 @@ en: be53c71505c0d1b4 zh: 6210dee577715539 /deepseek-ai-dsh-sdk-protocol/use-this-package/the-sdk-methods: - en: 2627f74033af9a8b - zh: 67ae6c42ff1170eb + en: 235f340c91f59334 + zh: fe12c84e383b53dd /deepseek-ai-dsh-sdk-protocol/use-this-package/native-user-questions: en: 65ea66231b17b6cf zh: 8e1c57d0c27f842f diff --git a/packages/sdk/protocol/README.md b/packages/sdk/protocol/README.md index 745c51b639..f2598026a8 100644 --- a/packages/sdk/protocol/README.md +++ b/packages/sdk/protocol/README.md @@ -62,7 +62,9 @@ Both wire ends share one method set: client-to-server requests, opt-in server-to `session/query` accepts `{ sessionId, contentBlocks, delivery }`, where `contentBlocks` uses `SdkPromptContentBlock[]` and delivery is `queue` or `steer`. Both SDKs normalize string input into a text block and send content-block arrays through this same entry. The runtime replies with `{ kind: "prompt", messageId }` for ordinary input, preserving its block order, or `{ kind: "command", execution }` after a supported command settles. A recognized command requires one complete text block with only encoded images beside it; other combinations reject before execution or enqueue. The images go through native attachment admission and handler validation. Empty arrays and arrays containing only blank text are rejected; image-only input is allowed. A prompt receipt is not a model result; command completion does not require an inbox receipt or idle notification. Recognition and the supported command set belong to the [SDK server](../server/README.md). -`session/command` accepts `{ sessionId, line }` after initialization and delegates the [supported command names](../server/README.md#command-input) to the native registry. A non-blank preset is required; bare `/permission` is rejected. It returns `CommandExecution` after the handler settles, including an unknown preset or other business failure as `result.kind: "error"`. Unavailable or unsupported commands and malformed requests reject with a JSON-RPC error. There is no prompt fallback or attachment input; use `session/query` to submit commands with encoded images. Command lifecycle notifications precede the response; no inbox receipt or ordinary turn is required. `session/prompt` queues content blocks without native slash-command routing; runtime skill recognition still applies. +`session/command` accepts `{ sessionId, line }` after initialization and delegates the [supported canonical English command names](../server/README.md#command-input) to the native registry. Chinese aliases use `session/query`. A non-blank preset is required; bare `/permission` is rejected. It returns `CommandExecution` after the handler settles, including an unknown preset or other business failure as `result.kind: "error"`. Unavailable or unsupported commands and malformed requests reject with a JSON-RPC error. There is no prompt fallback or attachment input; use `session/query` to submit commands with encoded images. No inbox receipt or ordinary turn is required. `session/prompt` queues content blocks without native slash-command routing; runtime skill recognition still applies. + +Command lifecycle notifications precede their response on the same stream for both `session/command` and the command result of `session/query`. Forwarders must preserve this order. `session/resume` accepts a non-empty persisted `{ sessionId }` and returns `{}` after restoring that exact session. It requires initialization and the original persistence store, sends no new input, and rejects missing, unreadable, incompatible, or occupied sessions without creating a replacement. Historical events remain in the stored log; notifications report newly committed events. diff --git a/packages/sdk/protocol/README.zh.md b/packages/sdk/protocol/README.zh.md index 5598a677c1..3ee4b4ff2e 100644 --- a/packages/sdk/protocol/README.zh.md +++ b/packages/sdk/protocol/README.zh.md @@ -62,7 +62,9 @@ kind: "package-library" `session/query` 接受 `{ sessionId, contentBlocks, delivery }`,其中 `contentBlocks` 使用 `SdkPromptContentBlock[]`,delivery 为 `queue` 或 `steer`。两个 SDK 都将字符串规范化为文本块,并通过同一入口发送内容块数组。普通输入保留内容块顺序,返回 `{ kind: "prompt", messageId }`;受支持命令结束后返回 `{ kind: "command", execution }`。已识别命令要求一个完整文本块,其他块只能是编码图片;其他组合会在执行或入队前被拒绝。图片经过原生附件接纳与处理器校验。空数组和只包含空白文本的数组会被拒绝;允许仅图片输入。Prompt 回执不是模型结果,命令完成也不要求 inbox 回执或 idle 通知。识别规则及受支持命令由 [SDK 服务端](../server/README.zh.md)定义。 -`session/command` 在初始化后接受 `{ sessionId, line }`,将[支持的命令名](../server/README.zh.md#command-input)委托给原生注册器。preset 必须非空,裸 `/permission` 会被拒绝。Handler 结束后返回 `CommandExecution`,未知 preset 或其他业务失败以 `result.kind: "error"` 返回。命令不可用、不受支持或请求格式错误时返回 JSON-RPC 错误。不回退为普通 prompt,也不接受附件;携带编码图片的命令使用 `session/query`。命令生命周期通知在响应前到达,不要求 inbox 回执或普通 turn。`session/prompt` 将内容块排入队列,不经过原生 slash command 路由;运行时 skill(技能)识别仍然适用。 +`session/command` 在初始化后接受 `{ sessionId, line }`,将[支持的英文规范命令名](../server/README.zh.md#command-input)委托给原生注册器。中文别名使用 `session/query`。preset 必须非空,裸 `/permission` 会被拒绝。Handler 结束后返回 `CommandExecution`,未知 preset 或其他业务失败以 `result.kind: "error"` 返回。命令不可用、不受支持或请求格式错误时返回 JSON-RPC 错误。不回退为普通 prompt,也不接受附件;携带编码图片的命令使用 `session/query`。不要求 inbox 回执或普通 turn。`session/prompt` 将内容块排入队列,不经过原生 slash command 路由;运行时 skill(技能)识别仍然适用。 + +对于 `session/command` 和 `session/query` 的命令结果,命令生命周期通知都在同一流上先于对应响应到达。转发方必须保留这一顺序。 `session/resume` 接受非空的持久化 `{ sessionId }`,恢复该会话后返回 `{}`。它要求先完成初始化并使用原持久化存储,不发送新输入;会话缺失、不可读、不兼容或被占用时,请求失败且不创建替代会话。历史事件保留在存储日志中,通知只报告新提交的事件。 diff --git a/packages/sdk/protocol/src/types.ts b/packages/sdk/protocol/src/types.ts index 091b38cc65..b226b7471b 100644 --- a/packages/sdk/protocol/src/types.ts +++ b/packages/sdk/protocol/src/types.ts @@ -99,7 +99,7 @@ export interface SessionPromptResult { export interface SessionCommandParams { /** SDK-owned Session identity; an unknown id creates its Agent. */ sessionId: SessionId - /** Complete command line; invalid arguments are handled by the native command. */ + /** Complete command line using the canonical English name; the native command validates arguments. */ line: string } @@ -113,7 +113,10 @@ export interface SessionQueryParams { delivery: 'queue' | 'steer' } -/** A prompt's enqueue receipt or a settled native command; neither means all Session work ended. */ +/** + * A prompt's enqueue receipt or a settled native command; neither means all Session work ended. + * Command lifecycle notifications precede the command response on the same stream. + */ export type SessionQueryResult = | ({ kind: 'prompt' } & SessionPromptResult) | { kind: 'command'; execution: CommandExecution } diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml index c0a7d201fd..2a4b856c5f 100644 --- a/packages/sdk/server/README.i18n.yaml +++ b/packages/sdk/server/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package: - en: 7a012ceb1dbc150e - zh: b885503305cf1e21 + en: b19a32cb7fe97056 + zh: d7d656b5b4385370 /deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/wiring: en: 47a33e9397e36e15 zh: e6d173820f9ea6e5 diff --git a/packages/sdk/server/README.md b/packages/sdk/server/README.md index 20b7f89bac..3f688b6f4f 100644 --- a/packages/sdk/server/README.md +++ b/packages/sdk/server/README.md @@ -37,11 +37,13 @@ The plugin publishes the `sdkSessionActivation` readiness marker once its remind The supported commands below are existing Desktop native operations that can execute without a GUI. `/permission ` uses that native path; the Desktop `/model` picker uses a separate model-selection API. -`session/query` is the wire entry used by high-level SDK `run()` for strings and content-block arrays. Ordinary inputs retain their blocks and use the selected queue/steer operation. The full SDK recognizes bare `/compact`, `/permission ` with a non-blank preset, `/plan [off|message]`, and `/goal [objective|clear|edit objective|pause|resume]`, including Desktop aliases bound to their current definitions. Bare `/permission` or `/权限`, `/compact extra`, and all other input including `/model` remain ordinary prompts. A missing command also preserves the prompt path; a selected handler's failure never falls back. An unknown preset is a native command error. Encoded image blocks accompany recognized commands through native attachment admission. `session/prompt` remains the low-level queued-input entry without native slash-command routing. +`session/query` is the wire entry used by high-level SDK `run()` for strings and content-block arrays. Ordinary inputs retain their blocks and use the selected queue/steer operation. The full SDK recognizes bare `/compact`, `/permission ` with a non-blank preset, `/plan [off|message]`, and `/goal [objective|clear|edit objective|pause|resume]` under those fixed canonical names. Bare `/permission` or `/权限`, `/compact extra`, and all other input including `/model` remain ordinary prompts. A missing command also preserves the prompt path; a selected handler's failure never falls back. An unknown preset is a native command error. Encoded image blocks accompany recognized commands through native attachment admission. `session/prompt` remains the low-level queued-input entry without native slash-command routing. + +The fixed Chinese aliases are `/压缩` → `compact`, `/权限` → `permission`, `/计划` → `plan`, and `/目标` → `goal`. Each alias requires the corresponding official `definitionId` and canonical registered name. Custom command renaming is unsupported even when `definitionId` is preserved. The first text block supplies the complete command line; encoded images may precede or follow it. When that line matches a supported command, it must be the only text block and every other block must be an encoded image. Additional text blocks, durable image references, and other content types reject with a JSON-RPC error before execution or enqueue, without prompt fallback. Ordinary prompts preserve all block content and ordering. Empty arrays and arrays containing only blank text are rejected; image-only input is allowed. -`session/command` accepts `{ sessionId, line }` for the supported command names above, requires a non-blank permission preset, and lets native handlers validate other arguments. The full sdk profile composes these native handlers; profiles without the requested command reject it. The server resolves its own live Agent and calls `commands.execute()` without queueing a prompt or waiting for idle. Compaction owns its auxiliary model request, history replacement and checkpoint, and refuses a busy Agent. Permission changes retain their native state and notification behavior. Shutdown cancels pending commands and drains SDK command waiters before releasing Agents. Timeout is not rollback; clients must not retry automatically. See the [protocol](../protocol/README.md) for results and errors. +`session/command` accepts `{ sessionId, line }` with English canonical command names only; Chinese aliases and custom renamed commands are rejected with a JSON-RPC error. It requires a non-blank permission preset and lets native handlers validate other arguments. The full sdk profile composes these native handlers; profiles without the requested command reject it. The server resolves its own live Agent and calls `commands.execute()` without queueing a prompt or waiting for idle. Compaction owns its auxiliary model request, history replacement and checkpoint, and refuses a busy Agent. Permission changes retain their native state and notification behavior. Shutdown cancels pending commands and drains SDK command waiters before releasing Agents. Timeout is not rollback; clients must not retry automatically. See the [protocol](../protocol/README.md) for results and errors. Permission commands change only the receiving Session. Later queries and restored Sessions retain its persisted permission facts; other Sessions keep their own state and new Sessions use the configured defaults. Neither the caller's launch configuration nor saved user defaults are rewritten. Model selection is not an SDK slash command; its resume override remains the separate behavior described above. diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md index 487d8b85c3..f4fe4949c3 100644 --- a/packages/sdk/server/README.zh.md +++ b/packages/sdk/server/README.zh.md @@ -37,11 +37,13 @@ kind: "package-reference" 下列支持范围是桌面已有、无需图形界面即可执行的原生命令。`/permission ` 使用这条原生调用路径;Desktop 的 `/model` 选择器使用独立的模型选择 API。 -`session/query` 是高层 SDK `run()` 发送字符串和内容块数组时使用的协议入口。普通输入保留内容块,使用选定的 queue/steer 操作。完整 SDK 识别裸 `/compact`、带非空 preset 的 `/permission `、`/plan [off|message]` 和 `/goal [objective|clear|edit objective|pause|resume]`,包括绑定到当前定义的 Desktop 别名。裸 `/permission` 或 `/权限`、`/compact extra` 以及 `/model` 等其余输入仍是普通提示词。命令缺失时也保留提示词路径;选中的 handler 失败后绝不回退。未知 preset 是原生命令错误。随已识别命令提交的编码图片块经过原生附件接纳。`session/prompt` 保留为低层排队输入入口,不经过原生 slash command 路由。 +`session/query` 是高层 SDK `run()` 发送字符串和内容块数组时使用的协议入口。普通输入保留内容块,使用选定的 queue/steer 操作。完整 SDK 识别裸 `/compact`、带非空 preset 的 `/permission `、`/plan [off|message]` 和 `/goal [objective|clear|edit objective|pause|resume]`,这些命令必须保留上述固定规范名。裸 `/permission` 或 `/权限`、`/compact extra` 以及 `/model` 等其余输入仍是普通提示词。命令缺失时也保留提示词路径;选中的 handler 失败后绝不回退。未知 preset 是原生命令错误。随已识别命令提交的编码图片块经过原生附件接纳。`session/prompt` 保留为低层排队输入入口,不经过原生 slash command 路由。 + +固定中文别名为 `/压缩` → `compact`、`/权限` → `permission`、`/计划` → `plan` 和 `/目标` → `goal`。每个别名都要求对应的官方 `definitionId` 和规范注册名;即使保留 `definitionId`,自定义重命名后的命令也不受支持。 第一个文本块提供完整命令行;编码图片可位于它之前或之后。该行匹配到受支持命令时,它必须是唯一的文本块,其余块必须为编码图片。额外文本块、持久图片引用及其他内容类型会在执行或入队前返回 JSON-RPC 错误,不回退为普通提示词。普通提示词保留全部内容块及其顺序。空数组和只包含空白文本的数组会被拒绝;允许仅图片输入。 -`session/command` 接受上述命令名的 `{ sessionId, line }`,要求 permission preset 非空,其余参数由原生 handler 校验。完整 sdk profile 组成这些原生 handler;缺少所请求命令的 profile 会拒绝调用。服务端解析自己拥有的存活 Agent,并调用 `commands.execute()`,不排入普通 prompt,也不等待 idle。压缩负责其辅助模型请求、历史替换和 checkpoint,并拒绝忙碌 Agent。权限变更保留原生状态和通知行为。Shutdown 取消待决命令,在释放 Agent 前排空 SDK 命令等待者。超时不等于回滚,客户端不能自动重试。结果和错误见[协议](../protocol/README.zh.md)。 +`session/command` 接受 `{ sessionId, line }`,只识别英文规范命令名;中文别名和自定义重命名后的命令会被拒绝,并返回 JSON-RPC 错误。它要求 permission preset 非空,其余参数由原生 handler 校验。完整 sdk profile 组成这些原生 handler;缺少所请求命令的 profile 会拒绝调用。服务端解析自己拥有的存活 Agent,并调用 `commands.execute()`,不排入普通 prompt,也不等待 idle。压缩负责其辅助模型请求、历史替换和 checkpoint,并拒绝忙碌 Agent。权限变更保留原生状态和通知行为。Shutdown 取消待决命令,在释放 Agent 前排空 SDK 命令等待者。超时不等于回滚,客户端不能自动重试。结果和错误见[协议](../protocol/README.zh.md)。 权限命令只修改接收命令的 Session。后续输入和恢复的 Session 保留其持久化权限事实;其他 Session 保留各自状态,新 Session 使用配置的默认值。调用方的启动配置和已保存的用户默认值均不会被重写。模型选择不是 SDK slash 命令;恢复时的模型覆盖仍按上文描述的独立行为执行。 diff --git a/packages/sdk/server/src/query-input.ts b/packages/sdk/server/src/query-input.ts index e5d86142c6..e093004fc5 100644 --- a/packages/sdk/server/src/query-input.ts +++ b/packages/sdk/server/src/query-input.ts @@ -3,10 +3,12 @@ import type { CommandDescriptor } from '@deepseek-ai/dsh-commands/types' /** - * Recognize compact, explicit permission changes, plan, and goal input; other text remains a prompt. + * Recognize compact, explicit permission changes, plan, and goal using fixed names or Chinese aliases. + * Aliases require the official definitionId and canonical registered name; custom renaming is unsupported. + * Other text remains a prompt. * @param text - original input; the caller preserves it on the ordinary-prompt path. * @param commands - exact receiving Agent's current command descriptors. - * @returns the supported command line using its effective name and a native-compatible separator, otherwise undefined. + * @returns the supported command line using its canonical name and a native-compatible separator, otherwise undefined. */ export function queryCommand(text: string, commands: readonly CommandDescriptor[]): string | undefined { const trimmed = text.trim() diff --git a/packages/sdk/server/src/server.ts b/packages/sdk/server/src/server.ts index ce0bb31ab3..ce330c302c 100644 --- a/packages/sdk/server/src/server.ts +++ b/packages/sdk/server/src/server.ts @@ -317,6 +317,7 @@ export class HarnessSdkJsonRpcServer { /** * Execute supported compact, permission, plan, or goal input through the native command registry. + * Only English canonical names are accepted; Chinese aliases and custom renamed commands are rejected. * @param params - target Session and complete command line. * @returns the native handler result; command errors remain result values. * @throws for unsupported input, unavailable commands, or runtime/transport failure. diff --git a/packages/sdk/server/tests/commands.spec.ts b/packages/sdk/server/tests/commands.spec.ts index 66e016ab79..a0d9376e48 100644 --- a/packages/sdk/server/tests/commands.spec.ts +++ b/packages/sdk/server/tests/commands.spec.ts @@ -1,11 +1,13 @@ import { Context } from '@deepseek-ai/cordis' +import { once } from 'node:events' +import { PassThrough } from 'node:stream' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import CommandRuntime, { CommandDefinitionId, type CommandDefinition, type CommandInvocation } from '@deepseek-ai/dsh-commands' import { AttachmentId, type AttachmentStore, type ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' import { LlmAdapter, type GenerateOptions, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' -import type { JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol' +import { JsonRpcLineTransport, JsonRpcResponseError, type JsonRpcTransportPeer } from '@deepseek-ai/dsh-sdk-protocol' import { describe, expect, it, onTestFinished, vi } from 'vitest' import { HarnessSdkJsonRpcServer } from '../src/server.ts' @@ -35,14 +37,14 @@ class TextModel extends LlmAdapter { } } -async function setup(registry = true) { +async function setup(registry = true, peer?: JsonRpcTransportPeer) { const ctx = new Context() onTestFinished(() => ctx.fiber.dispose()) await mountAgentLoopTestDependencies(ctx, { workingDirectory: true }) const loop = await ctx.plugin(AgentLoop, { agents: [] }) if (registry) await ctx.plugin(CommandRuntime) ctx.llm.registerAdapter(['mock'], new UnusedModel()) - const transport: JsonRpcTransportPeer = { + const transport: JsonRpcTransportPeer = peer ?? { request: async () => { throw new Error('Unexpected runtime request') }, notify: () => {}, } @@ -208,6 +210,65 @@ describe('SDK command dispatch', () => { expect(h.ctx.agents.list()).toEqual([]) }) + it.each([ + ['compact', '@deepseek-ai/dsh-command-compact', '/compact', '/压缩'], + ['permission', '@deepseek-ai/dsh-permission-presets', '/permission read-only', '/权限 read-only'], + ['plan', '@deepseek-ai/dsh-plan-mode', '/plan', '/计划'], + ['goal', '@deepseek-ai/dsh-command-goal', '/goal', '/目标'], + ])('accepts the %s alias only through query and delivers its lifecycle before resolution', async (name, definitionId, line, alias) => { + const input = new PassThrough() + const output = new PassThrough() + const serverWire = new JsonRpcLineTransport(input, output) + const client = new JsonRpcLineTransport(output, input) + onTestFinished(async () => { + serverWire.close() + client.close() + const closed = [once(input, 'close'), once(output, 'close')] + input.destroy() + output.destroy() + await Promise.all(closed) + }) + const h = await setup(true, serverWire) + const received: { method: string; params: Record }[] = [] + let receivedAtQueryResolution: typeof received = [] + client.onNotification((method, params) => { received.push({ method, params }) }) + serverWire.onRequest(async (method, params) => { + const result = await h.server.handleRequest(method, params) + if (method === 'session/query') receivedAtQueryResolution = [...received] + return result + }) + serverWire.start() + client.start() + await client.request('initialize', { cwd: process.cwd(), provider: 'mock', model: 'mock' }) + const handler = vi.fn((_invocation: CommandInvocation) => ({ kind: 'success' as const, text: 'native outcome' })) + h.ctx.effect(() => h.ctx.commands.register({ + name, definitionId: CommandDefinitionId(definitionId), description: 'Fixture command', handler, + })) + const sessionId = SessionId('alias-command') + const rejected = client.request('session/command', { sessionId, line: alias }) + await expect(rejected).rejects.toBeInstanceOf(JsonRpcResponseError) + await expect(rejected).rejects.toMatchObject({ code: -32603 }) + await expect(rejected).rejects.toThrow('only /compact') + expect(handler).not.toHaveBeenCalled() + expect(received).toEqual([]) + expect(h.ctx.agents.list()).toEqual([]) + + await expect(client.request('session/command', { sessionId, line })) + .resolves.toMatchObject({ result: { kind: 'success', text: 'native outcome' } }) + expect(handler).toHaveBeenCalledOnce() + received.length = 0 + const result = await client.request('session/query', { sessionId, contentBlocks: [{ type: 'text', text: alias }], delivery: 'queue' }) + expect(handler).toHaveBeenCalledTimes(2) + const commandId = handler.mock.calls[1]![0].commandId + expect(result).toMatchObject({ kind: 'command', execution: { commandId, result: { kind: 'success', text: 'native outcome' } } }) + expect(receivedAtQueryResolution).toMatchObject([ + { method: 'session.event', params: { sessionId, event: { type: 'command/run', data: { commandId, name } } } }, + { method: 'session.event', params: { sessionId, event: { type: 'command/done', data: { commandId, kind: 'success' } } } }, + ]) + expect(h.ctx.agents.get(sessionId)!.session.snapshotEvents().map(event => event.type)) + .toEqual(['command/run', 'command/done', 'command/run', 'command/done']) + }) + it('requires initialization and the composed registry and command', async () => { const absent = await setup(false) await expect(absent.command()).rejects.toThrow('not initialized') diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 64de157a1d..22552558f3 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -12,8 +12,8 @@ en: ac793c588b13ce87 zh: 502015cda46de3ac /deepseek-harness-python-sdk/run-text-and-commands: - en: bfdb0c48d5a29f15 - zh: b6164533835279dc + en: 6ff1985e095b3022 + zh: 1a8373d3a9b787bb /deepseek-harness-python-sdk/client-approvals: en: fc69be1beb8abff1 zh: 91b56781931a6f68 diff --git a/python/sdk/README.md b/python/sdk/README.md index f48d167b5f..50aa2eb25c 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -71,7 +71,7 @@ Select a model that supports image input. Submit a command and image in the exis Each call subscribes before sending and releases its subscription before returning or raising. `on_notification` receives the ordinary activity interval in wire order; command callbacks replay Session-tree notifications from subscription through their own `command/done` after the command response, so that interval can include overlapping work. Calls may overlap without consuming each other's results. Handler refusals return `CommandExecution.result.kind="error"`; routing, protocol, transport and observer failures raise. A timeout or observer failure does not roll back runtime work or authorize an automatic retry. `request_timeout_seconds` bounds the protocol response wait, not later ordinary prompt activity. -The default full `sdk` profile provides the supported native commands; `sdk-minimal` keeps unsupported input on its ordinary prompt path. `/compact` can make an auxiliary model request and completes without a normal turn or idle notification; empty history can succeed without a summary. The lower-level `HarnessClient.session_command(session_id, line)` executes supported command names without prompt fallback or attachments. See the [compaction example](examples/README.md#compact-a-full-sdk-session). +The default full `sdk` profile provides the supported native commands; `sdk-minimal` keeps unsupported input on its ordinary prompt path. `/compact` can make an auxiliary model request and completes without a normal turn or idle notification; empty history can succeed without a summary. The lower-level `HarnessClient.session_command(session_id, line)` accepts only supported canonical English command names, without prompt fallback or attachments. Use `run()` for Chinese aliases. See the [compaction example](examples/README.md#compact-a-full-sdk-session). `/permission ` changes only the receiving Session. An unknown preset returns a command error without changing policy. Later input and restoration retain successful changes; other Sessions and the caller's `DeepSeekHarnessConfig` keep their own values. A running approval-policy change can inject a notice and cause another model step. See the [permission example](examples/README.md#change-session-permissions). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 0320654c4c..5409cdb090 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -71,7 +71,7 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness: 每次调用在发送前订阅,并在返回或抛出异常前释放自己的订阅。`on_notification` 按线上的顺序接收普通活动区间通知;命令回调在命令响应到达后,按顺序交付从订阅时刻到自身 `command/done` 的 Session 树通知,因此该区间可能包含重叠工作的通知。调用可以重叠,不会消耗彼此的结果。处理器拒绝返回 `CommandExecution.result.kind="error"`,路由、协议、传输和观察回调失败则抛出异常。超时或观察回调失败不回滚运行时工作,也不授权自动重试。`request_timeout_seconds` 限制协议响应等待,不限制之后的普通提示词活动。 -默认的完整 `sdk` profile 提供已支持的原生命令;`sdk-minimal` 将未支持的输入保留在普通提示词路径。`/compact` 可以发起辅助模型请求,其完成不需要普通 turn 或 idle 通知;空历史可以在不生成摘要时成功。底层 `HarnessClient.session_command(session_id, line)` 严格执行支持的命令名,不回退为提示词,也不接受附件。见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。 +默认的完整 `sdk` profile 提供已支持的原生命令;`sdk-minimal` 将未支持的输入保留在普通提示词路径。`/compact` 可以发起辅助模型请求,其完成不需要普通 turn 或 idle 通知;空历史可以在不生成摘要时成功。底层 `HarnessClient.session_command(session_id, line)` 只接受受支持的英文规范命令名,不回退为提示词,也不接受附件。中文别名使用 `run()`。见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。 `/permission ` 只修改接收命令的 Session。未知 preset 返回命令错误,不改变策略。后续输入与恢复保留成功的修改,其他 Session 和调用方的 `DeepSeekHarnessConfig` 保留自己的值。运行中的审批策略变化可以注入通知并触发下一模型步骤。见[权限示例](examples/README.zh.md#change-session-permissions)。