diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.i18n.yaml index c5105311c6..e5444d2f37 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.i18n.yaml @@ -9,8 +9,8 @@ en: 54f8a31426f4758c zh: 283820d3f187092e /sdk-run-accepts-native-commands/change: - en: bc2b0a35fa7e0ac8 - zh: b16695b1c2941070 + en: 4ded2e89cef962d2 + zh: 097e25c63d81bc15 /sdk-run-accepts-native-commands/migration: - en: 7f7badce57324d22 - zh: af36089bf5a6346e + en: 302378fd8aeb24c5 + zh: 854a98cf76e83323 diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.md b/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.md index 35e35422bf..815a7b1379 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.md +++ b/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.md @@ -9,7 +9,7 @@ English | [中文](guide.zh.md) ## Change -Python `DeepSeekHarness.run()` and `Session.run()`, and their TypeScript equivalents, route strings and content-block arrays through one prompt-or-command entry. Ordinary input returns `RunResult`; supported commands return `CommandExecution`. Commands are identified by their official definition IDs and execute under their current registered names. English and Chinese aliases follow renamed definitions; an unrelated exact-name replacement remains ordinary input. See the [supported command rules](../../../../packages/sdk/server/README.md#command-input). +Python `DeepSeekHarness.run()` and `Session.run()`, and their TypeScript equivalents, keep the existing public `run()` entry for strings and content-block arrays and route both as prompts or commands. Ordinary input returns `RunResult`; supported commands return `RunCommandResult`. Commands are identified by their official definition IDs and execute under their current registered names. English and Chinese aliases follow renamed definitions; an unrelated exact-name replacement remains ordinary input. See the [supported command rules](../../../../packages/sdk/server/README.md#command-input). Arrays no longer bypass native routing: `run([{"type": "text", "text": "/compact"}])` executes compaction. Commands with encoded raster images follow native attachment rules; `/plan off` with an image returns a command error without changing plan state. Command completion does not mean subsequent planning, review, or goal rounds have finished. Handler refusals remain command results; routing and transport failures raise or reject. @@ -17,7 +17,7 @@ Arrays no longer bypass native routing: `run([{"type": "text", "text": "/compact ## Migration -1. Filter empty or blank input before `run()`. Check the returned type before reading `final_response` in Python or `finalResponse` in TypeScript: use `isinstance(result, RunResult)` or `'finalResponse' in result`. Command results carry `command_id`/`commandId` and `result.kind`. +1. Filter empty or blank input before `run()`. Check the returned type before reading `final_response` in Python or `finalResponse` in TypeScript: use `isinstance(result, RunResult)` or `'finalResponse' in result`. Both results carry `session_id`/`sessionId`, including for an automatically created Session. `RunCommandResult` also retains the native `CommandExecution` fields, including `command_id`/`commandId` and `result.kind`. 2. For literal queued input, use Python `client.session_prompt(session_id, blocks)` or TypeScript `client.prompt(sessionId, blocks)`. Subscribe first and manage notifications and `session/wait`; these methods return receipts, not `RunResult`. Enqueue acceptance does not guarantee the model accepts empty input. Runtime skill recognition still applies. 3. For images, select an image-capable model and use the existing `run([text_block, image_block])` form. Encoded images contain `type: "image"`, base64 `data`, and `mimeType`. A matched command requires one complete text block and only encoded images beside it; extra text or other attachments reject without prompt fallback. Images may precede the text block. 4. Custom `session/query` clients send `{ sessionId, contentBlocks, delivery }`; delivery is `queue` or `steer` for ordinary prompts. `session/command` keeps `{ sessionId, line }` and accepts ASCII registered names or canonical English aliases of supported definitions. Chinese aliases use `run()`/`session/query`; a matching name alone does not establish builtin identity. diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.zh.md b/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.zh.md index fd0efd52f2..1968791eaf 100644 --- a/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.zh.md +++ b/docs/upgrade-guide/v0.2.2-alpha.2/sdk-run-commands/guide.zh.md @@ -9,7 +9,7 @@ description: "SDK run 按定义身份路由原生命令,返回提示词活动 ## 变更 -Python 的 `DeepSeekHarness.run()`、`Session.run()` 及其 TypeScript 对应方法,将字符串与内容块数组发送到同一提示词/命令入口。普通输入返回 `RunResult`,受支持命令返回 `CommandExecution`。命令按官方定义 ID 识别,并以当前注册名执行。中英文别名跟随重命名后的定义,无关的精确同名替换仍作为普通输入。详见[受支持命令规则](../../../../packages/sdk/server/README.zh.md#command-input)。 +Python 的 `DeepSeekHarness.run()`、`Session.run()` 及其 TypeScript 对应方法,沿用现有公开 `run()` 入口接收字符串与内容块数组,并将两者路由为提示词或命令。普通输入返回 `RunResult`,受支持命令返回 `RunCommandResult`。命令按官方定义 ID 识别,并以当前注册名执行。中英文别名跟随重命名后的定义,无关的精确同名替换仍作为普通输入。详见[受支持命令规则](../../../../packages/sdk/server/README.zh.md#command-input)。 数组不再绕过原生路由:`run([{"type": "text", "text": "/compact"}])` 会执行压缩。命令携带编码栅格图片时遵循原生附件规则;`/plan off` 携带图片会返回命令错误,且不改变计划状态。命令完成不表示后续计划、审阅或目标轮次已经结束。处理器拒绝仍是命令结果,路由与传输失败会抛出异常或拒绝。 @@ -17,7 +17,7 @@ Python 的 `DeepSeekHarness.run()`、`Session.run()` 及其 TypeScript 对应方 ## 迁移 -1. 在调用 `run()` 前过滤空输入或空白输入。在 Python 读取 `final_response` 或 TypeScript 读取 `finalResponse` 前检查返回类型:使用 `isinstance(result, RunResult)` 或 `'finalResponse' in result`。命令结果包含 `command_id`/`commandId` 及 `result.kind`。 +1. 在调用 `run()` 前过滤空输入或空白输入。在 Python 读取 `final_response` 或 TypeScript 读取 `finalResponse` 前检查返回类型:使用 `isinstance(result, RunResult)` 或 `'finalResponse' in result`。两种结果都包含 `session_id`/`sessionId`,包括自动创建的 Session。`RunCommandResult` 还保留原生 `CommandExecution` 字段,包括 `command_id`/`commandId` 及 `result.kind`。 2. 原样排队输入使用 Python `client.session_prompt(session_id, blocks)` 或 TypeScript `client.prompt(sessionId, blocks)`。先订阅,再管理通知及 `session/wait`;这些方法返回回执,而不是 `RunResult`。入队被接受不保证模型接受空输入。运行时技能识别仍适用。 3. 图片输入选择支持图片的模型,沿用 `run([text_block, image_block])`。编码图片包含 `type: "image"`、base64 `data` 和 `mimeType`。匹配到的命令要求一个完整文本块,其余只能是编码图片;额外文本或其他附件会被拒绝,不回退为提示词。图片可放在文本块之前。 4. 自定义 `session/query` 客户端发送 `{ sessionId, contentBlocks, delivery }`,普通提示词的 delivery 为 `queue` 或 `steer`。`session/command` 保留 `{ sessionId, line }`,接受受支持定义的 ASCII 注册名或英文规范别名。中文别名使用 `run()`/`session/query`;名称相同不代表内置定义身份。 diff --git a/packages/sdk/client/README.i18n.yaml b/packages/sdk/client/README.i18n.yaml index 11a39561e9..ea487c70d8 100644 --- a/packages/sdk/client/README.i18n.yaml +++ b/packages/sdk/client/README.i18n.yaml @@ -21,11 +21,11 @@ en: 0e155c826651ff5e zh: 874fceaa9c13e3fa /deepseek-ai-dsh-sdk-client/use-this-package/run-text-and-native-commands: - en: 026ad56ba7eb2d28 - zh: 4adbe9b508bcb86e + en: cb3ec67ee5c0575d + zh: 33a6b19ef65b5d14 /deepseek-ai-dsh-sdk-client/use-this-package/lower-level-control-with-harnessclient: - en: 2fef48f0f14072be - zh: 198ed6524838f656 + en: 823c71066513eb8d + zh: 83970539344ea4f7 /deepseek-ai-dsh-sdk-client/use-this-package/native-user-questions: en: f5b9d6a7363bd9e5 zh: f53b3786d22a729d @@ -33,11 +33,11 @@ en: 8773f50c51da85f0 zh: 4649ebc69357deb0 /deepseek-ai-dsh-sdk-client/understand-the-implementation/design-concept: - en: 6632c06d819233cc - zh: 207d55d8217ec6b1 + en: ac3c6eae3f8248fc + zh: b929079016396c8e /deepseek-ai-dsh-sdk-client/understand-the-implementation/source-map: - en: fd2b42bb40dfd445 - zh: 45cbb461b1c99759 + en: 2a4da3d0785201b6 + zh: 56bbe428222a5805 /deepseek-ai-dsh-sdk-client/understand-the-implementation/owned-activity-flow: en: 94a42babac5f1c7d zh: df9e42ed02f7d002 diff --git a/packages/sdk/client/README.md b/packages/sdk/client/README.md index 163d54d1a2..b997ad6066 100644 --- a/packages/sdk/client/README.md +++ b/packages/sdk/client/README.md @@ -70,19 +70,19 @@ if ('commandId' in result) console.log(result.result) else console.log(result.finalResponse) ``` -Both string and content-block input return `CommandExecution` for a command or `RunResult` for ordinary prompt activity. Commands settle without requiring a turn or idle notification. Business refusals remain `result.kind === "error"`; routing, protocol, transport and observer failures reject. A command retains `commandId` and optional summary `sourceEventSeq` instead of selecting an assistant reply. To bypass native command routing, use the low-level `client.prompt(sessionId, blocks)` and manage the subsequent subscription and wait yourself; runtime skill recognition still applies. +Both string and content-block input return `RunCommandResult` for a command or `RunResult` for ordinary prompt activity. Both results include `sessionId`, including when `harness.run()` creates a fresh Session. `RunCommandResult` extends the native `CommandExecution` with that Session identity. Commands settle without requiring a turn or idle notification. Business refusals remain `result.kind === "error"`; routing, protocol, transport and observer failures reject. A command retains `commandId` and optional summary `sourceEventSeq` instead of selecting an assistant reply. To bypass native command routing, use the low-level `client.prompt(sessionId, blocks)` and manage the subsequent subscription and wait yourself; runtime skill recognition still applies. Every call owns a subscription installed before sending and released before settlement. `onNotification` observes ordinary receipt-to-idle activity in wire order. Command callbacks replay Session-tree notifications from subscription through their own `command/done` after the command response; overlapping work can appear in that interval. Concurrent calls keep their own results, so a running model turn can coexist with a command's native busy result. Timeout or observer failure does not roll back runtime work or authorize automatic retry; command completion does not end independent model activity. Launch options supply initial values. A permission command changes only its target Session; later inputs and restoration retain that state, other Sessions keep theirs, and new Sessions use configured defaults. It does not update the options object or saved user defaults. An approval-policy change during a running turn can inject a notice and cause another step. See [permission presets](../../interaction/permission-presets/README.md) for native transition and restoration behavior. -`run('/plan')` enters plan mode; `/plan off` leaves; `/plan ` enters and steers the message. The call returns `CommandExecution` while model work and review can continue. Enter first and run ordinary text when a `RunResult` is needed. For combined input, subscribe before `run()` and wait through `client.request('session/wait', { sessionId })`; keep question handling alive until the work finishes. Plan state does not change permissions. +`run('/plan')` enters plan mode; `/plan off` leaves; `/plan ` enters and steers the message. The call returns `RunCommandResult` while model work and review can continue. Enter first and run ordinary text when a `RunResult` is needed. For combined input, subscribe before `run()` and wait through `client.request('session/wait', { sessionId })`; keep question handling alive until the work finishes. Plan state does not change permissions. ### 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)` accepts ASCII registered names or canonical English aliases of supported builtin definitions, 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. +`client.executeCommand(sessionId, line)` accepts ASCII registered names or canonical English aliases of supported builtin definitions, without prompt fallback or attachments; it requires a non-blank permission preset and returns the native `CommandExecution`. Use `run()` for Chinese aliases. A non-blank unknown preset reaches the native handler and returns `result.kind === "error"`. Subscribe before the command call 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. +Native goal creation and resume can start automatic rounds after the command call returns. 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. `HarnessClient` is the protocol client under the run API: explicit `start()`, `initialize()`, `prompt()`, `request()`, and `close()`, plus notification subscriptions. `prompt()` returns the queued message id as soon as the runtime accepts it and never waits for agent activity. `subscribe(filter?)` returns a `NotificationSubscription` (awaitable `next()`, non-blocking `tryNext()`, async iteration); `subscribeSessionTree(id)` scopes to one session and the descendants discovered from `subagent.started` lineage edges — the runtime notifies for every session in its context, and scoping is client-side, exactly like the Python SDK. @@ -109,7 +109,7 @@ This section explains the design behind the client; the observable behavior is f ### Design concept -The client is two layers over one wire: `DeepSeekHarness` (owned runs) over `HarnessClient` (the protocol client), mirroring the Python SDK's layering. It runs outside any harness context, so it spawns the runtime directly rather than through the `dsh-subprocess` service — the seam's documented exception for SDK-managed transports — and its teardown ladder lives in this package. The runtime notifies for every session in its context; session-tree scoping is a client-side filter over `subagent.started` lineage edges. +The client is two layers over one wire: `DeepSeekHarness` (owned runs) over `HarnessClient` (the protocol client), mirroring the Python SDK's layering. Both SDKs expose `run()` for prompt-or-command input; request construction and response validation stay in internal helpers. It runs outside any harness context, so it spawns the runtime directly rather than through the `dsh-subprocess` service — the seam's documented exception for SDK-managed transports — and its teardown ladder lives in this package. The runtime notifies for every session in its context; session-tree scoping is a client-side filter over `subagent.started` lineage edges. ### Source map @@ -118,7 +118,7 @@ The client is two layers over one wire: `DeepSeekHarness` (owned runs) over `Har | [`src/api.ts`](src/api.ts) | `DeepSeekHarness` + `HarnessSession`: unified input routing, owned activity intervals and command results | | [`src/client.ts`](src/client.ts) | `HarnessClient`: spawn, handshake, requests, subscription fan-out, typed errors | | [`src/dispose.ts`](src/dispose.ts) | Private teardown ladder: stdin EOF → SIGTERM → SIGKILL to actual exit | -| [`src/types.ts`](src/types.ts) | Launch and timeout options, notifications, `RunResult` and `CommandExecution` | +| [`src/types.ts`](src/types.ts) | Launch and timeout options, notifications, `RunResult` and `RunCommandResult` | | [`src/index.ts`](src/index.ts) | Consumer interface: the two client layers and caller-facing types | ### Owned activity flow diff --git a/packages/sdk/client/README.zh.md b/packages/sdk/client/README.zh.md index 3d6290b674..829df894e1 100644 --- a/packages/sdk/client/README.zh.md +++ b/packages/sdk/client/README.zh.md @@ -70,19 +70,19 @@ if ('commandId' in result) console.log(result.result) else console.log(result.finalResponse) ``` -字符串和内容块输入被识别为命令时都返回 `CommandExecution`,普通提示词活动返回 `RunResult`。命令完成不需要 turn 或 idle 通知。业务拒绝仍为 `result.kind === "error"`;路由、协议、传输和观察回调失败会拒绝 Promise。命令保留 `commandId` 及可选摘要 `sourceEventSeq`,不会选取助手回复作为结果。要绕过原生命令路由,使用低层 `client.prompt(sessionId, blocks)` 并自行管理后续订阅与等待;运行时 skill(技能)识别仍然适用。 +字符串和内容块输入被识别为命令时都返回 `RunCommandResult`,普通提示词活动返回 `RunResult`。两种结果都包含 `sessionId`,包括 `harness.run()` 创建新 Session 的情况。`RunCommandResult` 在原生 `CommandExecution` 上增加该 Session 标识。命令完成不需要 turn 或 idle 通知。业务拒绝仍为 `result.kind === "error"`;路由、协议、传输和观察回调失败会拒绝 Promise。命令保留 `commandId` 及可选摘要 `sourceEventSeq`,不会选取助手回复作为结果。要绕过原生命令路由,使用低层 `client.prompt(sessionId, blocks)` 并自行管理后续订阅与等待;运行时 skill(技能)识别仍然适用。 每次调用都拥有自己的订阅,在发送前安装,在调用完成前释放。`onNotification` 按线上的顺序观察普通输入从入队回执到 idle 的活动。命令回调在命令响应到达后,按顺序交付从订阅时刻到自身 `command/done` 的 Session 树通知;该区间可能包含重叠工作的通知。并发调用保留各自结果,所以运行中的模型轮次可以与命令的原生 busy 结果并存。超时或观察回调失败不回滚运行时工作,也不授权自动重试;命令完成不会结束独立的模型活动。 启动选项提供初始值。权限命令只修改目标 Session;后续输入与恢复保留该状态,其他 Session 保留自己的状态,新 Session 使用配置默认值。命令不修改选项对象或已保存的用户默认值。运行中的审批策略变化可以注入通知并触发下一步骤。原生转换与恢复行为见[权限 preset](../../interaction/permission-presets/README.zh.md)。 -`run('/plan')` 进入计划模式;`/plan off` 退出;`/plan ` 进入并 steer 正文。调用返回 `CommandExecution` 时,模型工作和审阅仍可能继续。需要 `RunResult` 时,先进入,再运行普通文本。组合输入应在 `run()` 前订阅,并通过 `client.request('session/wait', { sessionId })` 等待;保持问答处理器存活直到工作结束。计划状态不修改权限。 +`run('/plan')` 进入计划模式;`/plan off` 退出;`/plan ` 进入并 steer 正文。调用返回 `RunCommandResult` 时,模型工作和审阅仍可能继续。需要 `RunResult` 时,先进入,再运行普通文本。组合输入应在 `run()` 前订阅,并通过 `client.request('session/wait', { sessionId })` 等待;保持问答处理器存活直到工作结束。计划状态不修改权限。 ### 用 HarnessClient 做低层控制 -`client.query(sessionId, input, delivery)` 接受字符串或内容块数组,等待经过校验的提示词回执或命令结果。`client.executeCommand(sessionId, line)` 接受受支持内置定义的 ASCII 注册名或英文规范别名,不回退为提示词,也不接受附件;permission preset 必须非空。中文别名使用 `run()` 或 `query()`。非空的未知 preset 会进入原生 handler,返回 `result.kind === "error"`。需要通知时,应在调用前订阅。请求超时仅放弃本地等待,运行时工作仍可继续。 +`client.executeCommand(sessionId, line)` 接受受支持内置定义的 ASCII 注册名或英文规范别名,不回退为提示词,也不接受附件;permission preset 必须非空,返回原生 `CommandExecution`。中文别名使用 `run()`。非空的未知 preset 会进入原生 handler,返回 `result.kind === "error"`。需要通知时,应在命令调用前订阅。请求超时仅放弃本地等待,运行时工作仍可继续。 -`/goal` 使用原生目标 handler,可在 `CommandExecution` 返回后启动自动轮次。发送前应订阅;先观察终态 `goal/change` 操作,再用 `session/wait` 等待收尾回复。轮次之间的 idle 不代表目标完成。暂停、清除、恢复和权限行为见[服务端语义](../server/README.zh.md)。 +原生目标创建和恢复可在命令调用返回后启动自动轮次。发送前应订阅;先观察终态 `goal/change` 操作,再用 `session/wait` 等待收尾回复。轮次之间的 idle 不代表目标完成。暂停、清除、恢复和权限行为见[服务端语义](../server/README.zh.md)。 `HarnessClient` 是运行 API 之下的协议客户端:显式 `start()`、`initialize()`、`prompt()`、`request()` 与 `close()`,外加通知订阅。`prompt()` 在运行时接受排队消息后立即返回该消息的 id,绝不等待 agent 活动。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。 @@ -109,7 +109,7 @@ else console.log(result.finalResponse) ### 设计理念 -客户端是同一协议上的两层:`DeepSeekHarness`(自有运行)叠加在 `HarnessClient`(协议客户端)之上,与 Python SDK 的分层一致。它运行在任何 harness 上下文之外,因此直接 spawn 运行时而非经由 `dsh-subprocess` 服务——即该 seam 记录的 SDK 托管传输例外——其关闭阶梯也位于本包。运行时对上下文内每个会话都发通知;会话树范围限定是客户端对 `subagent.started` 血缘边的过滤。 +客户端是同一协议上的两层:`DeepSeekHarness`(自有运行)叠加在 `HarnessClient`(协议客户端)之上,与 Python SDK 的分层一致。两种 SDK 都通过公开 `run()` 接收提示词或命令,请求构造与响应校验保留在内部辅助函数中。它运行在任何 harness 上下文之外,因此直接 spawn 运行时而非经由 `dsh-subprocess` 服务——即该 seam 记录的 SDK 托管传输例外——其关闭阶梯也位于本包。运行时对上下文内每个会话都发通知;会话树范围限定是客户端对 `subagent.started` 血缘边的过滤。 ### 源码地图 @@ -118,7 +118,7 @@ else console.log(result.finalResponse) | [`src/api.ts`](src/api.ts) | `DeepSeekHarness` + `HarnessSession`:统一输入路由、自有活动区间与命令结果 | | [`src/client.ts`](src/client.ts) | `HarnessClient`:spawn、握手、请求、订阅扇出、类型化错误 | | [`src/dispose.ts`](src/dispose.ts) | 私有关闭阶梯:stdin EOF → SIGTERM → SIGKILL 直到真正退出 | -| [`src/types.ts`](src/types.ts) | 启动与超时选项、通知、`RunResult` 与 `CommandExecution` | +| [`src/types.ts`](src/types.ts) | 启动与超时选项、通知、`RunResult` 与 `RunCommandResult` | | [`src/index.ts`](src/index.ts) | 消费方接口:两层客户端与面向调用方的类型 | ### 自有活动流程 diff --git a/packages/sdk/client/src/api.ts b/packages/sdk/client/src/api.ts index 6731bc4373..a5afc24dac 100644 --- a/packages/sdk/client/src/api.ts +++ b/packages/sdk/client/src/api.ts @@ -8,10 +8,9 @@ import { randomUUID } from 'node:crypto' import { resolve } from 'node:path' import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session' -import type { CommandExecution } from '@deepseek-ai/dsh-sdk-protocol' -import { createProcessHarnessClient, HarnessClient, isRecord, SdkProtocolError } from './client.ts' +import { createProcessHarnessClient, HarnessClient, isRecord, queryInput, SdkProtocolError } from './client.ts' import type { RuntimeProcessOptions } from './launch.ts' -import type { ContentBlock, DeepSeekHarnessOptions, HarnessNotification, RunResult, SdkPromptContentBlock } from './types.ts' +import type { RunCommandResult, ContentBlock, DeepSeekHarnessOptions, HarnessNotification, RunResult, SdkPromptContentBlock } from './types.ts' /** * Reusable SDK for running DeepSeek Harness agent turns in a runtime @@ -123,9 +122,9 @@ export class DeepSeekHarness implements AsyncDisposable { * Run text or content blocks on a fresh or named Session through native input routing. * @param input - prompt or command text, optionally accompanied by image content blocks. * @param options - optional Session identity, delivery and notification observer. - * @returns ordinary prompt activity or the native command result. + * @returns ordinary prompt activity or the native command result, both with the receiving Session id. */ - run(input: string | SdkPromptContentBlock[], options?: RunOptions): Promise { + run(input: string | SdkPromptContentBlock[], options?: RunOptions): Promise { return this.session(options?.sessionId).run(input, options) } @@ -246,17 +245,17 @@ export class HarnessSession { * Run text or content blocks through native prompt-or-command routing. * @param input - prompt or command text, optionally accompanied by image content blocks; image-only input is an ordinary prompt. * @param options - input delivery and a per-notification observer. - * @returns the prompt's receipt-to-idle activity or a settled command; command + * @returns the prompt's receipt-to-idle activity or a settled command with this Session id; command * completion does not wait for later model work. Handler errors remain results. * @throws on routing, protocol, transport or observer failure; runtime work is not rolled back. */ - async run(input: string | SdkPromptContentBlock[], options?: Omit): Promise { + async run(input: string | SdkPromptContentBlock[], options?: Omit): Promise { const delivery = resolveRunDelivery(options?.delivery) await this.harness.start() const client = this.harness.client const subscription = client.subscribeSessionTree(this.id) try { - const response = await client.query(this.id, input, delivery) + const response = await queryInput(client, this.id, input, delivery) if (response.kind === 'command') { for (let notification = subscription.tryNext(); notification !== undefined; notification = subscription.tryNext()) { options?.onNotification?.(notification) @@ -265,7 +264,7 @@ export class HarnessSession { && isRecord(event) && event.type === 'command/done' && isRecord(event.data) && event.data.commandId === response.execution.commandId) break } - return response.execution + return { ...response.execution, sessionId: this.id } } const activity = new PromptActivity(this.id, response.messageId) while (true) { diff --git a/packages/sdk/client/src/client.ts b/packages/sdk/client/src/client.ts index 09665e043f..9c7995ff38 100644 --- a/packages/sdk/client/src/client.ts +++ b/packages/sdk/client/src/client.ts @@ -374,33 +374,6 @@ export class HarnessClient { return commandExecution(value, 'session/command') } - /** - * Send text or content blocks for runtime prompt-or-command routing and await its receipt. - * @param sessionId - target Session; an unknown id creates it. - * @param input - text or content blocks, without client-side command interpretation. - * @param delivery - ordinary prompt delivery; commands retain their own admission rules. - * @returns the prompt receipt or native command execution. - */ - async query( - sessionId: string, - input: string | SdkPromptContentBlock[], - delivery: SessionQueryParams['delivery'], - ): Promise { - const params: SessionQueryParams = { - sessionId: SessionId(sessionId), contentBlocks: normalizeInput(input), delivery, - } - const value = await this.request('session/query', params) - if (isRecord(value)) { - if (value.kind === 'prompt' && typeof value.messageId === 'string' && value.messageId.length > 0) { - return { kind: 'prompt', messageId: value.messageId } - } - if (value.kind === 'command') { - return { kind: 'command', execution: commandExecution(value.execution, 'session/query') } - } - } - throw new SdkProtocolError(`session/query returned an invalid routing result: ${JSON.stringify(value)}`) - } - /** * Restore persisted history without sending a prompt; failures propagate without creating a session. * @param sessionId - persisted identity in this runtime's configured persistence store. @@ -621,6 +594,35 @@ export function createProcessHarnessClient(options: RuntimeProcessOptions): Harn return new Constructor({}, options) } +/** + * Send routed input for the package's high-level run API; absent from the package exports. + * @param client - client owning the initialized runtime connection. + * @param sessionId - target Session; an unknown id creates it. + * @param input - text or content blocks, without client-side command interpretation. + * @param delivery - ordinary prompt delivery; commands retain their own admission rules. + * @returns the validated prompt receipt or native command execution. + */ +export async function queryInput( + client: HarnessClient, + sessionId: string, + input: string | SdkPromptContentBlock[], + delivery: SessionQueryParams['delivery'], +): Promise { + const params: SessionQueryParams = { + sessionId: SessionId(sessionId), contentBlocks: normalizeInput(input), delivery, + } + const value = await client.request('session/query', params) + if (isRecord(value)) { + if (value.kind === 'prompt' && typeof value.messageId === 'string' && value.messageId.length > 0) { + return { kind: 'prompt', messageId: value.messageId } + } + if (value.kind === 'command') { + return { kind: 'command', execution: commandExecution(value.execution, 'session/query') } + } + } + throw new SdkProtocolError(`session/query returned an invalid routing result: ${JSON.stringify(value)}`) +} + /** * Normalize routed input: a string becomes one text block; blocks pass verbatim. * @param input - prompt or command text, or content blocks. diff --git a/packages/sdk/client/src/index.ts b/packages/sdk/client/src/index.ts index b4b01de4a5..91de7583c5 100644 --- a/packages/sdk/client/src/index.ts +++ b/packages/sdk/client/src/index.ts @@ -22,6 +22,7 @@ export { JsonRpcResponseError } from '@deepseek-ai/dsh-sdk-protocol' export type { ContentBlock, CommandExecution, + RunCommandResult, SdkEncodedImageBlock, SdkPromptContentBlock, SdkUserQuestionAnswer, diff --git a/packages/sdk/client/src/types.ts b/packages/sdk/client/src/types.ts index 137ee668f6..c764b29905 100644 --- a/packages/sdk/client/src/types.ts +++ b/packages/sdk/client/src/types.ts @@ -82,6 +82,12 @@ export interface RunResult { notifications: HarnessNotification[] } +/** A high-level run's native command outcome and its receiving Session. */ +export interface RunCommandResult extends CommandExecution { + /** Session to use for subsequent input, observation, or persisted restoration. */ + sessionId: string +} + /** Re-exported content-block alias so SDK callers need no extra import. */ export type { ContentBlock } export type { CommandExecution } diff --git a/packages/sdk/client/tests/run.spec.ts b/packages/sdk/client/tests/run.spec.ts index 8ccbd5e5ca..2415f0a3b9 100644 --- a/packages/sdk/client/tests/run.spec.ts +++ b/packages/sdk/client/tests/run.spec.ts @@ -1,10 +1,11 @@ /** Unified input receipt and observation over an independently running protocol peer. */ import { fileURLToPath } from 'node:url' -import { expect, it, onTestFinished, vi } from 'vitest' +import { expect, expectTypeOf, it, onTestFinished, vi } from 'vitest' import { createProcessDeepSeekHarness } from '../src/api.ts' +import * as sdk from '../src/index.ts' import { JsonRpcResponseError, RequestTimeoutError, SdkProtocolError, TransportClosedError } from '../src/index.ts' import type { SdkEncodedImageBlock, SdkPromptContentBlock } from '../src/index.ts' -import type { CommandExecution, HarnessNotification, RunResult } from '../src/types.ts' +import type { RunCommandResult, HarnessNotification, RunResult } from '../src/types.ts' function harnessWith(environment: Record = {}, requestTimeoutMs?: number) { const harness = createProcessDeepSeekHarness({ @@ -19,7 +20,7 @@ function harnessWith(environment: Record = {}, requestTimeoutMs? return harness } -function activity(result: RunResult | CommandExecution): RunResult { +function activity(result: RunResult | RunCommandResult): RunResult { if ('commandId' in result) throw new Error('expected ordinary prompt activity') return result } @@ -29,6 +30,29 @@ const images: [SdkEncodedImageBlock, SdkEncodedImageBlock] = [ { type: 'image', data: 'c2Vjb25k', mimeType: 'image/jpeg' }, ] +it('exposes run while keeping its routing request helper internal', () => { + expect(sdk.HarnessClient.prototype).not.toHaveProperty('query') + expect(sdk).not.toHaveProperty('queryInput') + expectTypeOf().not.toHaveProperty('query') +}) + +it('returns generated Session identities that can receive subsequent input', async () => { + const harness = harnessWith() + const first = await harness.run('/compact') + const second = await harness.run('/compact') + expect(first.sessionId).toEqual(expect.any(String)) + expect(first.sessionId).not.toBe('') + expect(second.sessionId).not.toBe(first.sessionId) + const continued = activity(await harness.run('continue', { sessionId: first.sessionId })) + expect(continued.sessionId).toBe(first.sessionId) + expect(continued.finalResponse).toBe('reply:continue') + expect(await harness.client.request('test/state')).toHaveProperty('queries', [ + { sessionId: first.sessionId, contentBlocks: [{ type: 'text', text: '/compact' }], delivery: 'queue' }, + { sessionId: second.sessionId, contentBlocks: [{ type: 'text', text: '/compact' }], delivery: 'queue' }, + { sessionId: first.sessionId, contentBlocks: [{ type: 'text', text: 'continue' }], delivery: 'queue' }, + ]) +}) + it.each(['session', 'harness'])('runs a native command through the %s entry without waiting for idle', async (entry) => { const harness = harnessWith() const notifications: HarnessNotification[] = [] @@ -36,7 +60,7 @@ it.each(['session', 'harness'])('runs a native command through the %s entry with const result = entry === 'session' ? await harness.session('main').run('/compact', { onNotification }) : await harness.run('/compact', { sessionId: 'main', onNotification }) - expect(result).toEqual({ commandId: 'command-1', result: { kind: 'success', text: 'compacted', sourceEventSeq: 0 } }) + expect(result).toEqual({ sessionId: 'main', commandId: 'command-1', result: { kind: 'success', text: 'compacted', sourceEventSeq: 0 } }) expect(notifications.map(notification => (notification.params.event as { type: string }).type)) .toEqual(['command/run', 'command/done']) }) @@ -59,7 +83,7 @@ it.each(['session', 'harness'])('forwards native command content blocks through const result = entry === 'session' ? await harness.session('images').run(contentBlocks) : await harness.run(contentBlocks, { sessionId: 'images' }) - expect(result).toEqual(execution) + expect(result).toEqual({ ...execution, sessionId: 'images' }) expect(await harness.client.request('test/state')).toHaveProperty('queries', [ { sessionId: 'images', contentBlocks, delivery: 'queue' }, ]) @@ -82,14 +106,14 @@ it.each([ ]) }) -it('normalizes text and preserves block order in low-level routing requests', async () => { +it('normalizes text and preserves block order through run', async () => { const harness = harnessWith() await harness.start() const contentBlocks: SdkPromptContentBlock[] = [ images[0], { type: 'text', text: 'inspect ' }, images[1], { type: 'text', text: 'both' }, ] - expect(await harness.client.query('images', ' inspect\n', 'steer')).toEqual({ kind: 'prompt', messageId: 'input-1' }) - expect(await harness.client.query('images', contentBlocks, 'queue')).toEqual({ kind: 'prompt', messageId: 'input-2' }) + expect(activity(await harness.run(' inspect\n', { sessionId: 'images', delivery: 'steer' })).finalResponse).toBe('reply: inspect\n') + expect(activity(await harness.run(contentBlocks, { sessionId: 'images' })).finalResponse).toBe('reply:inspect \nboth') expect(await harness.client.executeCommand('images', ' /plan inspect ')).toEqual({ commandId: 'strict-command-1', result: { kind: 'success' }, }) @@ -105,6 +129,7 @@ it('returns native command results for text-only blocks', async () => { const harness = harnessWith() const contentBlocks: SdkPromptContentBlock[] = [{ type: 'text', text: '/compact' }] expect(await harness.session('blocks').run(contentBlocks)).toEqual({ + sessionId: 'blocks', commandId: 'command-1', result: { kind: 'success', text: 'compacted', sourceEventSeq: 0 }, }) expect(await harness.client.request('test/state')).toHaveProperty('queries', [ @@ -115,7 +140,8 @@ it('returns native command results for text-only blocks', async () => { it('retains a rejected command outcome without retrying its content as a prompt', async () => { const execution = { commandId: 'rejected-command', result: { kind: 'error', text: 'command rejected images' } } const harness = harnessWith({ QUERY_RESULT: JSON.stringify({ kind: 'command', execution }) }) - expect(await harness.session('images').run([{ type: 'text', text: '/plan off' }, ...images])).toEqual(execution) + expect(await harness.session('images').run([{ type: 'text', text: '/plan off' }, ...images])) + .toEqual({ ...execution, sessionId: 'images' }) expect(await harness.client.request('test/state')).toHaveProperty('methods', ['initialize', 'session/query', 'test/state']) }) @@ -148,7 +174,7 @@ it('returns a busy command result while another run waits for model activity', a if (notification.method === 'session.status' && notification.params.status === 'running') entered.resolve(undefined) } }) await entered.promise - expect(await session.run('/compact')).toEqual({ commandId: 'command-2', result: { kind: 'error', text: 'busy' } }) + expect(await session.run('/compact')).toEqual({ sessionId: 'main', commandId: 'command-2', result: { kind: 'error', text: 'busy' } }) await harness.client.request('test/release') expect(activity(await running).finalResponse).toBe('reply:hold') }) @@ -163,7 +189,7 @@ it('returns a command result without lifecycle notifications', async () => { const result = await harnessWith({ QUERY_RESULT: JSON.stringify({ kind: 'command', execution }) }).session('main').run('/compact', { onNotification: (notification) => { notifications.push(notification) }, }) - expect(result).toEqual(execution) + expect(result).toEqual({ ...execution, sessionId: 'main' }) expect(notifications).toEqual([]) }) diff --git a/packages/sdk/client/tests/sdk-client.spec.ts b/packages/sdk/client/tests/sdk-client.spec.ts index 55a2252426..9a3ef01e83 100644 --- a/packages/sdk/client/tests/sdk-client.spec.ts +++ b/packages/sdk/client/tests/sdk-client.spec.ts @@ -191,7 +191,7 @@ describe('DeepSeekHarness', () => { let closed = false const harness = harnessWith() vi.spyOn(harness, 'start').mockResolvedValue() - vi.spyOn(harness.client, 'query').mockResolvedValue({ kind: 'prompt', messageId: 'accepted-message' }) + vi.spyOn(harness.client, 'request').mockResolvedValue({ kind: 'prompt', messageId: 'accepted-message' }) vi.spyOn(harness.client, 'subscribeSessionTree').mockImplementation(() => ({ next: async () => { const notification = notifications.shift() @@ -280,7 +280,7 @@ describe('DeepSeekHarness', () => { ] as HarnessNotification[] const harness = harnessWith() vi.spyOn(harness, 'start').mockResolvedValue() - vi.spyOn(harness.client, 'query').mockResolvedValue({ kind: 'prompt', messageId: 'accepted-message' }) + vi.spyOn(harness.client, 'request').mockResolvedValue({ kind: 'prompt', messageId: 'accepted-message' }) vi.spyOn(harness.client, 'subscribeSessionTree').mockImplementation(() => ({ next: async () => { const notification = notifications.shift() diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 7e4046cf0b..5b43e9ce25 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: 4a70013e5ba565a9 - zh: 00bb9b06d38f91ba + en: 477bb559cbf13f84 + zh: c5825be4ae2f1508 /deepseek-harness-python-sdk/client-approvals: en: fc69be1beb8abff1 zh: 91b56781931a6f68 diff --git a/python/sdk/README.md b/python/sdk/README.md index fcfae52e50..13b639ac15 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -61,21 +61,21 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness: ## Run text and commands -`session.run(input, on_notification=..., delivery="queue")` is the high-level input entry for strings and content-block arrays. Both use the runtime's [supported command forms and Desktop aliases](../../packages/sdk/server/README.md#command-input); unrecognized input remains an ordinary prompt with its original block order. `delivery="steer"` targets the next available step for ordinary input, including images. Command admission and timing remain owned by the native handler. +`session.run(input, on_notification=..., delivery="queue")` is the public prompt-or-command input entry for strings and content-block arrays. Both use the runtime's [supported command forms and Desktop aliases](../../packages/sdk/server/README.md#command-input); unrecognized input remains an ordinary prompt with its original block order. `delivery="steer"` targets the next available step for ordinary input, including images. Command admission and timing remain owned by the native handler. -The call returns `RunResult` for an ordinary prompt's inbox-receipt-to-idle interval, or `CommandExecution` after a command settles. Use `isinstance(result, RunResult)` before reading `final_response`; otherwise inspect the command's `result.kind`. A command retains its `command_id` and optional `source_event_seq`, rather than selecting an earlier assistant reply. +The call returns `RunResult` for an ordinary prompt's inbox-receipt-to-idle interval, or `RunCommandResult` after a command settles. Both results include `session_id`, including when `harness.run()` creates a fresh Session. `RunCommandResult` extends the native `CommandExecution` with that Session identity. Use `isinstance(result, RunResult)` before reading `final_response`; otherwise inspect the command's `result.kind`. A command retains its `command_id` and optional `source_event_seq`, rather than selecting an earlier assistant reply. -Content-block arrays use the same command routing as strings. Both `session.run([{"type": "text", "text": "/compact"}])` and `session.run("/compact")` invoke native compaction and return `CommandExecution`. To bypass native command routing, call the low-level `client.session_prompt(session_id, blocks)` and manage the subsequent subscription and wait yourself; runtime skill recognition still applies. +Content-block arrays use the same command routing as strings. Both `session.run([{"type": "text", "text": "/compact"}])` and `session.run("/compact")` invoke native compaction and return `RunCommandResult`. To bypass native command routing, call the low-level `client.session_prompt(session_id, blocks)` and manage the subsequent subscription and wait yourself; runtime skill recognition still applies. Select a model that supports image input. Submit a command and image in the existing content-block array, for example `session.run([{"type": "text", "text": "/plan Analyze this image."}, {"type": "image", "data": image_base64, "mimeType": "image/png"}])`; `harness.run()` accepts the same input. Plan input and goal creation or editing accept images; `/plan off`, goal controls, compact, and permission commands reject them through the [native attachment rules](../../packages/sdk/server/README.md#command-input). Native command input supports encoded raster images, not general file uploads or client-supplied attachment references. -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. Malformed native-command responses from `run()` or `session_command()` raise `SdkProtocolError`, with the model validation error retained as the cause. 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. +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 `RunCommandResult.result.kind="error"`; routing, protocol, transport and observer failures raise. Malformed native-command responses from `run()` or `session_command()` raise `SdkProtocolError`, with the model validation error retained as the cause. 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)` accepts ASCII registered names or canonical English aliases of supported builtin definitions, without prompt fallback or attachments. Use `run()` for Chinese aliases. 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 ASCII registered names or canonical English aliases of supported builtin definitions, without prompt fallback or attachments, and returns the native `CommandExecution`. 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). -`/plan` enters plan mode, `/plan off` leaves, and `/plan ` enters then submits the trimmed message through native steer. Its `CommandExecution` confirms command completion, not completion of planning or review. Enter with `/plan` then run ordinary text when a `RunResult` is needed. For combined input, subscribe before `run()` and use `session/wait` while keeping the question handler alive, as in the [plan example](examples/README.md#review-a-plan). Plan guidance and `exit_plan_mode` follow their native lifecycle without changing permission settings. +`/plan` enters plan mode, `/plan off` leaves, and `/plan ` enters then submits the trimmed message through native steer. Its `RunCommandResult` confirms command completion, not completion of planning or review. Enter with `/plan` then run ordinary text when a `RunResult` is needed. For combined input, subscribe before `run()` and use `session/wait` while keeping the question handler alive, as in the [plan example](examples/README.md#review-a-plan). Plan guidance and `exit_plan_mode` follow their native lifecycle without changing permission settings. `/goal` shows the current goal; `/goal `, `edit `, `pause`, `resume`, and `clear` use the native handler. Creation and resume can start automatic rounds after `run()` returns. Subscribe before sending, observe terminal `goal/change` operations, then call `session/wait` for any closing model reply. Idle between rounds is not goal completion. A restored active goal stays disarmed until explicit resume. See the [goal example](examples/README.md#run-a-goal). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index ac35adbc88..2be97e77b9 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -61,21 +61,21 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness: ## 运行文本与命令 -`session.run(input, on_notification=..., delivery="queue")` 是字符串和内容块数组共用的高层输入入口。两者都使用运行时的[命令形式与 Desktop 别名](../../packages/sdk/server/README.zh.md#command-input)识别规则;未识别的输入保持普通提示词及原始内容块顺序。对普通输入,包括图片,`delivery="steer"` 指向下一个可用步骤。命令的接纳规则与执行时序仍由原生处理器负责。 +`session.run(input, on_notification=..., delivery="queue")` 是字符串和内容块数组共用的公开提示词/命令输入入口。两者都使用运行时的[命令形式与 Desktop 别名](../../packages/sdk/server/README.zh.md#command-input)识别规则;未识别的输入保持普通提示词及原始内容块顺序。对普通输入,包括图片,`delivery="steer"` 指向下一个可用步骤。命令的接纳规则与执行时序仍由原生处理器负责。 -普通提示词返回其入队回执到下一次 idle 区间的 `RunResult`,命令则在执行结束后返回 `CommandExecution`。读取 `final_response` 前使用 `isinstance(result, RunResult)` 检查类型,否则检查命令的 `result.kind`。命令保留自己的 `command_id` 和可选 `source_event_seq`,不会选取更早的助手回复作为结果。 +普通提示词返回其入队回执到下一次 idle 区间的 `RunResult`,命令则在执行结束后返回 `RunCommandResult`。两种结果都包含 `session_id`,包括 `harness.run()` 创建新 Session 的情况。`RunCommandResult` 在原生 `CommandExecution` 上增加该 Session 标识。读取 `final_response` 前使用 `isinstance(result, RunResult)` 检查类型,否则检查命令的 `result.kind`。命令保留自己的 `command_id` 和可选 `source_event_seq`,不会选取更早的助手回复作为结果。 -内容块数组与字符串使用相同的命令路由。`session.run([{"type": "text", "text": "/compact"}])` 和 `session.run("/compact")` 都调用原生压缩并返回 `CommandExecution`。要绕过原生命令路由,调用低层 `client.session_prompt(session_id, blocks)` 并自行管理后续订阅与等待;运行时 skill(技能)识别仍然适用。 +内容块数组与字符串使用相同的命令路由。`session.run([{"type": "text", "text": "/compact"}])` 和 `session.run("/compact")` 都调用原生压缩并返回 `RunCommandResult`。要绕过原生命令路由,调用低层 `client.session_prompt(session_id, blocks)` 并自行管理后续订阅与等待;运行时 skill(技能)识别仍然适用。 先选择支持图片输入的模型。通过现有内容块数组同时提交命令和图片,例如 `session.run([{"type": "text", "text": "/plan Analyze this image."}, {"type": "image", "data": image_base64, "mimeType": "image/png"}])`;`harness.run()` 接受相同输入。计划输入和目标创建、编辑接受图片;`/plan off`、目标控制、压缩和权限命令按[原生附件规则](../../packages/sdk/server/README.zh.md#command-input)拒绝图片。原生命令输入支持编码栅格图片,不支持一般文件上传或客户端提供的附件引用。 -每次调用在发送前订阅,并在返回或抛出异常前释放自己的订阅。`on_notification` 按线上的顺序接收普通活动区间通知;命令回调在命令响应到达后,按顺序交付从订阅时刻到自身 `command/done` 的 Session 树通知,因此该区间可能包含重叠工作的通知。调用可以重叠,不会消耗彼此的结果。处理器拒绝返回 `CommandExecution.result.kind="error"`,路由、协议、传输和观察回调失败则抛出异常。`run()` 或 `session_command()` 收到格式错误的原生命令响应时抛出 `SdkProtocolError`,并保留模型校验错误作为 cause。超时或观察回调失败不回滚运行时工作,也不授权自动重试。`request_timeout_seconds` 限制协议响应等待,不限制之后的普通提示词活动。 +每次调用在发送前订阅,并在返回或抛出异常前释放自己的订阅。`on_notification` 按线上的顺序接收普通活动区间通知;命令回调在命令响应到达后,按顺序交付从订阅时刻到自身 `command/done` 的 Session 树通知,因此该区间可能包含重叠工作的通知。调用可以重叠,不会消耗彼此的结果。处理器拒绝返回 `RunCommandResult.result.kind="error"`,路由、协议、传输和观察回调失败则抛出异常。`run()` 或 `session_command()` 收到格式错误的原生命令响应时抛出 `SdkProtocolError`,并保留模型校验错误作为 cause。超时或观察回调失败不回滚运行时工作,也不授权自动重试。`request_timeout_seconds` 限制协议响应等待,不限制之后的普通提示词活动。 -默认的完整 `sdk` profile 提供已支持的原生命令;`sdk-minimal` 将未支持的输入保留在普通提示词路径。`/compact` 可以发起辅助模型请求,其完成不需要普通 turn 或 idle 通知;空历史可以在不生成摘要时成功。底层 `HarnessClient.session_command(session_id, line)` 接受受支持内置定义的 ASCII 注册名或英文规范别名,不回退为提示词,也不接受附件。中文别名使用 `run()`。见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。 +默认的完整 `sdk` profile 提供已支持的原生命令;`sdk-minimal` 将未支持的输入保留在普通提示词路径。`/compact` 可以发起辅助模型请求,其完成不需要普通 turn 或 idle 通知;空历史可以在不生成摘要时成功。底层 `HarnessClient.session_command(session_id, line)` 接受受支持内置定义的 ASCII 注册名或英文规范别名,不回退为提示词,也不接受附件,返回原生 `CommandExecution`。中文别名使用 `run()`。见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。 `/permission ` 只修改接收命令的 Session。未知 preset 返回命令错误,不改变策略。后续输入与恢复保留成功的修改,其他 Session 和调用方的 `DeepSeekHarnessConfig` 保留自己的值。运行中的审批策略变化可以注入通知并触发下一模型步骤。见[权限示例](examples/README.zh.md#change-session-permissions)。 -`/plan` 进入计划模式,`/plan off` 退出,`/plan ` 进入后通过原生 steer 提交去除首尾空白的正文。其 `CommandExecution` 确认命令执行结束,不代表计划生成或审阅结束。需要 `RunResult` 时,先用 `/plan` 进入,再运行普通文本。组合输入应在 `run()` 前订阅,并保持问答处理器存活,通过 `session/wait` 观察后续工作,见[计划示例](examples/README.zh.md#review-a-plan)。计划指导和 `exit_plan_mode` 遵循原生生命周期,不修改权限设置。 +`/plan` 进入计划模式,`/plan off` 退出,`/plan ` 进入后通过原生 steer 提交去除首尾空白的正文。其 `RunCommandResult` 确认命令执行结束,不代表计划生成或审阅结束。需要 `RunResult` 时,先用 `/plan` 进入,再运行普通文本。组合输入应在 `run()` 前订阅,并保持问答处理器存活,通过 `session/wait` 观察后续工作,见[计划示例](examples/README.zh.md#review-a-plan)。计划指导和 `exit_plan_mode` 遵循原生生命周期,不修改权限设置。 `/goal` 查看当前目标;`/goal `、`edit `、`pause`、`resume` 和 `clear` 使用原生处理器。创建和恢复可以在 `run()` 返回后启动自动轮次。发送前订阅,观察终态 `goal/change` 操作后,再调用 `session/wait` 等待可能的模型收尾。轮次之间的 idle 不代表目标完成。恢复的活动目标在显式 resume 前保持 disarmed。见[目标示例](examples/README.zh.md#run-a-goal)。 diff --git a/python/sdk/examples/compact.py b/python/sdk/examples/compact.py index 7b3cacc49d..c1edf0efad 100644 --- a/python/sdk/examples/compact.py +++ b/python/sdk/examples/compact.py @@ -7,12 +7,12 @@ import argparse import os from pathlib import Path -from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult +from deepseek_harness import RunCommandResult, DeepSeekHarness, RunResult -def display(result: RunResult | CommandExecution) -> None: +def display(result: RunResult | RunCommandResult) -> None: """Print the input's result and stop on a reported failure.""" - if isinstance(result, CommandExecution): + if isinstance(result, RunCommandResult): print(f"Command {result.command_id}: {result.result.kind}") if result.result.kind == "error": raise RuntimeError(result.result.text) diff --git a/python/sdk/examples/goal.py b/python/sdk/examples/goal.py index d2aa902e8b..0da5196c76 100644 --- a/python/sdk/examples/goal.py +++ b/python/sdk/examples/goal.py @@ -11,7 +11,7 @@ from pathlib import Path from pydantic import BaseModel -from deepseek_harness import CommandExecution, DeepSeekHarness, Notification +from deepseek_harness import RunCommandResult, DeepSeekHarness, Notification from deepseek_harness.api import final_response @@ -68,7 +68,7 @@ def main() -> None: return event execution = session.run(f"/goal {args.objective}") - if not isinstance(execution, CommandExecution): + if not isinstance(execution, RunCommandResult): raise RuntimeError("The SDK profile did not execute the requested command") print(execution.result.text, flush=True) if execution.result.kind == "error": diff --git a/python/sdk/examples/minimal.py b/python/sdk/examples/minimal.py index fdbbd400a5..d869cc7a33 100644 --- a/python/sdk/examples/minimal.py +++ b/python/sdk/examples/minimal.py @@ -7,7 +7,7 @@ import argparse import os from pathlib import Path -from deepseek_harness import CommandExecution, DeepSeekHarness +from deepseek_harness import RunCommandResult, DeepSeekHarness def main() -> None: @@ -41,7 +41,7 @@ def main() -> None: profile=args.profile, ) as harness: result = harness.run(args.prompt, session_id=args.session_id) - if isinstance(result, CommandExecution): + if isinstance(result, RunCommandResult): if result.result.kind == "error": raise RuntimeError(result.result.text) print(result.result.text or "") diff --git a/python/sdk/examples/permission.py b/python/sdk/examples/permission.py index 99b165044d..4403612684 100644 --- a/python/sdk/examples/permission.py +++ b/python/sdk/examples/permission.py @@ -7,12 +7,12 @@ import argparse import os from pathlib import Path -from deepseek_harness import CommandExecution, DeepSeekHarness, RunResult +from deepseek_harness import RunCommandResult, DeepSeekHarness, RunResult -def display(result: RunResult | CommandExecution) -> None: +def display(result: RunResult | RunCommandResult) -> None: """Print the input's result and stop on a reported failure.""" - if isinstance(result, CommandExecution): + if isinstance(result, RunCommandResult): print(f"Command {result.command_id}: {result.result.kind}") if result.result.kind == "error": raise RuntimeError(result.result.text) diff --git a/python/sdk/examples/plan.py b/python/sdk/examples/plan.py index 7a52a39f9c..bcce4340f2 100644 --- a/python/sdk/examples/plan.py +++ b/python/sdk/examples/plan.py @@ -13,7 +13,7 @@ from pathlib import Path from pydantic import BaseModel -from deepseek_harness import CommandExecution, DeepSeekHarness, Notification, UserQuestionRequest +from deepseek_harness import RunCommandResult, DeepSeekHarness, Notification, UserQuestionRequest from deepseek_harness.api import final_response @@ -173,7 +173,7 @@ def main() -> None: session = harness.start_session() with closing(harness.client.subscribe_session_notifications(session.id)) as notifications: execution = session.run(f"/plan {args.task}") - if not isinstance(execution, CommandExecution): + if not isinstance(execution, RunCommandResult): raise RuntimeError("The SDK profile did not execute the requested command") print(execution.result.text, flush=True) if execution.result.kind == "error": diff --git a/python/sdk/src/deepseek_harness/__init__.py b/python/sdk/src/deepseek_harness/__init__.py index fdc6bda037..cb71031306 100644 --- a/python/sdk/src/deepseek_harness/__init__.py +++ b/python/sdk/src/deepseek_harness/__init__.py @@ -1,4 +1,4 @@ -from .api import DeepSeekHarness, DeepSeekHarnessConfig, RunResult, Session +from .api import RunCommandResult, DeepSeekHarness, DeepSeekHarnessConfig, RunResult, Session from .client import HarnessClient, HarnessConfig, UserQuestionHandler from .errors import SdkProtocolError from .models import ( @@ -12,6 +12,7 @@ __all__ = [ "DeepSeekHarnessConfig", "Session", "RunResult", + "RunCommandResult", "CommandExecution", "CommandResult", "CommandSuccessResult", diff --git a/python/sdk/src/deepseek_harness/api.py b/python/sdk/src/deepseek_harness/api.py index 658997c933..f4c44d827b 100644 --- a/python/sdk/src/deepseek_harness/api.py +++ b/python/sdk/src/deepseek_harness/api.py @@ -50,6 +50,12 @@ class RunResult: notifications: list[Notification] +class RunCommandResult(CommandExecution): + """Settled native command result with the Session id for continued interaction.""" + + session_id: str + + class _PromptActivity: """Collect an ordinary prompt's notifications from its inbox receipt through idle.""" @@ -174,8 +180,8 @@ class DeepSeekHarness: session_id: str | None = None, on_notification: Callable[[Notification], None] | None = None, delivery: Literal["queue", "steer"] = "queue", - ) -> RunResult | CommandExecution: - """Run text or content blocks through native input routing on a fresh or named Session.""" + ) -> RunResult | RunCommandResult: + """Run input on a fresh or named Session, returning its id with either result kind.""" return self.start_session(session_id).run(input, on_notification=on_notification, delivery=delivery) @@ -200,11 +206,12 @@ class Session: *, on_notification: Callable[[Notification], None] | None = None, delivery: Literal["queue", "steer"] = "queue", - ) -> RunResult | CommandExecution: + ) -> RunResult | RunCommandResult: """Run text or content blocks through native input routing. Input returns the prompt's receipt-to-idle RunResult or a settled - CommandExecution; command completion does not wait for later model work. + RunCommandResult. Both include this Session's id. Command completion does + not wait for later model work. The first text block selects native commands. Matched commands require one text block with only encoded images beside it; native handlers decide which commands accept those images. Ordinary prompts retain block order @@ -232,7 +239,7 @@ class Session: on_notification(notification) if _is_command_done(notification, self.id, execution.command_id): break - return execution + return RunCommandResult(session_id=self.id, commandId=execution.command_id, result=execution.result) activity = _PromptActivity(self.id, response) while True: notification = subscription.next() diff --git a/python/sdk/tests/test_plan_runtime.py b/python/sdk/tests/test_plan_runtime.py index 82337306ef..05c64c5094 100644 --- a/python/sdk/tests/test_plan_runtime.py +++ b/python/sdk/tests/test_plan_runtime.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest -from deepseek_harness import CommandExecution, RunResult, Session, UserQuestionRequest +from deepseek_harness import CommandExecution, RunCommandResult, RunResult, Session, UserQuestionRequest from test_commands_runtime import ( _Completion, _Runtime, _SESSION_ID, _WAIT_SECONDS, _assert_request_permission, _command_pair, _of_type, _permission_facts, _read_events, @@ -214,6 +214,36 @@ def test_plan_state_survives_resume_without_changing_permissions(tmp_path: Path) assert [e["data"] for e in _of_type(events, "plan/mode")] == [{"active": True}] +def test_top_level_plan_result_identifies_the_session_for_wait_continue_and_resume(tmp_path: Path) -> None: + first = _Completion("PLAN_READY", blocked=True) + with _runtime(tmp_path, [first, _Completion("CONTINUED_PLAN")], initial_permission="workspace-write") as runtime: + execution = runtime.call(runtime.harness.run, "/plan Design this change without tools.") + assert isinstance(execution, RunCommandResult) + assert execution.result.kind == "success" + assert first.entered.wait(_WAIT_SECONDS), runtime.model.failures + assert not first.release.is_set() + session = runtime.harness.start_session(execution.session_id) + first.release.set() + _wait_activity(runtime, session) + continued = runtime.call(runtime.harness.run, "Continue this plan without tools.", session_id=execution.session_id) + assert isinstance(continued, RunResult) + assert continued.session_id == execution.session_id + assert continued.final_response == "CONTINUED_PLAN" + assert "Design this change without tools." in _request_texts(runtime.model.captured()[1]) + with _runtime(tmp_path, [_Completion("RESTORED_PLAN")], initial_permission="workspace-write") as resumed: + session = resumed.call(resumed.harness.resume_session, execution.session_id) + result = resumed.call(session.run, "Finish reviewing this plan without tools.") + assert isinstance(result, RunResult) and result.session_id == execution.session_id + assert result.final_response == "RESTORED_PLAN" + [request] = resumed.model.captured() + assert _GUIDANCE in _effective_system(request) + assert "Design this change without tools." in _request_texts(request) + assert "Continue this plan without tools." in _request_texts(request) + events = _read_events(resumed.logs, session_id=execution.session_id) + _command_pair(events, execution, "plan") + assert len(_of_type(events, "turn/start")) == 3 + + def test_plan_state_is_local_and_literal_prompts_do_not_toggle_it(tmp_path: Path) -> None: with _runtime(tmp_path, [_Completion("OTHER"), _Completion("PLANNING"), _Completion("RAW_INPUT")], initial_permission="workspace-write") as runtime: diff --git a/python/sdk/tests/test_run.py b/python/sdk/tests/test_run.py index b163b4d33c..7a0dee7c39 100644 --- a/python/sdk/tests/test_run.py +++ b/python/sdk/tests/test_run.py @@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor import pytest from pydantic import BaseModel, ValidationError -from deepseek_harness import CommandExecution, DeepSeekHarness, Notification, RunResult, SdkProtocolError +from deepseek_harness import CommandExecution, RunCommandResult, DeepSeekHarness, Notification, RunResult, SdkProtocolError from deepseek_harness.errors import JsonRpcError @@ -146,11 +146,56 @@ def test_run_returns_command_without_waiting_for_idle(sdk: DeepSeekHarness, entr result = (sdk.run("/compact", session_id="main", on_notification=notifications.append) if entry == "harness" else sdk.start_session("main").run("/compact", on_notification=notifications.append)) assert isinstance(result, CommandExecution) + assert isinstance(result, RunCommandResult) + assert result.session_id == "main" assert result.result.kind == "success" assert [notice.payload["event"]["type"] for notice in notifications] == ["command/run", "command/done"] assert_released(sdk) +def test_top_level_command_results_identify_distinct_sessions_and_allow_continuing_one( + sdk: DeepSeekHarness, tmp_path: Path, +) -> None: + first = sdk.run("/compact") + second = sdk.run("/compact") + assert isinstance(first, RunCommandResult) and isinstance(second, RunCommandResult) + assert first.session_id and second.session_id and first.session_id != second.session_id + continued = sdk.run("continue the first session", session_id=first.session_id) + assert isinstance(continued, RunResult) + assert continued.session_id == first.session_id + sent = [json.loads(line) for line in (tmp_path / "requests.jsonl").read_text(encoding="utf-8").splitlines()] + queries = [frame["params"] for frame in sent if frame["method"] == "session/query"] + assert [query["sessionId"] for query in queries] == [first.session_id, second.session_id, first.session_id] + assert_released(sdk) + + +@pytest.mark.parametrize("result", [ + {"kind": "success"}, + {"kind": "success", "text": "Compacted.", "sourceEventSeq": 0}, + {"kind": "success", "sourceEventSeq": 2**53 - 1}, + {"kind": "error", "text": "The agent is busy."}, +]) +def test_run_adds_session_identity_without_changing_command_results(peer: Path, tmp_path: Path, result: dict) -> None: + execution = {"commandId": "custom-command", "result": result} + with DeepSeekHarness( + cwd=str(tmp_path), dsh_home=str(tmp_path / "home"), request_timeout_seconds=5, + _launch_args=(sys.executable, str(peer)), + env={"REQUEST_LOG": str(tmp_path / "requests.jsonl"), "QUERY_RESPONSE": json.dumps({ + "kind": "command", "execution": execution, + })}, + ) as sdk: + returned = sdk.run("custom-response") + assert isinstance(returned, RunCommandResult) + assert returned.session_id + assert returned.model_dump(by_alias=True, exclude_none=True) == { + **execution, "session_id": returned.session_id, + } + low_level = sdk.client._query(returned.session_id, "custom-response", "queue") + assert isinstance(low_level, CommandExecution) + assert low_level.model_dump(by_alias=True, exclude_none=True) == execution + assert_released(sdk) + + @pytest.mark.parametrize("text", ["ordinary", "fast-prompt", "/model", " /compact extra "]) def test_run_preserves_ordinary_text_and_result(sdk: DeepSeekHarness, text: str) -> None: notifications = [] diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 496515924c..47a85ea141 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -884,6 +884,7 @@ async function runScenario(scenario: CorpusScenario): Promise<{ { type: 'text', text: action.line }, ] : action.line) if (!('commandId' in execution)) throw new Error('SDK command input returned prompt activity') + expect(execution.sessionId).toBe(sessionId) collectCommandNotifications(subscription, sessionId, execution, notifications, observe) continue }