mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(subagent): align DSH SDK diagnostics with reachable facts
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: c0fe606ee02447221f2b4727264a7baa93fd9558
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 866cda3c607b00c103cba796f1cba3e8a111f620
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: de443778541470e4000756f3f8613c086e820e14
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 4f0bb590cae020e4ac256352c9946108e440e0d1
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ Three packages, layered exactly like the existing Python stack, plus one Service
|
||||
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-sdk-jsonrpc-server` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
|
||||
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `TurnResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Non-completed child reasons and SDK failures add the bounded safe diagnostic defined by the [out-of-process diagnostics decision](2026-08-21-out-of-process-subagent-minimal-diagnostics.md), using only the child reason, current provider stage, and exported SDK error class. Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`.
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through, `blocked` becomes `refusal`, and remaining non-completed values become `error`). Reachable child failures and SDK errors add the bounded safe diagnostic defined by the [out-of-process diagnostics decision](2026-08-21-out-of-process-subagent-minimal-diagnostics.md), using one category plus the current provider stage. Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`.
|
||||
- **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself.
|
||||
|
||||
`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical); `dsh-jsonrpc-agent-pkg` (the Python runtime closure) gains the `dsh-sdk-protocol` dependency line.
|
||||
@@ -23,7 +23,7 @@ Three packages, layered exactly like the existing Python stack, plus one Service
|
||||
|
||||
Four tiers, per [testing policy](../../../../docs/testing.md):
|
||||
|
||||
- **Keyless unit** — `sdk-client` drives a scripted fake runtime (`tests/fake-runtime.ts`, env-scripted, protocol-only — the Python `test_client.py` pattern) over real stdio; `subagent-dsh-sdk` drives the same fake through the real provider, including child-reason, typed-error, startup, process, and shutdown diagnostics. 100% per-file coverage on all three packages.
|
||||
- **Keyless unit** — `sdk-client` drives a scripted fake runtime (`tests/fake-runtime.ts`, env-scripted, protocol-only — the Python `test_client.py` pattern) over real stdio; `subagent-dsh-sdk` drives the same fake through the real provider, including reachable child reasons, typed errors, and initialize/session-run/shutdown diagnostics. 100% per-file coverage on all three packages.
|
||||
- **Keyless Loader composition** — `subagent-dsh-sdk/tests/loader-composition.e2e.ts` boots a test-only cordis.yml (`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/`) where the child is a REAL second harness runtime with its own cordis.yml; asserts cwd inheritance and the model-visible child-error diagnostic with separate partial output. The child launch resolves through `resolveExampleLaunch`, so src/lib modes both hold.
|
||||
- **Keyless snapshot** — `examples/jsonrpc-agent/tests/sdk.snapshot.ts` is the jsonrpc example's snapshot suite: the real `dsh-jsonrpc-agent` runtime driven through the real `dsh-sdk-client`, replaying recorded fixtures via `llm-replay` behind `cordis.snapshot.yml` overlays passed explicitly through `DSH_CORDIS_CONFIG`. Text, bash, in-process subagent, persistent-tool, and DSH SDK diagnostic scenarios pin the normalized notification stream, SDK result, persisted logs, and the provider's foreground/background failure text.
|
||||
- **With-key e2e** — the snapshot suite's `DSH_SNAPSHOT=record` mode is the live-API path (it produced the committed fixtures); the composition e2e needs no key by design.
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
|
||||
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/sdk/protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport` 从 `dsh-sdk-jsonrpc-server` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data` 的 `JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
|
||||
- **`@deepseek-ai/dsh-sdk-client`**(`packages/sdk/client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`TurnResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费方的发行问题);`env` 整体替换而非合并(凭据策略归调用方;subprocess seam 的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。非完成子原因与 SDK 失败会附加[进程外诊断决策](2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md)定义的有界安全诊断,只使用子原因、当前提供方 stage 与导出的 SDK 错误 class。其 `provider`/`model` 配置喂给子进程的 `initialize`;`env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通,`blocked` 变为 `refusal`,其余非完成值变为 `error`)。可达子失败与 SDK 错误会附加[进程外诊断决策](2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md)定义的有界安全诊断,只使用一个 category 和当前提供方 stage。其 `provider`/`model` 配置喂给子进程的 `initialize`;`env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。
|
||||
- **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。
|
||||
|
||||
`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致);`dsh-jsonrpc-agent-pkg`(Python 运行时闭包)增加 `dsh-sdk-protocol` 一行依赖。
|
||||
@@ -23,7 +23,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
|
||||
|
||||
四层,依[测试政策](../../../../docs/testing.zh.md):
|
||||
|
||||
- **免密钥单元**——`sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-dsh-sdk` 经真实提供方驱动同一伪运行时,包括子原因、typed 错误、启动、进程与 shutdown 诊断。三个包全部 100% 逐文件覆盖。
|
||||
- **免密钥单元**——`sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-dsh-sdk` 经真实提供方驱动同一伪运行时,包括可达子原因、typed 错误,以及 initialize/session-run/shutdown 诊断。三个包全部 100% 逐文件覆盖。
|
||||
- **免密钥 Loader 组合**——`subagent-dsh-sdk/tests/loader-composition.e2e.ts` 启动仅测试用 cordis.yml(`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-dsh-sdk/`),其中子进程是真实的第二个 harness 运行时、带自己的 cordis.yml;断言 cwd 继承,以及模型可见的子错误诊断与分离的部分输出。子启动经 `resolveExampleLaunch` 解析,src/lib 两种模式都成立。
|
||||
- **免密钥快照**——`examples/jsonrpc-agent/tests/sdk.snapshot.ts` 是 jsonrpc 示例的 snapshot 套件:真实 `dsh-jsonrpc-agent` 运行时经真实 `dsh-sdk-client` 驱动,并经由 `DSH_CORDIS_CONFIG` 显式传入的 `cordis.snapshot.yml` 覆盖层回放已录制 fixture。文本、bash、进程内 subagent、持久工具与 DSH SDK 诊断场景会固定规范化通知流、SDK 结果、持久日志,以及提供方前台/后台失败文本。
|
||||
- **带密钥 e2e**——快照套件的 `DSH_SNAPSHOT=record` 模式即真实 API 路径(已提交 fixture 由它产出);组合 e2e 设计上无需密钥。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-21-out-of-process-subagent-minimal-diagnostics.md
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.md: 58ac32c7a3869e8acb18a130660095c8fcd8a671
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md: 9b36c45594931303e6582f995c442a6c804dedc4
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.md: fa76f9116cce6ab6632c3bb601623e078e64c77a
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md: aa4e6f44f900093c57fb311900c06fe9154ebe3e
|
||||
|
||||
+6
-7
@@ -42,14 +42,13 @@ When an ACP permission request contributes to a non-completed result, a second f
|
||||
|
||||
| Stage | Owned operation | Safe categories and facts |
|
||||
| --- | --- | --- |
|
||||
| `initialize` | Parent workspace resolution, SDK runtime spawn, and initialize handshake | `configuration`, `protocol`, `timeout`, `transport`, or `unknown` |
|
||||
| `session-run` | Prompt acceptance, session notifications, and final child reason | `child-error`, `child-interrupted`, `child-disposed`, `child-blocked`, `missing-terminal`, `protocol`, `timeout`, or `unknown` |
|
||||
| `process` | SDK transport closes during a published child run | `transport`; the Error message and its stderr tail stay internal |
|
||||
| `initialize` | Parent workspace resolution, SDK runtime spawn, and initialize handshake | `configuration`, `protocol`, `transport`, or `unknown` |
|
||||
| `session-run` | Prompt acceptance, session notifications, and final child reason | `child-error`, `child-disposed`, `child-unknown`, `missing-terminal`, `protocol`, `transport`, or `unknown` |
|
||||
| `shutdown` | Bounded SDK shutdown and runtime process release | The same typed SDK categories with shutdown stage |
|
||||
|
||||
Child `completed`, `max-tokens`, and ordinary `aborted` results keep their existing shared stop reasons without extra text. An `aborted` turn whose closed cause is `disposed` keeps `aborted` and adds `child-disposed`. `blocked`, `error`, and `interrupted` remain `error` and add their fixed categories. A missing terminal event adds `missing-terminal`; an unknown reason uses `unknown` without copying the value or the child's structured failure message.
|
||||
Child `completed`, `max-tokens`, and ordinary `aborted` results keep their existing shared stop reasons without extra text. An `aborted` turn whose closed cause is `disposed` keeps `aborted` and adds `child-disposed`. `blocked` reuses `refusal`; `error` adds `child-error`. A missing terminal event adds `missing-terminal`; an unknown or unreachable reason uses `child-unknown` without copying the value or the child's structured failure message.
|
||||
|
||||
`SdkProtocolError` and JSON-RPC error responses map to `protocol`, `RequestTimeoutError` maps to `timeout`, and `TransportClosedError` maps to `transport`; the provider never reads their messages. Other exceptions use `unknown`.
|
||||
`SdkProtocolError` and JSON-RPC error responses map to `protocol`, and `TransportClosedError` maps to `transport`; the provider never reads their messages. Other exceptions use `unknown`. Request timeout classification remains deferred because this provider does not configure or propagate a request timeout.
|
||||
|
||||
### Ownership and lifecycle
|
||||
|
||||
@@ -66,7 +65,7 @@ Startup publishes no run until the provider's handshake completes. A startup fai
|
||||
|
||||
## Verification
|
||||
|
||||
ACP package tests drive a real stdio protocol child and pin every stop-reason mapping, remote-limit and unknown fallbacks, permission allow/deny facts, configuration, initialize, new-session, prompt, process, and teardown stages, startup rollback, successful-result and local-cancellation omission, partial output, concurrent-run isolation, Host-only raw errors, process quiescence, and the shared multibyte diagnostic limit. DSH SDK package tests drive the real SDK client against its stdio fake runtime and pin every child reason, typed SDK category, all four stages, startup and shutdown aggregation, partial output, cancellation omission, concurrency, sanitization, and quiescence. Loader compositions prove each real configured provider reaches the model-visible foreground result. Keyless ACP and JSON-RPC snapshots pin each provider's exact foreground and one-shot background diagnostic text.
|
||||
ACP package tests drive a real stdio protocol child and pin every stop-reason mapping, remote-limit and unknown fallbacks, permission allow/deny facts, configuration, initialize, new-session, prompt, process, and teardown stages, startup rollback, successful-result and local-cancellation omission, partial output, concurrent-run isolation, Host-only raw errors, process quiescence, and the shared multibyte diagnostic limit. DSH SDK package tests drive the real SDK client against its stdio fake runtime and pin every reachable child reason, current typed SDK category, initialize/session-run/shutdown stages, SDK-owned failed-start cleanup, cancellation cleanup, partial output, concurrency, sanitization, and quiescence. Loader compositions prove each real configured provider reaches the model-visible foreground result. Keyless ACP and JSON-RPC snapshots pin each provider's exact foreground and one-shot background diagnostic text.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -82,6 +81,6 @@ ACP package tests drive a real stdio protocol child and pin every stop-reason ma
|
||||
|
||||
## Consequences
|
||||
|
||||
The parent can distinguish an ACP remote limit or permission decision and a DSH child-turn, protocol, timeout, transport/process, or shutdown failure without receiving child-controlled text. Startup and cleanup errors use the same safe facts as published results, while Host observation retains the original cause.
|
||||
The parent can distinguish an ACP remote limit or permission decision and a DSH child-turn, protocol, transport, or shutdown failure without receiving child-controlled text. Startup and cleanup errors use the same safe facts as published results, while Host observation retains the original cause.
|
||||
|
||||
The diagnostic remains display text rather than a public protocol. Consumers may present it but must not branch on its format. This decision adds no retry policy, recovery controller, shared provider-error enum, stderr classifier, authentication taxonomy, session persistence, progress stream, or new ACP capability.
|
||||
|
||||
+6
-7
@@ -42,14 +42,13 @@ Subagent failure (provider: <provider>; stage: <stage>; category: <category>; st
|
||||
|
||||
| Stage | 归属操作 | 安全 category 与事实 |
|
||||
| --- | --- | --- |
|
||||
| `initialize` | 父工作区解析、SDK 运行时 spawn 与 initialize 握手 | `configuration`、`protocol`、`timeout`、`transport` 或 `unknown` |
|
||||
| `session-run` | prompt 接受、会话通知与最终子轮次原因 | `child-error`、`child-interrupted`、`child-disposed`、`child-blocked`、`missing-terminal`、`protocol`、`timeout` 或 `unknown` |
|
||||
| `process` | 已发布子运行期间 SDK 传输关闭 | `transport`;Error 消息及其中的 stderr tail 仍留在内部 |
|
||||
| `initialize` | 父工作区解析、SDK 运行时 spawn 与 initialize 握手 | `configuration`、`protocol`、`transport` 或 `unknown` |
|
||||
| `session-run` | prompt 接受、会话通知与最终子轮次原因 | `child-error`、`child-disposed`、`child-unknown`、`missing-terminal`、`protocol`、`transport` 或 `unknown` |
|
||||
| `shutdown` | 有界 SDK shutdown 与运行时进程释放 | 使用 shutdown stage 的同一套 typed SDK category |
|
||||
|
||||
子 `completed`、`max-tokens` 与普通 `aborted` 结果保持既有共享结束原因,不附加文本。闭集原因是 `disposed` 的 `aborted` 轮次仍保持 `aborted`,并附加 `child-disposed`。`blocked`、`error` 与 `interrupted` 继续映射到 `error`,并附加各自固定 category。缺失终态事件会附加 `missing-terminal`;未知原因使用 `unknown`,且不复制原值或子进程结构化失败消息。
|
||||
子 `completed`、`max-tokens` 与普通 `aborted` 结果保持既有共享结束原因,不附加文本。闭集原因是 `disposed` 的 `aborted` 轮次仍保持 `aborted`,并附加 `child-disposed`。`blocked` 复用 `refusal`;`error` 附加 `child-error`。缺失终态事件会附加 `missing-terminal`;未知或不可达原因使用 `child-unknown`,且不复制原值或子进程结构化失败消息。
|
||||
|
||||
`SdkProtocolError` 与 JSON-RPC 错误响应映射为 `protocol`,`RequestTimeoutError` 映射为 `timeout`,`TransportClosedError` 映射为 `transport`;提供方绝不读取其消息。其他异常使用 `unknown`。
|
||||
`SdkProtocolError` 与 JSON-RPC 错误响应映射为 `protocol`,`TransportClosedError` 映射为 `transport`;提供方绝不读取其消息。其他异常使用 `unknown`。由于本提供方没有配置或传播 request timeout,请求超时分类继续推迟。
|
||||
|
||||
### 所有权与生命周期
|
||||
|
||||
@@ -66,7 +65,7 @@ Subagent failure (provider: <provider>; stage: <stage>; category: <category>; st
|
||||
|
||||
## Verification
|
||||
|
||||
ACP 包测试通过真实 stdio 协议子进程固定全部结束原因映射、远端限制与 unknown 回退、权限 allow/deny 事实、configuration、initialize、new-session、prompt、process 与 teardown stage、启动回滚、成功结果与本地取消省略、部分输出、并发运行隔离、仅 Host 可见的原始错误、进程完全停稳,以及共享多字节诊断限制。DSH SDK 包测试通过真实 SDK 客户端驱动其 stdio 伪运行时,固定全部子轮次原因、typed SDK category、四个 stage、启动与 shutdown 聚合、部分输出、取消省略、并发、脱敏与停稳。Loader 组合证明两个真实配置的提供方都能到达模型可见前台结果。无密钥 ACP 与 JSON-RPC snapshot 会固定各自提供方的准确前台与一次性后台诊断文本。
|
||||
ACP 包测试通过真实 stdio 协议子进程固定全部结束原因映射、远端限制与 unknown 回退、权限 allow/deny 事实、configuration、initialize、new-session、prompt、process 与 teardown stage、启动回滚、成功结果与本地取消省略、部分输出、并发运行隔离、仅 Host 可见的原始错误、进程完全停稳,以及共享多字节诊断限制。DSH SDK 包测试通过真实 SDK 客户端驱动其 stdio 伪运行时,固定全部可达子轮次原因、当前 typed SDK category、initialize/session-run/shutdown stage、SDK 自有失败启动清理、本地取消清理、部分输出、并发、脱敏与停稳。Loader 组合证明两个真实配置的提供方都能到达模型可见前台结果。无密钥 ACP 与 JSON-RPC snapshot 会固定各自提供方的准确前台与一次性后台诊断文本。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -82,6 +81,6 @@ ACP 包测试通过真实 stdio 协议子进程固定全部结束原因映射、
|
||||
|
||||
## Consequences
|
||||
|
||||
父 agent 可以区分 ACP 远端限制或权限决定,以及 DSH 子轮次、协议、超时、传输/进程或 shutdown 失败,同时不会接收子进程控制的文本。启动和清理错误与已发布结果使用同一套安全事实,而 Host 观测仍保留原始 cause。
|
||||
父 agent 可以区分 ACP 远端限制或权限决定,以及 DSH 子轮次、协议、传输或 shutdown 失败,同时不会接收子进程控制的文本。启动和清理错误与已发布结果使用同一套安全事实,而 Host 观测仍保留原始 cause。
|
||||
|
||||
诊断仍是展示文本,不是公共协议。消费方可以呈现它,但不得按格式分支。本决策不增加重试策略、恢复控制器、共享提供方错误 enum、stderr 分类器、认证分类、会话持久化、进度流或新的 ACP 能力。
|
||||
|
||||
@@ -27,5 +27,28 @@
|
||||
toolName: subagent_dsh_sdk
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: subagent-dsh-sdk-diagnostic-background
|
||||
name: '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
config:
|
||||
providerName: dsh-sdk-diagnostic-background
|
||||
command: !!js process.execPath
|
||||
args:
|
||||
- !!js process.env.DSH_TEST_FAKE_SDK_RUNTIME
|
||||
provider: fake-provider
|
||||
model: fake-model
|
||||
env:
|
||||
FAKE_TEXT: partial DSH SDK assistant text
|
||||
FAKE_REASON_KIND: error
|
||||
FAKE_INIT_READY: .dsh-sdk-background-ready
|
||||
FAKE_INIT_GO: .dsh-sdk-background-release
|
||||
- id: tool-subagent-dsh-sdk-diagnostic-background
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: dsh-sdk-diagnostic-background
|
||||
toolName: subagent_dsh_sdk_background
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: tool-jobs-dsh-sdk-diagnostic
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
- id: release-dsh-sdk-background-on-job-output
|
||||
name: './tests/fixtures/subagent-dsh-sdk-release-on-job-output.ts'
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
paceMs: 100
|
||||
providers:
|
||||
- id: deepseek-official
|
||||
name: DeepSeek
|
||||
@@ -37,5 +36,28 @@
|
||||
toolName: subagent_dsh_sdk
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: subagent-dsh-sdk-diagnostic-background
|
||||
name: '@deepseek-ai/dsh-subagent-dsh-sdk'
|
||||
config:
|
||||
providerName: dsh-sdk-diagnostic-background
|
||||
command: !!js process.execPath
|
||||
args:
|
||||
- !!js process.env.DSH_TEST_FAKE_SDK_RUNTIME
|
||||
provider: fake-provider
|
||||
model: fake-model
|
||||
env:
|
||||
FAKE_TEXT: partial DSH SDK assistant text
|
||||
FAKE_REASON_KIND: error
|
||||
FAKE_INIT_READY: .dsh-sdk-background-ready
|
||||
FAKE_INIT_GO: .dsh-sdk-background-release
|
||||
- id: tool-subagent-dsh-sdk-diagnostic-background
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: dsh-sdk-diagnostic-background
|
||||
toolName: subagent_dsh_sdk_background
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: tool-jobs-dsh-sdk-diagnostic
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
- id: release-dsh-sdk-background-on-job-output
|
||||
name: './tests/fixtures/subagent-dsh-sdk-release-on-job-output.ts'
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/** Release the gated background SDK child only after job_output starts waiting. */
|
||||
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
|
||||
export const name = 'subagent-dsh-sdk-release-on-job-output'
|
||||
export const inject = ['tools']
|
||||
|
||||
/**
|
||||
* Register the test-only execution-order barrier.
|
||||
* @param ctx - parent runtime context carrying the tool execution waterfall.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.on('tools/execute', async (exec, next) => {
|
||||
const delegated = next()
|
||||
if (exec.name === 'job_output') {
|
||||
writeFileSync('.dsh-sdk-background-release', 'release\n')
|
||||
}
|
||||
return delegated
|
||||
}, { prepend: true })
|
||||
}
|
||||
@@ -105,7 +105,7 @@ const SCENARIOS: SdkScenario[] = [
|
||||
},
|
||||
{
|
||||
name: 'subagent-dsh-sdk-diagnostic',
|
||||
prompt: 'Observe the DSH SDK diagnostic twice with subagent_dsh_sdk. First call it in the foreground. Then call it in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools.',
|
||||
prompt: 'Observe the DSH SDK diagnostic twice. First call subagent_dsh_sdk in the foreground. Then call subagent_dsh_sdk_background in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools.',
|
||||
sessionId: 'sdk-snapshot-dsh-sdk-diagnostic',
|
||||
children: 0,
|
||||
configs: { live: diagnosticLiveConfig, replay: diagnosticReplayConfig },
|
||||
|
||||
+25
-28
@@ -1,9 +1,9 @@
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice with subagent_dsh_sdk. First call it in the foreground. Then call it in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":0,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice. First call subagent_dsh_sdk in the foreground. Then call subagent_dsh_sdk_background in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}}
|
||||
{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":1,"time":0,"data":{"turn":1}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice with subagent_dsh_sdk. First call it in the foreground. Then call it in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice. First call subagent_dsh_sdk in the foreground. Then call subagent_dsh_sdk_background in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":5,"time":0,"data":{"title":"Observe the DSH SDK diagnostic","messageSeqs":[4],"source":{"kind":"fallback"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}
|
||||
@@ -14,38 +14,35 @@
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_foreground","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK foreground failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"call_dsh_sdk_foreground","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK foreground failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":false}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_foreground"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)\nPartial output before the run ended:\npartial DSH SDK assistant text"}],"isError":true}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_foreground"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)\nPartial output before the run ended:\npartial DSH SDK assistant text"}],"isError":true}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_background","name":"subagent_dsh_sdk","argumentsDelta":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","argumentsDelta":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":23,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":24,"time":0,"data":{"turn":1,"step":2,"callId":"call_dsh_sdk_background","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":23,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":24,"time":0,"data":{"turn":1,"step":2,"callId":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":25,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_background"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_background","content":[{"type":"text","text":"started background subagent job subagent-1"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":26,"time":0,"data":{"turn":1,"step":2}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":27,"time":0,"data":{"turn":1,"step":3}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":28,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job subagent-1 (subagent: Observe DSH SDK background failure) finished [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe DSH SDK background failure [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; st…"},"role":"user","id":"{{sessionId}}"}]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_output"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":37,"time":0,"data":{"turn":1,"step":3}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":38,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":39,"time":0,"data":{"turn":1,"step":4}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":40,"time":0,"data":{"content":[{"type":"text","text":"background job subagent-1 (subagent: Observe DSH SDK background failure) finished [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe DSH SDK background failure [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; st…"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":44,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":47,"time":0,"data":{"turn":1,"step":4}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":48,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":33,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":34,"time":0,"data":{"turn":1,"step":3,"callId":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":35,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_output"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":36,"time":0,"data":{"turn":1,"step":3}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":37,"time":0,"data":{"turn":1,"step":4}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":43,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":44,"time":0,"data":{"turn":1,"step":4}}}}
|
||||
{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":45,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}}
|
||||
{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{"type":"session","version":0,"id":"sdk-snapshot-dsh-sdk-diagnostic","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"agent/inbox/spliced","seq":0,"time":1787257517557,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice with subagent_dsh_sdk. First call it in the foreground. Then call it in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"fb50a593-f729-430e-a0b4-591fd05c9a80"}]}}
|
||||
{"type":"agent/inbox/spliced","seq":0,"time":1787257517557,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice. First call subagent_dsh_sdk in the foreground. Then call subagent_dsh_sdk_background in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"8b7f4e4f-76fb-4c3b-bab5-28569fdde5a8"}]}}
|
||||
{"type":"turn/start","seq":1,"time":1787257517557,"data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","seq":2,"time":1787257517557,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":3,"time":1787257517607,"data":{"turn":1,"step":1}}
|
||||
{"type":"user/message","seq":4,"time":1787257517607,"data":{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice with subagent_dsh_sdk. First call it in the foreground. Then call it in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"fb50a593-f729-430e-a0b4-591fd05c9a80"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1787257517607,"data":{"content":[{"type":"text","text":"Observe the DSH SDK diagnostic twice. First call subagent_dsh_sdk in the foreground. Then call subagent_dsh_sdk_background in the background and collect subagent-1 with job_output using wait true. After both failures, reply with exactly PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"8b7f4e4f-76fb-4c3b-bab5-28569fdde5a8"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":5,"time":1787257517607,"data":{"title":"Observe the DSH SDK diagnostic","messageSeqs":[4],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":6,"time":1787257517609,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":7,"time":1787257517609,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
@@ -14,37 +14,34 @@
|
||||
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":13,"time":1787257517614,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_foreground","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK foreground failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"fb422889-0cdd-4a7f-8228-b94db8ae58ca"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":14,"time":1787257517615,"data":{"turn":1,"step":1,"callId":"call_dsh_sdk_foreground","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK foreground failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":false}"}}
|
||||
{"type":"tool/result","seq":15,"time":1787257517679,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_foreground"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)\nPartial output before the run ended:\npartial DSH SDK assistant text"}],"isError":true}],"role":"user","id":"d3fb3f99-1bab-432b-b79e-009e6a8e2891"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":15,"time":1787257517679,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_foreground"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)\nPartial output before the run ended:\npartial DSH SDK assistant text"}],"isError":true}],"role":"user","id":"20d7cece-10e0-461e-85e1-6e2209a8e24d"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":16,"time":1787257517679,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":17,"time":1787257517683,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_background","name":"subagent_dsh_sdk","argumentsDelta":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","argumentsDelta":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}}}
|
||||
{"type":"assistant/chunk","seq":21,"time":1787257517687,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":22,"time":1787257517687,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":23,"time":1787257517687,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"2c0e8318-c5f2-4885-be54-8716b21005e0"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":24,"time":1787257517688,"data":{"turn":1,"step":2,"callId":"call_dsh_sdk_background","name":"subagent_dsh_sdk","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}
|
||||
{"type":"assistant/message","seq":23,"time":1787257517687,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"2c0e8318-c5f2-4885-be54-8716b21005e0"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":24,"time":1787257517688,"data":{"turn":1,"step":2,"callId":"call_dsh_sdk_background","name":"subagent_dsh_sdk_background","arguments":"{\"description\":\"Observe DSH SDK background failure\",\"prompt\":\"Return the scripted DSH SDK failure.\",\"run_in_background\":true}"}}
|
||||
{"type":"tool/result","seq":25,"time":1787257517691,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_background"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_background","content":[{"type":"text","text":"started background subagent job subagent-1"}],"isError":false}],"role":"user","id":"092ae9df-ff5f-4add-8533-3e800d54d4e9"}},"sourceEventSeqs":[24],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":26,"time":1787257517692,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":27,"time":1787257517695,"data":{"turn":1,"step":3}}
|
||||
{"type":"agent/inbox/spliced","seq":28,"time":1787260957336,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job subagent-1 (subagent: Observe DSH SDK background failure) finished [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe DSH SDK background failure [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; st…"},"role":"user","id":"fa91218c-343e-4ca5-99c8-fdb48b3b7377"}]}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}
|
||||
{"type":"assistant/chunk","seq":31,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}
|
||||
{"type":"assistant/chunk","seq":32,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":33,"time":1787260957803,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":34,"time":1787260957804,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"1233bddb-7034-4298-b30d-4edc9cb541e6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":35,"time":1787260957804,"data":{"turn":1,"step":3,"callId":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}
|
||||
{"type":"tool/result","seq":36,"time":1787260957811,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_output"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)]"}],"isError":false}],"role":"user","id":"2d58158d-6d53-4b87-8423-9a64e567bf68"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":37,"time":1787260957811,"data":{"turn":1,"step":3}}
|
||||
{"type":"agent/inbox/spliced","seq":38,"time":1787260957811,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":39,"time":1787260957816,"data":{"turn":1,"step":4}}
|
||||
{"type":"user/message","seq":40,"time":1787260957817,"data":{"content":[{"type":"text","text":"background job subagent-1 (subagent: Observe DSH SDK background failure) finished [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe DSH SDK background failure [status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; st…"},"role":"user","id":"fa91218c-343e-4ca5-99c8-fdb48b3b7377"},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":41,"time":1787257517711,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":42,"time":1787257517711,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}
|
||||
{"type":"assistant/chunk","seq":43,"time":1787260958124,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}}
|
||||
{"type":"assistant/chunk","seq":44,"time":1787260958226,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
|
||||
{"type":"assistant/chunk","seq":45,"time":1787260958327,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":46,"time":1787260958327,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"8bdd016d-cb60-4922-a239-65958f5e9910"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":47,"time":1787260958327,"data":{"turn":1,"step":4}}
|
||||
{"type":"turn/end","seq":48,"time":1787260958328,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1787262595001,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_dsh_sdk_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}
|
||||
{"type":"assistant/chunk","seq":31,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":32,"time":1787257517699,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":33,"time":1787262595001,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"1233bddb-7034-4298-b30d-4edc9cb541e6"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":34,"time":1787262595001,"data":{"turn":1,"step":3,"callId":"call_dsh_sdk_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}
|
||||
{"type":"tool/result","seq":35,"time":1787262595046,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_dsh_sdk_output"},"content":[{"type":"tool-result","toolCallId":"call_dsh_sdk_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)]"}],"isError":false}],"role":"user","id":"cb9e08e1-3f02-41b4-a670-72afccdd15a7"}},"sourceEventSeqs":[34],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":36,"time":1787262595046,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":37,"time":1787262595050,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":38,"time":1787262595054,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":39,"time":1787262595054,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}
|
||||
{"type":"assistant/chunk","seq":40,"time":1787262595054,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}}}}
|
||||
{"type":"assistant/chunk","seq":41,"time":1787257517711,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
|
||||
{"type":"assistant/chunk","seq":42,"time":1787257517711,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":43,"time":1787262595054,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_DSH_SDK_DIAGNOSTIC"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"8bdd016d-cb60-4922-a239-65958f5e9910"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":44,"time":1787262595054,"data":{"turn":1,"step":4}}
|
||||
{"type":"turn/end","seq":45,"time":1787262595054,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/sdk/client/README.md
|
||||
README.md: b33457875f81d11d09bab2e5aa5ce730e233c78a
|
||||
README.zh.md: b89e56629fefe572394751bc1bee38aaba6f3300
|
||||
README.md: 92c43004bbde7e2740906ed9496fee4b92af5083
|
||||
README.zh.md: c5fad93264b21ff4be895f80338da92ac1b87e8c
|
||||
|
||||
@@ -21,7 +21,7 @@ const result = await harness.run('say hi')
|
||||
console.log(result.finalResponse)
|
||||
```
|
||||
|
||||
The subprocess starts lazily on first use and stays owned by the instance across `run()` calls; `close()` (or `await using`) is required so the child is always reaped. `start()` memoizes the `initialize` handshake (the workspace cwd — resolved absolute before it crosses the wire — plus the provider/model route and optional positive `maxTokens` output cap); a failed handshake reaps the runtime and swaps in a fresh client, so a later call retries with a new subprocess (until `close()`, which is terminal). The cap applies to each root-agent request and is inherited by in-process descendants; compaction plugins own their separate summary limits. `session(id?)` opens a named or fresh session handle.
|
||||
The subprocess starts lazily on first use and stays owned by the instance across `run()` calls; `close()` (or `await using`) is required so the child is always reaped. `start()` memoizes the `initialize` handshake (the workspace cwd — resolved absolute before it crosses the wire — plus the provider/model route and optional positive `maxTokens` output cap); a failed handshake reaps the runtime and swaps in a fresh client, so a later call retries with a new subprocess (until `close()`, which is terminal). If both initialize and that SDK-owned cleanup fail, `start()` rejects with an `AggregateError` whose ordered errors preserve both causes. The cap applies to each root-agent request and is inherited by in-process descendants; compaction plugins own their separate summary limits. `session(id?)` opens a named or fresh session handle.
|
||||
|
||||
`run(input, { sessionId?, onNotification? })` owns one activity interval: it queues the prompt, waits until its `MessageId` appears in a durable `agent/inbox/spliced` receipt, then collects through the next whole-agent `idle`. It returns `RunResult { sessionId, finalResponse, events, notifications }`. `finalResponse` is the last committed root-session assistant text in that interval, not a response causally assigned to the prompt; steering, injected context, and other queued work may contribute before idle. `events` contains root-session events, while `notifications` also contains descendants discovered from `subagent.started`, all in wire order. The result carries no prompt-level status or turn reason. Transport loss, timeout, and protocol violations reject; model outcomes remain observable in the event stream without being attributed to one input.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const result = await harness.run('say hi')
|
||||
console.log(result.finalResponse)
|
||||
```
|
||||
|
||||
子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被回收。`start()` 记忆化 `initialize` 握手(工作区 cwd——在通过协议传输之前解析为绝对路径——加 provider/model 路由和可选的正整数 `maxTokens` 输出上限);握手失败会回收运行时并换入全新客户端,后续调用用新子进程重试(直到终结性的 `close()`)。该上限作用于根 agent(智能体)的每次请求,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。`session(id?)` 打开具名或全新的会话句柄。
|
||||
子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被回收。`start()` 记忆化 `initialize` 握手(工作区 cwd——在通过协议传输之前解析为绝对路径——加 provider/model 路由和可选的正整数 `maxTokens` 输出上限);握手失败会回收运行时并换入全新客户端,后续调用用新子进程重试(直到终结性的 `close()`)。如果 initialize 与这次 SDK 自有清理都失败,`start()` 会以 `AggregateError` 拒绝,其有序 errors 保留两个 cause。该上限作用于根 agent(智能体)的每次请求,并由进程内后代继承;压缩(compaction)插件单独持有摘要上限。`session(id?)` 打开具名或全新的会话句柄。
|
||||
|
||||
`run(input, { sessionId?, onNotification? })` 拥有一个活动区间:它将提示词排入队列,等待其 `MessageId` 出现在持久的 `agent/inbox/spliced` 回执中,然后持续收集到整个 agent 下一次进入 `idle`。它返回 `RunResult { sessionId, finalResponse, events, notifications }`。`finalResponse` 是该区间内根会话最后提交的助手文本,并非因果上归属于该提示词的响应;steering(中途引导)、注入的上下文和其他排队工作都可能在 idle 前参与其中。`events` 包含根会话事件,`notifications` 还包含通过 `subagent.started` 发现的后代,均按协议传输顺序排列。结果不携带提示词级状态或轮次原因。传输丢失、超时和协议违例会导致 Promise 被拒绝;模型结果仍可在事件流中观察,但不会归属于某一输入。
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ export class DeepSeekHarness implements AsyncDisposable {
|
||||
* Start the subprocess and perform the `initialize` handshake once. On
|
||||
* failure the runtime is reaped and a fresh client replaces it
|
||||
* (`HarnessClient.close` is permanent), so a later call retries with a new
|
||||
* subprocess — unless {@link close} already ended this harness.
|
||||
* subprocess — unless {@link close} already ended this harness. When both
|
||||
* initialize and SDK-owned cleanup fail, rejects with an `AggregateError`
|
||||
* whose ordered errors preserve both causes.
|
||||
* @returns settlement of the (memoized) handshake.
|
||||
*/
|
||||
start(): Promise<void> {
|
||||
@@ -71,7 +73,14 @@ export class DeepSeekHarness implements AsyncDisposable {
|
||||
})
|
||||
} catch (error) {
|
||||
this.initialized = undefined
|
||||
await this.clientInstance.close()
|
||||
try {
|
||||
await this.clientInstance.close()
|
||||
} catch (cleanupError: unknown) {
|
||||
throw new AggregateError(
|
||||
[error, cleanupError],
|
||||
'DeepSeek Harness initialization and cleanup failed',
|
||||
)
|
||||
}
|
||||
if (!this.closed) this.clientInstance = new HarnessClient(this.launch)
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { mkdir, mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { isAbsolute, join, relative, resolve as resolvePath } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
DeepSeekHarness,
|
||||
HarnessClient,
|
||||
@@ -208,6 +208,46 @@ describe('DeepSeekHarness', () => {
|
||||
await expect(harness.run('later')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('preserves both initialize and SDK-owned cleanup failures', async () => {
|
||||
const initializeError = new SdkProtocolError('malformed initialize')
|
||||
const cleanupError = new Error('cleanup failed')
|
||||
const start = vi.spyOn(HarnessClient.prototype, 'start').mockImplementation(() => {})
|
||||
const initialize = vi.spyOn(HarnessClient.prototype, 'initialize').mockRejectedValue(initializeError)
|
||||
const close = vi.spyOn(HarnessClient.prototype, 'close').mockRejectedValue(cleanupError)
|
||||
try {
|
||||
const harness = new DeepSeekHarness({ launch: { command: 'unused' } })
|
||||
const failure = await harness.start().catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(AggregateError)
|
||||
expect((failure as AggregateError).errors).toEqual([initializeError, cleanupError])
|
||||
expect((failure as Error).message).toBe('DeepSeek Harness initialization and cleanup failed')
|
||||
} finally {
|
||||
start.mockRestore()
|
||||
initialize.mockRestore()
|
||||
close.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not replace the client after terminal close wins a failed handshake', async () => {
|
||||
let rejectInitialize!: (error: Error) => void
|
||||
const initializeResult = new Promise<never>((_resolve, reject) => { rejectInitialize = reject })
|
||||
const start = vi.spyOn(HarnessClient.prototype, 'start').mockImplementation(() => {})
|
||||
const initialize = vi.spyOn(HarnessClient.prototype, 'initialize').mockReturnValue(initializeResult)
|
||||
const close = vi.spyOn(HarnessClient.prototype, 'close').mockResolvedValue()
|
||||
try {
|
||||
const harness = new DeepSeekHarness({ launch: { command: 'unused' } })
|
||||
const original = harness.client
|
||||
const pending = harness.start()
|
||||
await harness.close()
|
||||
rejectInitialize(new SdkProtocolError('late initialize failure'))
|
||||
await expect(pending).rejects.toThrow('late initialize failure')
|
||||
expect(harness.client).toBe(original)
|
||||
} finally {
|
||||
start.mockRestore()
|
||||
initialize.mockRestore()
|
||||
close.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('retries a failed handshake with a fresh runtime process', async () => {
|
||||
const dir = await tempDir('sdk-client-retry-')
|
||||
const marker = join(dir, 'first-boot-failed')
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-dsh-sdk/README.md
|
||||
README.md: df7953eebe7a4239e9346136a78928ae7eb2bb6e
|
||||
README.zh.md: f422e9486d03abee5649d37d33ac665d8d4f9d95
|
||||
README.md: e8e478963eb8d1eba677ec3f69ea59a5d60b0964
|
||||
README.zh.md: 66a9001a0afb75e44d2f99cf42d93274327c5bce
|
||||
|
||||
@@ -23,21 +23,20 @@ The SDK client returns an owned child activity rather than a prompt result. The
|
||||
| `completed` | `completed` | None. |
|
||||
| `max-tokens` | `max-tokens` | None; the stop reason is already actionable. |
|
||||
| `aborted` | `aborted` | `child-disposed` only for the closed `disposed` cause; local parent cancellation never adds one. |
|
||||
| `blocked` | `error` | `child-blocked`. |
|
||||
| `blocked` | `refusal` | None; the shared stop reason already identifies a declined task. |
|
||||
| `error` | `error` | `child-error`; the child failure message/code is excluded. |
|
||||
| `interrupted` | `error` | `child-interrupted`. |
|
||||
| no `turn/end` | `error` | `missing-terminal`. |
|
||||
| unknown variant | `error` | Fixed `unknown`; the value is not copied. |
|
||||
| unknown or unreachable variant | `error` | Fixed `child-unknown`; the value is not copied. |
|
||||
|
||||
## Failure diagnostics
|
||||
|
||||
The first line follows the shared fixed form:
|
||||
|
||||
```text
|
||||
Subagent failure (provider: DSH SDK; stage: <stage>; category: <category>; child reason: <reason>)
|
||||
Subagent failure (provider: DSH SDK; stage: <stage>; category: <category>)
|
||||
```
|
||||
|
||||
Unavailable optional fields are omitted, and the shared result boundary limits the complete text to 4096 UTF-8 bytes. The provider derives `initialize`, `session-run`, `process`, or `shutdown` at the operation that owns the failure. `SdkProtocolError` and JSON-RPC error responses map to `protocol`, `RequestTimeoutError` maps to `timeout`, `TransportClosedError` maps to `transport` (with `process` stage during a published child run), and other exceptions use `unknown`. Classification never reads an error message, so the stderr tail carried by `TransportClosedError`, paths, task content, environment values, credentials, and protocol payloads remain Host-only.
|
||||
The shared result boundary limits the complete text to 4096 UTF-8 bytes. The provider derives `initialize`, `session-run`, or `shutdown` at the operation that owns the failure. `SdkProtocolError` and JSON-RPC error responses map to `protocol`, `TransportClosedError` maps to `transport`, and other exceptions use `unknown`. Classification never reads an error message, so the stderr tail carried by `TransportClosedError`, paths, task content, environment values, credentials, and protocol payloads remain Host-only. Request timeout classification is deferred until this provider configures or propagates a request timeout; the current SDK launch waits indefinitely for ordinary requests.
|
||||
|
||||
Successful results and local cancellation omit diagnostics. Startup and shutdown rejections use the same safe line in their Error message while retaining the original cause internally. A diagnostic-bearing child `aborted` result remains `aborted`; the one-shot Job adapter classifies it as failed, while diagnostic-free local cancellation remains killed.
|
||||
|
||||
|
||||
@@ -23,21 +23,20 @@ SDK 客户端返回自有子活动,而不是提示词结果。提供方读取
|
||||
| `completed` | `completed` | 无。 |
|
||||
| `max-tokens` | `max-tokens` | 无;结束原因本身已经可行动。 |
|
||||
| `aborted` | `aborted` | 只有闭集 `disposed` 原因会附加 `child-disposed`;父级本地取消绝不附加。 |
|
||||
| `blocked` | `error` | `child-blocked`。 |
|
||||
| `blocked` | `refusal` | 无;共享结束原因已经表示任务被拒绝。 |
|
||||
| `error` | `error` | `child-error`;不包含子失败消息或 code。 |
|
||||
| `interrupted` | `error` | `child-interrupted`。 |
|
||||
| 缺少 `turn/end` | `error` | `missing-terminal`。 |
|
||||
| 未知 variant | `error` | 固定 `unknown`,不复制原值。 |
|
||||
| 未知或不可达 variant | `error` | 固定 `child-unknown`,不复制原值。 |
|
||||
|
||||
## 失败诊断
|
||||
|
||||
首行遵循共享固定格式:
|
||||
|
||||
```text
|
||||
Subagent failure (provider: DSH SDK; stage: <stage>; category: <category>; child reason: <reason>)
|
||||
Subagent failure (provider: DSH SDK; stage: <stage>; category: <category>)
|
||||
```
|
||||
|
||||
不可用的可选字段会被省略,共享结果边界会把完整文本限制在 4096 个 UTF-8 字节以内。提供方从实际拥有失败的操作派生 `initialize`、`session-run`、`process` 或 `shutdown`。`SdkProtocolError` 与 JSON-RPC 错误响应映射为 `protocol`,`RequestTimeoutError` 映射为 `timeout`,`TransportClosedError` 映射为 `transport`(已发布子运行期间使用 `process` stage),其他异常使用 `unknown`。分类绝不读取错误消息,因此 `TransportClosedError` 携带的 stderr tail、路径、任务内容、环境值、凭证与协议 payload 都只留在 Host。
|
||||
共享结果边界会把完整文本限制在 4096 个 UTF-8 字节以内。提供方从实际拥有失败的操作派生 `initialize`、`session-run` 或 `shutdown`。`SdkProtocolError` 与 JSON-RPC 错误响应映射为 `protocol`,`TransportClosedError` 映射为 `transport`,其他异常使用 `unknown`。分类绝不读取错误消息,因此 `TransportClosedError` 携带的 stderr tail、路径、任务内容、环境值、凭证与协议 payload 都只留在 Host。请求超时分类会推迟到本提供方实际配置或传播 request timeout 时;当前 SDK launch 会无限等待普通请求。
|
||||
|
||||
成功结果与本地取消会省略诊断。启动和 shutdown 拒绝会在 Error 消息中使用同一安全行,同时把原始 cause 留在内部。带诊断的子 `aborted` 结果仍保持 `aborted`;一次性 Job adapter 会把它判为 failed,而不带诊断的本地取消仍是 killed。
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
DeepSeekHarness,
|
||||
type HarnessNotification,
|
||||
JsonRpcResponseError,
|
||||
RequestTimeoutError,
|
||||
SdkProtocolError,
|
||||
TransportClosedError,
|
||||
} from '@deepseek-ai/dsh-sdk-client'
|
||||
@@ -74,24 +73,21 @@ export const DEFAULT_DISPOSE_GRACE_MS = 3_000
|
||||
/** Default bound on the protocol `shutdown` exchange during dispose. */
|
||||
export const DEFAULT_SHUTDOWN_TIMEOUT_MS = 1_000
|
||||
|
||||
type SdkFailureStage = 'initialize' | 'session-run' | 'process' | 'shutdown'
|
||||
type SdkFailureStage = 'initialize' | 'session-run' | 'shutdown'
|
||||
|
||||
type SdkFailureCategory =
|
||||
| 'configuration'
|
||||
| 'protocol'
|
||||
| 'timeout'
|
||||
| 'transport'
|
||||
| 'child-error'
|
||||
| 'child-interrupted'
|
||||
| 'child-disposed'
|
||||
| 'child-blocked'
|
||||
| 'child-unknown'
|
||||
| 'missing-terminal'
|
||||
| 'unknown'
|
||||
|
||||
interface SdkFailureFacts {
|
||||
readonly stage: SdkFailureStage
|
||||
readonly category: SdkFailureCategory
|
||||
readonly childReason?: 'error' | 'interrupted' | 'disposed' | 'blocked' | 'missing' | 'unknown'
|
||||
}
|
||||
|
||||
/** Fixed safe failure text derived only from provider-owned structured facts. */
|
||||
@@ -101,7 +97,6 @@ function failureDiagnostic(facts: SdkFailureFacts): string {
|
||||
`stage: ${facts.stage}`,
|
||||
`category: ${facts.category}`,
|
||||
]
|
||||
if (facts.childReason !== undefined) fields.push(`child reason: ${facts.childReason}`)
|
||||
return `Subagent failure (${fields.join('; ')})`
|
||||
}
|
||||
|
||||
@@ -124,12 +119,10 @@ export function sdkConfigurationFailure(cause: unknown): Error {
|
||||
/** Classify one SDK rejection without reading its message or stderr tail. */
|
||||
function sdkFailure(error: unknown, stage: SdkFailureStage): SdkRunFailure {
|
||||
const facts: SdkFailureFacts = error instanceof TransportClosedError
|
||||
? { stage: stage === 'session-run' ? 'process' : stage, category: 'transport' }
|
||||
: error instanceof RequestTimeoutError
|
||||
? { stage, category: 'timeout' }
|
||||
: error instanceof SdkProtocolError || error instanceof JsonRpcResponseError
|
||||
? { stage, category: 'protocol' }
|
||||
: { stage, category: 'unknown' }
|
||||
? { stage, category: 'transport' }
|
||||
: error instanceof SdkProtocolError || error instanceof JsonRpcResponseError
|
||||
? { stage, category: 'protocol' }
|
||||
: { stage, category: 'unknown' }
|
||||
return new SdkRunFailure(facts, error)
|
||||
}
|
||||
|
||||
@@ -141,18 +134,16 @@ function childDiagnostic(reason: TurnEndReason | undefined): string | undefined
|
||||
return undefined
|
||||
case 'aborted':
|
||||
return reason.reason.kind === 'disposed'
|
||||
? failureDiagnostic({ stage: 'session-run', category: 'child-disposed', childReason: 'disposed' })
|
||||
? failureDiagnostic({ stage: 'session-run', category: 'child-disposed' })
|
||||
: undefined
|
||||
case 'blocked':
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'child-blocked', childReason: 'blocked' })
|
||||
return undefined
|
||||
case 'error':
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'child-error', childReason: 'error' })
|
||||
case 'interrupted':
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'child-interrupted', childReason: 'interrupted' })
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'child-error' })
|
||||
case undefined:
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'missing-terminal', childReason: 'missing' })
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'missing-terminal' })
|
||||
default:
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'unknown', childReason: 'unknown' })
|
||||
return failureDiagnostic({ stage: 'session-run', category: 'child-unknown' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +162,8 @@ export function sdkStopReason(reason: TurnEndReason | undefined): SubagentStopRe
|
||||
return 'max-tokens'
|
||||
case 'aborted':
|
||||
return 'aborted'
|
||||
case 'blocked':
|
||||
return 'refusal'
|
||||
// error / interrupted / disposed / a future merged variant /
|
||||
// no turn at all: the task did NOT finish cleanly — surface a generic
|
||||
// failure so the consumer maps it to an isError result.
|
||||
@@ -197,6 +190,24 @@ function reportFailure(spec: SdkRunSpec, error: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Map an SDK-owned failed-start aggregate into safe initialize/shutdown lines. */
|
||||
function sdkStartupFailure(spec: SdkRunSpec, error: unknown): Error {
|
||||
if (!(error instanceof AggregateError) || error.errors.length < 2) {
|
||||
reportFailure(spec, error)
|
||||
return sdkFailure(error, 'initialize')
|
||||
}
|
||||
const initializeError: unknown = error.errors[0]
|
||||
const cleanupError: unknown = error.errors[1]
|
||||
reportFailure(spec, initializeError)
|
||||
reportFailure(spec, cleanupError)
|
||||
const initializeFailure = sdkFailure(initializeError, 'initialize')
|
||||
const cleanupFailure = sdkFailure(cleanupError, 'shutdown')
|
||||
return new AggregateError(
|
||||
[initializeFailure, cleanupFailure],
|
||||
`${initializeFailure.message}; ${cleanupFailure.message}`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start and publish one SDK runtime child after its `initialize` handshake.
|
||||
* Child failures resolve through the run result; startup and shutdown failures
|
||||
@@ -256,23 +267,17 @@ export async function startSdkRun(request: SubagentStartRequest, spec: SdkRunSpe
|
||||
} catch (error: unknown) {
|
||||
request.signal.removeEventListener('abort', onAbort)
|
||||
const cancelledBeforeCleanup = flags.cancelled
|
||||
const failure = sdkFailure(error, 'initialize')
|
||||
if (!cancelledBeforeCleanup) reportFailure(spec, error)
|
||||
if (!cancelledBeforeCleanup) {
|
||||
throw sdkStartupFailure(spec, error)
|
||||
}
|
||||
try {
|
||||
await harness.close()
|
||||
} catch (cleanupError: unknown) {
|
||||
reportFailure(spec, cleanupError)
|
||||
const cleanupFailure = sdkFailure(cleanupError, 'shutdown')
|
||||
if (cancelledBeforeCleanup) throw cleanupFailure
|
||||
throw new AggregateError(
|
||||
[failure, cleanupFailure],
|
||||
`${failure.message}; ${cleanupFailure.message}`,
|
||||
)
|
||||
throw new AggregateError([cleanupFailure], cleanupFailure.message)
|
||||
}
|
||||
if (cancelledBeforeCleanup) {
|
||||
throw new Error('subagent request was aborted before the SDK child started')
|
||||
}
|
||||
throw failure
|
||||
throw new Error('subagent request was aborted before the SDK child started')
|
||||
}
|
||||
|
||||
const childSessionId = `session-${randomUUID().replaceAll('-', '')}`
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('SDK subagent cwd inheritance through a real cordis.yml', () => {
|
||||
expect(stderr).not.toContain('UNHANDLED')
|
||||
expect(toolResultText(events)).toBe(
|
||||
'Error: subagent run failed\n'
|
||||
+ 'Diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error; child reason: error)\n'
|
||||
+ 'Diagnostic: Subagent failure (provider: DSH SDK; stage: session-run; category: child-error)\n'
|
||||
+ 'Partial output before the run ended:\npartial child loader answer',
|
||||
)
|
||||
}, 135_000)
|
||||
|
||||
@@ -16,8 +16,8 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import {
|
||||
DeepSeekHarness,
|
||||
HarnessClient,
|
||||
HarnessSession,
|
||||
RequestTimeoutError,
|
||||
} from '@deepseek-ai/dsh-sdk-client'
|
||||
import * as sdk from '../src/index.ts'
|
||||
import {
|
||||
@@ -173,7 +173,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('error')
|
||||
expect(result.diagnostic).toBe(
|
||||
expectedFailure('stage: session-run; category: child-error; child reason: error'),
|
||||
expectedFailure('stage: session-run; category: child-error'),
|
||||
)
|
||||
expect(text(result.output)).toBe('partial answer')
|
||||
await run.dispose()
|
||||
@@ -210,30 +210,25 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
const run = await ctx.subagents.start('dsh-sdk', request())
|
||||
expect(await run.result).toMatchObject({
|
||||
stopReason: 'error',
|
||||
diagnostic: expectedFailure('stage: session-run; category: missing-terminal; child reason: missing'),
|
||||
diagnostic: expectedFailure('stage: session-run; category: missing-terminal'),
|
||||
})
|
||||
await run.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['interrupted', 'child-interrupted', 'interrupted'],
|
||||
['blocked', 'child-blocked', 'blocked'],
|
||||
] as const)('preserves the %s child terminal fact', async (reason, category, safeReason) => {
|
||||
const ctx = await setup({ FAKE_REASON_KIND: reason })
|
||||
it('maps a blocked child turn to the shared refusal stop reason', async () => {
|
||||
const ctx = await setup({ FAKE_REASON_KIND: 'blocked' })
|
||||
const run = await ctx.subagents.start('dsh-sdk', request())
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('error')
|
||||
expect(result.diagnostic).toBe(
|
||||
expectedFailure(`stage: session-run; category: ${category}; child reason: ${safeReason}`),
|
||||
)
|
||||
expect(result.stopReason).toBe('refusal')
|
||||
expect(result.diagnostic).toBeUndefined()
|
||||
await run.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('aggregates safe initialize and shutdown facts when startup rollback fails', async () => {
|
||||
const rawCleanup = 'shutdown leaked /private/path SECRET_TOKEN'
|
||||
const spy = vi.spyOn(DeepSeekHarness.prototype, 'close').mockImplementation(async function (this: DeepSeekHarness) {
|
||||
const spy = vi.spyOn(HarnessClient.prototype, 'close').mockImplementation(async function (this: HarnessClient) {
|
||||
spy.mockRestore()
|
||||
await this.close()
|
||||
throw new Error(rawCleanup)
|
||||
@@ -275,7 +270,8 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
})
|
||||
controller.abort()
|
||||
const error = await pending.catch((cause: unknown) => cause)
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(error).toBeInstanceOf(AggregateError)
|
||||
expect((error as AggregateError).errors).toHaveLength(1)
|
||||
expect((error as Error).message).toBe(
|
||||
`subagent-dsh-sdk: ${expectedFailure('stage: shutdown; category: unknown')}`,
|
||||
)
|
||||
@@ -291,7 +287,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('aborted')
|
||||
expect(result.diagnostic).toBe(
|
||||
expectedFailure('stage: session-run; category: child-disposed; child reason: disposed'),
|
||||
expectedFailure('stage: session-run; category: child-disposed'),
|
||||
)
|
||||
await run.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
@@ -314,7 +310,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('error')
|
||||
expect(result.diagnostic).toBe(
|
||||
expectedFailure('stage: session-run; category: unknown; child reason: unknown'),
|
||||
expectedFailure('stage: session-run; category: child-unknown'),
|
||||
)
|
||||
expect(result.diagnostic).not.toContain(rawReason)
|
||||
await run.dispose()
|
||||
@@ -395,34 +391,13 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
expect(result.stopReason).toBe('error')
|
||||
expect(result.output).toEqual([{ type: 'text', text: 'partial before transport exit' }])
|
||||
expect(result.diagnostic).toBe(
|
||||
expectedFailure('stage: process; category: transport'),
|
||||
expectedFailure('stage: session-run; category: transport'),
|
||||
)
|
||||
expect(result.diagnostic).not.toContain(stderr)
|
||||
await run.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('classifies a typed SDK request timeout without copying its message', async () => {
|
||||
const rawMessage = 'session path SECRET_TOKEN timed out'
|
||||
const spy = vi.spyOn(HarnessSession.prototype, 'run')
|
||||
.mockRejectedValue(new RequestTimeoutError(rawMessage))
|
||||
try {
|
||||
const ctx = await setup()
|
||||
const run = await ctx.subagents.start('dsh-sdk', request())
|
||||
const result = await run.result
|
||||
expect(result).toEqual({
|
||||
output: [],
|
||||
diagnostic: expectedFailure('stage: session-run; category: timeout'),
|
||||
stopReason: 'error',
|
||||
})
|
||||
expect(result.diagnostic).not.toContain(rawMessage)
|
||||
await run.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('uses a fixed unknown category for an untyped SDK exception', async () => {
|
||||
const rawMessage = 'unknown SDK failure at /private/path SECRET_TOKEN'
|
||||
const spy = vi.spyOn(HarnessSession.prototype, 'run')
|
||||
@@ -443,7 +418,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
})
|
||||
|
||||
it('keeps child diagnostics isolated across concurrent runs', async () => {
|
||||
const start = (reason: 'error' | 'interrupted') => startSdkRun(request(), {
|
||||
const start = (reason: 'error' | 'unknown-reason') => startSdkRun(request(), {
|
||||
command: process.execPath,
|
||||
args: [fakeRuntime],
|
||||
cwd: process.cwd(),
|
||||
@@ -454,13 +429,13 @@ describe('dsh-subagent-dsh-sdk provider', () => {
|
||||
disposeEofGraceMs: 200,
|
||||
disposeGraceMs: 200,
|
||||
})
|
||||
const [errored, interrupted] = await Promise.all([start('error'), start('interrupted')])
|
||||
const [errorResult, interruptedResult] = await Promise.all([errored.result, interrupted.result])
|
||||
const [errored, unknown] = await Promise.all([start('error'), start('unknown-reason')])
|
||||
const [errorResult, unknownResult] = await Promise.all([errored.result, unknown.result])
|
||||
expect(errorResult.diagnostic).toContain('category: child-error')
|
||||
expect(errorResult.diagnostic).not.toContain('child-interrupted')
|
||||
expect(interruptedResult.diagnostic).toContain('category: child-interrupted')
|
||||
expect(interruptedResult.diagnostic).not.toContain('child-error')
|
||||
await Promise.all([errored.dispose(), interrupted.dispose()])
|
||||
expect(errorResult.diagnostic).not.toContain('child-unknown')
|
||||
expect(unknownResult.diagnostic).toContain('category: child-unknown')
|
||||
expect(unknownResult.diagnostic).not.toContain('child-error')
|
||||
await Promise.all([errored.dispose(), unknown.dispose()])
|
||||
})
|
||||
|
||||
it('dispose cancels a hung child locally and reaps it', async () => {
|
||||
|
||||
Reference in New Issue
Block a user