test(sdk): snapshot suite over the SDK path; docs for the SDK stack

- examples/jsonrpc-agent gains its first snapshot suite (sdk.snapshot.ts):
  the real dsh-jsonrpc-agent runtime driven through the real dsh-sdk-client,
  keyless llm-replay behind a new cordis.snapshot.yml overlay; three recorded
  scenarios (text turn, bash tool, spawn subagent) pin the notification
  stream, the SDK turn result, and the persisted parent+child session logs.
- Bilingual READMEs for dsh-sdk-protocol / dsh-sdk-client / dsh-subagent-sdk;
  sdk/ and subagent/ group tables extended; dsh-jsonrpc README points at the
  extracted protocol package; Agent Note (en+zh) owns the decision.
- The proposed make-jsonrpc-directional note is updated for the transport's
  new home and its second (client) consumer.
- Model Experience sentence allowlist entries for the two client-side
  packages; module graph + config catalog regenerated; i18n pairings
  recorded. doc-sync passes 24/24.
This commit is contained in:
Tianyi Cui
2026-07-27 04:37:23 +08:00
parent 3e89a73c71
commit 34aabc4183
42 changed files with 1570 additions and 39 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: b0a5461b00c76e06a75d9ac4bd8cde9778a26ce8
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 856db82f6d47ee686216c8ef5f91048493a90801

View File

@@ -0,0 +1,47 @@
# Agent Note: TypeScript SDK client and the SDK subagent backend
Status: implemented
English | [中文](2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)
## Problem
The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-jsonrpc`, the [single-exe Agent Note](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)) had exactly one client: the Python SDK. TypeScript consumers wanting the same drive-a-harness-as-a-subprocess capability — repo tests, automation, and above all a subagent backend whose child is a *complete harness runtime* rather than a generic ACP agent — had nothing to import: the request/notification payload shapes existed only as anonymous object literals inside the server, and the transport class lived inside the server plugin package.
## Decision
Three packages, layered exactly like the existing Python stack, plus one seam registration:
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/sdk-protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-jsonrpc` (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 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/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`). Session-tree scoping from `subagent.started` lineage edges 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; `buildChildEnv` is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown reuses `disposeChildProcess` instead of hand-rolled terminate/kill.
- **`@deepseek-ai/dsh-subagent-sdk`** (`packages/subagent/subagent-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`). Its `provider`/`model` config feeds the child's `initialize`; `env` is where deployments pass the child's own key and `DSH_CORDIS_CONFIG`.
- **`dsh-subagent-subprocess` grows a third shared concern**: child cwd resolution (`assertUsableCwd`/`validateConfiguredCwd`/`resolveChildCwd`), extracted from `subagent-acp` when the SDK backend needed the identical config-override-else-parent-session-cwd policy, prefix-parameterized for diagnostics.
`dsh-jsonrpc` keeps serving unchanged (the wire is byte-identical); `dsh-jsonrpc-agent-pkg` (the Python runtime closure) gains the `dsh-sdk-protocol` dependency line.
## Testing
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-sdk` drives the same fake through the real provider. 100% per-file coverage on all three packages.
- **Keyless Loader composition** — `subagent-sdk/tests/loader-composition.e2e.ts` boots a test-only cordis.yml (`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/`) where the child is a REAL second harness runtime with its own cordis.yml; asserts the parent tool result and the child's own persisted transcript both carry the parent session's cwd. 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 first snapshot suite: the real `dsh-jsonrpc-agent` runtime driven through the real `dsh-sdk-client`, replaying recorded fixtures via `llm-replay` behind the new `cordis.snapshot.yml` overlay (passed explicitly through `DSH_CORDIS_CONFIG`; the jsonrpc bin performs no snapshot config swap of its own). Three scenarios — text turn, bash tool, spawn subagent — each pinning the normalized notification stream, the SDK turn result, and the persisted parent+child logs. This also closes the protocol-tier gap the single-exe note's Python-side snapshot left on the vitest side.
- **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.
## Alternatives considered
**Import wire types from `dsh-jsonrpc` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (interface/implementation/consumer as separate packages) already names this shape; the transport is genuinely two-sided.
**Have `subagent-sdk` speak raw JSON-RPC without the client SDK.** Duplicates the request/notification pairing, subscription fan-out, timeout, and teardown logic the SDK exists to own; the user's ask was explicitly a backend that *uses* the SDK, and the layering earns its keep by making the backend ~200 lines of policy over a reusable client.
**Fold the SDK backend into `subagent-acp` with a transport switch.** The two backends share the subprocess lifecycle but nothing about the wire (ACP SDK connection vs harness JSON-RPC), the child contract (any ACP agent vs a harness runtime), or the result extraction (`agent_message_chunk` accumulation vs session-event reading). A config discriminant would bury two protocols in one package; the shared parts are exactly what `subagent-subprocess` already holds, so that library grew instead.
**Give the TS SDK bundled-runtime resolution parity with Python.** Python's carrier resolution exists to ship wheels to users without Node. A TypeScript consumer definitionally has Node and (in-repo) the workspace; inventing a distribution story with no consumer violates the require-current-need rule. Deferred until a real npm-distribution consumer appears.
**Reuse `dsh-acp-snapshot`'s `runScenario` for the SDK snapshots.** That harness speaks ACP (`ClientSideConnection`, `InputStep` scripts). The SDK suite's whole point is to drive the *SDK client* as the entry surface; it reuses the normalize/refresh library layer (`normalizeSessionLog`, `refreshFixtureReplacements`, …) and leaves the ACP driver alone.
## Consequences
**Bought**: the SDK runtime protocol now has named, compiler-checked types shared by its server and both client SDKs; TypeScript consumers get the same subprocess-driving capability Python has, with typed errors and structured turn reasons; the subagent seam gains a harness-native out-of-process backend whose children are full peers (own config, persistence, tools) — the recursive-composition story the seam note anticipated; the jsonrpc example finally has snapshot coverage, through the SDK path itself.
**Paid**: a third package in the `sdk/` group and a fourth subagent backend to keep current; the SDK backend boots a complete plugin tree per child (heavier per-run than an ACP child; pooling remains future work, same as ACP); the wire still has no cancel method, so both the SDK's `RequestTimeoutError` and the backend's dispose settle locally while the server-side turn runs on until process teardown; fixtures for the snapshot suite were recorded against `deepseek-v4-flash` and re-record on model-behavior drift like every other recorded corpus.

View File

@@ -0,0 +1,47 @@
# Agent Note: TypeScript SDK 客户端与 SDK subagent 后端
Status: implemented
[English](2026-07-27-typescript-sdk-and-sdk-subagent-backend.md) | 中文
## Problem
stdio JSON-RPC 服务表面(`@deepseek-ai/dsh-jsonrpc`,见[单文件可执行 Agent Note](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)当时只有一个客户端Python SDK。想要同样"把 harness 作为子进程驱动"能力的 TypeScript 消费者——仓库测试、自动化,尤其是一个其子进程是*完整 harness 运行时*(而非通用 ACP 代理)的 subagent 后端——无物可导入:请求/通知载荷形状只以匿名对象字面量存在于服务器内部,传输类也躺在服务器插件包里。
## Decision
三个包,分层与既有 Python 栈完全一致,外加一个接缝注册:
- **`@deepseek-ai/dsh-sdk-protocol`**`packages/sdk/sdk-protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport``dsh-jsonrpc` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result``SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data``JsonRpcResponseError` 拒绝Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
- **`@deepseek-ai/dsh-sdk-client`**`packages/sdk/sdk-client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(生成、分帧、通知扇出、有类型的错误表面、经共享处置阶梯关闭至静止)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize``run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费者的发行问题);`env` 整体替换而非合并(凭据策略归调用方;`buildChildEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`Python 只暴露 `status`);拆除复用 `disposeChildProcess` 而不是手写 terminate/kill。
- **`@deepseek-ai/dsh-subagent-sdk`**`packages/subagent/subagent-sdk/`)—— 第二个进程外 `SubagentProvider`,以 `subagent-acp` 的同胞结构组织:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样的经 `onError` 汇把结果压平为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未跑回合就尘埃落定的子进程,都是 `error`)。其 `provider`/`model` 配置喂给子进程的 `initialize``env` 是部署传入子进程自有密钥与 `DSH_CORDIS_CONFIG` 的地方。
- **`dsh-subagent-subprocess` 增长出第三个共享关注点**:子进程 cwd 解析(`assertUsableCwd`/`validateConfiguredCwd`/`resolveChildCwd`),在 SDK 后端需要与 `subagent-acp` 完全相同的"配置覆盖、否则父会话 cwd"策略时从后者提取,以前缀参数化诊断信息。
`dsh-jsonrpc` 的服务不变(线上字节完全一致);`dsh-jsonrpc-agent-pkg`Python 运行时闭包)增加 `dsh-sdk-protocol` 一行依赖。
## Testing
四层,依[测试政策](../../../../docs/testing.md)
- **免密钥单元** —— `sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-sdk` 经真实 provider 驱动同一伪运行时。三个包全部 100% 逐文件覆盖。
- **免密钥 Loader 组合** —— `subagent-sdk/tests/loader-composition.e2e.ts` 启动仅测试用 cordis.yml`examples/jsonrpc-agent/tests/fixtures/subagent/subagent-sdk/`),其中子进程是真实的第二个 harness 运行时、带自己的 cordis.yml断言父工具结果与子进程自己持久化的转录都携带父会话 cwd。子启动经 `resolveExampleLaunch` 解析src/lib 两种模式都成立。
- **免密钥快照** —— `examples/jsonrpc-agent/tests/sdk.snapshot.ts` 是 jsonrpc 示例的第一个快照套件:真实 `dsh-jsonrpc-agent` 运行时经真实 `dsh-sdk-client` 驱动,在新的 `cordis.snapshot.yml` 覆盖层后经 `llm-replay` 回放已录制夹具(经 `DSH_CORDIS_CONFIG` 显式传入jsonrpc bin 自身不做快照配置切换。三个场景——文本回合、bash 工具、spawn 子代理——各自钉住规范化通知流、SDK 回合结果与持久化的父+子日志。这也补上了单文件可执行 Note 的 Python 侧快照在 vitest 侧留下的协议层缺口。
- **带密钥 e2e** —— 快照套件的 `DSH_SNAPSHOT=record` 模式即真实 API 路径(已提交夹具由它产出);组合 e2e 设计上无需密钥。
## Alternatives considered
**从 `dsh-jsonrpc` 导入线类型而不是提取协议包。** 会让每个 SDK 消费者(包括绝不能提供 JSON-RPC 服务的 `subagent-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力接缝规则(接口/实现/消费者三包分立)已经点名了这种形态;这个传输是货真价实的双边物。
**让 `subagent-sdk` 直说裸 JSON-RPC、绕开客户端 SDK。** 会复制 SDK 存在意义所在的请求/通知配对、订阅扇出、超时与拆除逻辑;用户的要求明确是一个*使用* SDK 的后端,分层的回报是后端成为可复用客户端之上约 200 行的纯策略。
**把 SDK 后端折进 `subagent-acp`、用传输开关区分。** 两个后端共享子进程生命周期但线协议ACP SDK 连接 vs harness JSON-RPC、子进程契约任意 ACP 代理 vs harness 运行时)、结果提取(`agent_message_chunk` 累积 vs 会话事件读取)毫无共享。配置判别子会把两个协议埋进一个包;共享部分恰好就是 `subagent-subprocess` 已持有的,于是让那个库生长。
**给 TS SDK 与 Python 对等的捆绑运行时解析。** Python 的载体解析是为了给没有 Node 的用户发 wheel。TypeScript 消费者定义上就有 Node 且(仓库内)有工作区;为不存在的消费者发明发行故事违反"要求当前需求"规则。推迟到真实 npm 发行消费者出现。
**复用 `dsh-acp-snapshot` 的 `runScenario` 做 SDK 快照。** 那个 harness 说 ACP`ClientSideConnection``InputStep` 脚本。SDK 套件的全部意义就是以 *SDK 客户端*为入口表面;它复用 normalize/refresh 库层(`normalizeSessionLog``refreshFixtureReplacements`……),不动 ACP 驱动器。
## Consequences
**买到**SDK 运行时协议现在拥有服务器与两个客户端 SDK 共享的、编译器校验的具名类型TypeScript 消费者获得与 Python 相同的子进程驱动能力且带类型化错误与结构化回合原因subagent 接缝获得一个 harness 原生的进程外后端,其子进程是完整对等体(自有配置、持久化、工具)——正是接缝 Note 预期的递归组合故事jsonrpc 示例终于有了快照覆盖,而且走的就是 SDK 路径本身。
**付出**`sdk/` 组多了第三个包、subagent 多了第四个要保持最新的后端SDK 后端每个子进程启动完整插件树(单次成本高于 ACP 子进程;池化与 ACP 一样留作未来工作线上仍无取消方法SDK 的 `RequestTimeoutError` 与后端的 dispose 都只在本地定格、服务器侧回合继续跑到进程拆除为止;快照夹具录制于 `deepseek-v4-flash`,与其他录制语料一样随模型行为漂移而重录。

View File

@@ -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
2026-07-19-make-jsonrpc-directional.md: 74de3c960a415a9a2601e57ec75f244ca753193d
2026-07-19-make-jsonrpc-directional.zh.md: 76228ba56cfbd4fb86f39d0d0873d49edb13309b
2026-07-19-make-jsonrpc-directional.md: 910b4988aca34dec499b2e34cb2a42042c81b0cb
2026-07-19-make-jsonrpc-directional.zh.md: acc1b433e14c65da2ed37dff7a5fe181ebee36ad

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-19-make-jsonrpc-directional.zh.md)
## Problem
The JSON-RPC bridge models both endpoints as symmetric peers although the shipped protocol is directional. The TypeScript server accepts requests and emits responses or notifications, but its transport also implements unused outbound requests and inbound notification dispatch. The Python SDK sends requests and receives responses or notifications, but it also queues unused inbound server requests and exposes response helpers.
The JSON-RPC bridge models both endpoints as symmetric peers although the shipped protocol is directional. The shared transport (now `dsh-sdk-protocol`, used by the server and by the TypeScript SDK client, which exercises the outbound-request/inbound-notification direction) still implements two halves no endpoint uses: server-originated requests and client-originated notifications. The Python SDK sends requests and receives responses or notifications, but it also queues unused inbound server requests and exposes response helpers.
`session/prompt` also reports one settled turn through two protocol shapes. The server emits `session.finished` and then returns the constant `{ accepted: true }`; the Python SDK discards that response and waits for the notification to recover the status. Because the response is written only after the handler returns, the notification necessarily precedes the constant response on the same stream.
@@ -14,17 +14,17 @@ The unused halves add pending-request maps, generated IDs, request queues, close
## Proposal
Specialize each endpoint to its actual role. The TypeScript transport will retain inbound requests, outbound responses, and outbound notifications. The Python client will retain outbound requests and inbound responses or notifications. Delete the opposite-direction request machinery from each side.
Specialize each endpoint to its actual role. The server keeps inbound requests, outbound responses, and outbound notifications; the TypeScript and Python clients keep outbound requests and inbound responses or notifications. Delete the direction no endpoint uses — server-originated requests and client-originated notifications.
Return the settled outcome directly from `session/prompt` as `{ status, reason }` after `agent.whenIdle()`. Delete `session.finished`, the constant acceptance response, and the Python post-response completion loop. `session.event` and subagent notifications still stream before the response, and durable session events remain the source for final-response reconstruction.
## Implementation plan
1. In `packages/ui/jsonrpc/src/server.ts`, replace `SessionPromptResult.accepted` with `status: 'ok' | 'error' | 'aborted'` and the captured `TurnEndReason`. `HarnessSdkServer.prompt()` will return `completed` as `ok`, `aborted` as `aborted`, and every other current or merge-extensible reason as `error`; reaching idle without a `turn/end` remains an invariant error. Remove only `session.finished`, leaving `session.event`, `subagent.started`, and `subagent.finished` unchanged.
2. In `packages/ui/jsonrpc/src/transport.ts`, replace `JsonRpcTransportPeer` with a server-side notification surface and retain `onRequest()`, `notify()`, `start()`, `flush()`, and `close()`. Remove generated request IDs, the pending-response map, outbound `request()`, inbound response and notification dispatch, and close-time pending-request rejection. Incoming response- and notification-shaped frames will be ignored, while request result, method-not-found, and handler-error responses retain their current behavior and remain ordered after notifications emitted by the awaited handler.
2. In `packages/sdk/sdk-protocol/src/transport.ts`, narrow the shared class to the directions with consumers — inbound requests/outbound responses (the server) and outbound requests/inbound responses plus inbound notifications (the TypeScript SDK client) — removing only server-originated `request()` use and client-originated notification dispatch, or split the class into a server-side and client-side transport. Request result, method-not-found, and handler-error responses retain their current behavior and remain ordered after notifications emitted by the awaited handler.
3. In `python/sdk/src/deepseek_harness/client.py`, `models.py`, and `__init__.py`, remove `IncomingRequest`, `_requests`, `notify()`, `next_request()`, `respond()`, and `respond_error()`. Add a public validated `SessionPromptResponse` carrying status and reason, return it from `session_prompt()`, and keep an explicit reader guard that ignores unexpected server-request frames instead of allowing them to match a response waiter.
4. In `python/sdk/src/deepseek_harness/api.py`, build `TurnResult.status` and a new `TurnResult.reason` from `SessionPromptResponse`, then delete the `session.finished` branch and second completion loop. Keep the subscription open during the request and preserve `_request_raw()`'s final notification drain so the last `turn/end` event and any subagent notification written before the response are collected before `Session.run()` reconstructs the final assistant message.
5. Replace the symmetric transport-pair cases in `packages/ui/jsonrpc/tests/transport.spec.ts` with raw client-input/server-output coverage, and update `server.spec.ts`, `plugin-apply.spec.ts`, and `built-scope-carrier.e2e.ts` for direct outcomes, ordering, overlap, shutdown, and the narrowed fake. Update `python/sdk/tests/test_client.py` for response-based settlement, unexpected-request-frame handling, callback and concurrency behavior, and the removed public helpers. Update the JSON-RPC and bilingual Python SDK READMEs, export JSDoc and declarations, `scripts/smoke-python-runtime.py`, and the Python single-executable snapshot.
5. Replace the symmetric transport-pair cases in `packages/sdk/sdk-protocol/tests/transport.spec.ts` with per-direction coverage, and update `server.spec.ts`, `plugin-apply.spec.ts`, and `built-scope-carrier.e2e.ts` for direct outcomes, ordering, overlap, shutdown, and the narrowed fake; update the TypeScript SDK client (`packages/sdk/sdk-client`) and its suites for response-based settlement. Update `python/sdk/tests/test_client.py` for response-based settlement, unexpected-request-frame handling, callback and concurrency behavior, and the removed public helpers. Update the JSON-RPC and bilingual Python SDK READMEs, export JSDoc and declarations, `scripts/smoke-python-runtime.py`, and the Python single-executable snapshot.
## Alternatives considered

View File

@@ -6,7 +6,7 @@ Status: proposed
## 问题
JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协议具有固定方向。TypeScript 服务端接收请求并发出响应或通知,其传输层却还实现了未使用的出站请求和入站通知分发。Python SDK 发送请求并接收响应或通知,却还会把未使用的服务端入站请求放入队列,并公开响应辅助方法。
JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协议具有固定方向。共享传输层(现为 `dsh-sdk-protocol`,由服务端与 TypeScript SDK 客户端共用,后者行使出站请求/入站通知方向)仍实现着没有任何端点使用的两个半边:服务端发起的请求与客户端发起的通知。Python SDK 发送请求并接收响应或通知,却还会把未使用的服务端入站请求放入队列,并公开响应辅助方法。
`session/prompt` 还会用两种协议结构报告同一个已结束轮次。服务端先发出 `session.finished`,再返回常量 `{ accepted: true }`Python SDK 丢弃该响应,转而等待通知以取得状态。响应只有在处理函数返回后才会写入,因此在同一条有序流上,通知必然先于这个常量响应。
@@ -14,17 +14,17 @@ JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协
## 提案
按实际角色收窄两个端点。TypeScript 传输层只保留入站请求、出站响应和出站通知Python 客户端保留出站请求以及入站响应或通知。删除两侧与实际方向相反的请求机制
按实际角色收窄两个端点。服务端保留入站请求、出站响应和出站通知TypeScript 与 Python 客户端保留出站请求以及入站响应或通知。删除没有任何端点使用的方向——服务端发起的请求与客户端发起的通知
`agent.whenIdle()` 完成后,由 `session/prompt` 直接返回 `{ status, reason }` 作为轮次结果。删除 `session.finished`、常量接纳响应以及 Python 中响应后的完成等待循环。`session.event` 与 subagent 通知仍在响应前流式发出,持久会话事件仍是最终响应重建的真源。
## 实施计划
1.`packages/ui/jsonrpc/src/server.ts` 中,用 `status: 'ok' | 'error' | 'aborted'` 和捕获的 `TurnEndReason` 替换 `SessionPromptResult.accepted``HarnessSdkServer.prompt()``completed` 映射为 `ok`,把 `aborted` 映射为 `aborted`,把其他当前或可合并扩展的原因映射为 `error`;进入空闲状态却没有 `turn/end` 仍视为不变量错误。只删除 `session.finished`,保持 `session.event``subagent.started``subagent.finished` 不变。
2.`packages/ui/jsonrpc/src/transport.ts` 中,用服务端通知接口替换 `JsonRpcTransportPeer`,并保留 `onRequest()``notify()``start()``flush()``close()`。删除生成的请求 ID、待处理响应表、出站 `request()`、入站响应与通知分发,以及关闭时对待处理请求的拒绝逻辑。入站响应结构和通知结构将被忽略;请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。
2.`packages/sdk/sdk-protocol/src/transport.ts` 中,把共享类收窄到有消费者的方向——入站请求/出站响应(服务端)与出站请求/入站响应加入站通知TypeScript SDK 客户端)——只删除服务端发起的 `request()` 用法与客户端发起的通知分发,或把该类拆分为服务端与客户端两个传输。请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。
3.`python/sdk/src/deepseek_harness/client.py``models.py``__init__.py` 中,删除 `IncomingRequest``_requests``notify()``next_request()``respond()``respond_error()`。新增公开且经过校验的 `SessionPromptResponse` 来携带状态与原因,由 `session_prompt()` 返回该对象,并保留明确的读取保护:忽略意外的服务端请求帧,避免它们命中响应等待器。
4.`python/sdk/src/deepseek_harness/api.py` 中,根据 `SessionPromptResponse` 构造 `TurnResult.status` 和新增的 `TurnResult.reason`,再删除 `session.finished` 分支与第二个完成循环。请求期间保持订阅打开,并保留 `_request_raw()` 最后的通知排空步骤,确保写在响应前的最后一条 `turn/end` 事件与任何 subagent 通知,都会在 `Session.run()` 重建最终助手消息之前被收集。
5.原始客户端输入与服务端输出覆盖替换 `packages/ui/jsonrpc/tests/transport.spec.ts` 中的对称传输对用例,并更新 `server.spec.ts``plugin-apply.spec.ts``built-scope-carrier.e2e.ts`,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现。更新 `python/sdk/tests/test_client.py`,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、`scripts/smoke-python-runtime.py` 和 Python 单可执行文件快照。
5.按方向的覆盖替换 `packages/sdk/sdk-protocol/tests/transport.spec.ts` 中的对称传输对用例,并更新 `server.spec.ts``plugin-apply.spec.ts``built-scope-carrier.e2e.ts`,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现;同步更新 TypeScript SDK 客户端(`packages/sdk/sdk-client`)及其套件以采用基于响应的结束流程。更新 `python/sdk/tests/test_client.py`,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、`scripts/smoke-python-runtime.py` 和 Python 单可执行文件快照。
## 备选方案