Make JSON-RPC completion and transport directional
让 JSON-RPC 完成结果与传输方向单一化
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 r
English
Problem
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.
The unused halves add pending-request maps, generated IDs, request queues, close-time rejection paths, response helpers, and a second completion waiter without serving a production caller.
Proposal
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
- In
packages/sdk/server/src/server.ts, replaceSessionPromptResult.acceptedwithstatus: 'ok' | 'error' | 'aborted'and the capturedTurnEndReason.HarnessSdkJsonRpcServer.prompt()will returncompletedasok,abortedasaborted, and every other current or merge-extensible reason aserror; reaching idle without aturn/endremains an invariant error. Remove onlysession.finished, leavingsession.event,subagent.started, andsubagent.finishedunchanged. - In
packages/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-originatedrequest()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. - In
python/sdk/src/deepseek_harness/client.py,models.py, and__init__.py, removeIncomingRequest,_requests,notify(),next_request(),respond(), andrespond_error(). Add a public validatedSessionPromptResponsecarrying status and reason, return it fromsession_prompt(), and keep an explicit reader guard that ignores unexpected server-request frames instead of allowing them to match a response waiter. - In
python/sdk/src/deepseek_harness/api.py, buildTurnResult.statusand a newTurnResult.reasonfromSessionPromptResponse, then delete thesession.finishedbranch and second completion loop. Keep the subscription open during the request and preserve_request_raw()'s final notification drain so the lastturn/endevent and any subagent notification written before the response are collected beforeSession.run()reconstructs the final assistant message. - Replace the symmetric transport-pair cases in
packages/sdk/protocol/tests/transport.spec.tswith per-direction coverage, and updateserver.spec.ts,plugin-apply.spec.ts, andbuilt-scope-carrier.e2e.tsfor direct outcomes, ordering, overlap, shutdown, and the narrowed fake; update the TypeScript SDK client (packages/sdk/client) and its suites for response-based settlement. Updatepython/sdk/tests/test_client.pyfor 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
Keep a generic symmetric JSON-RPC peer for future methods. Server-initiated requests may eventually support interactive permissions, but no typed method or production consumer exists. The pre-release protocol can add the smallest required direction when that feature is designed instead of carrying an unexercised peer today.
Keep session.finished for streaming clients. Turn settlement is not incremental data: the request response already marks the same boundary and follows all earlier notifications on the ordered stream. A second terminal notification creates two representations that clients must reconcile.
Acceptance criteria
- The TypeScript endpoint cannot originate requests or consume notifications.
- The Python endpoint cannot originate notifications or consume server requests.
session/promptreturns the authoritativeok,error, orabortedoutcome and reason after turn settlement.- Session events and subagent lifecycle notifications emitted during the turn arrive before the response.
- Same-session overlap rejection, framing, multibyte input, handler errors, flush, shutdown ordering, and final-response reconstruction retain their behavior.
- TypeScript bridge tests, Python SDK tests, built JSON-RPC coverage, snapshots, and generated API documentation pass.
Risks
This deliberately narrows the pre-release wire protocol. Raw clients listening only for session.finished, or embedders using the unused symmetric transport methods, must move to the prompt response. A future server-initiated request requires a new typed protocol addition rather than reusing generic dormant machinery.
中文
问题
JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协议具有固定方向。共享传输层(现为 dsh-sdk-protocol,由服务端与 TypeScript SDK 客户端共用,后者行使出站请求/入站通知方向)仍实现着没有任何端点使用的两个半边:服务端发起的请求与客户端发起的通知。Python SDK 发送请求并接收响应或通知,却还会把来自服务端、但未使用的入站请求放入队列,并公开响应辅助方法。
session/prompt 还会用两种协议结构报告同一个已结束轮次。服务端先发出 session.finished,再返回常量 { accepted: true };Python SDK 丢弃该响应,转而等待通知以取得状态。响应只有在处理函数返回后才会写入,因此在同一条有序流上,通知必然先于这个常量响应。
这些未使用的双向能力引入了待处理请求表、生成 ID、请求队列、关闭时的拒绝路径、响应辅助方法和第二套完成等待逻辑,却没有任何生产调用方使用。
提案
按实际角色收窄两个端点。服务端保留入站请求、出站响应和出站通知;TypeScript 与 Python 客户端保留出站请求以及入站响应或通知。删除没有任何端点使用的方向——服务端发起的请求与客户端发起的通知。
在 agent.whenIdle() 完成后,由 session/prompt 直接返回 { status, reason } 作为轮次结果。删除 session.finished、常量接纳响应以及 Python 中响应后的完成等待循环。session.event 与 subagent 通知仍在响应前流式发出,持久化会话事件仍是最终响应重建的真源。
实施计划
- 在
packages/sdk/server/src/server.ts中,用status: 'ok' | 'error' | 'aborted'和捕获的TurnEndReason替换SessionPromptResult.accepted。HarnessSdkJsonRpcServer.prompt()把completed映射为ok,把aborted映射为aborted,把其他当前已有或可通过声明合并扩展的原因映射为error;进入空闲状态却没有turn/end仍视为不变量错误。只删除session.finished,保持session.event、subagent.started和subagent.finished不变。 - 在
packages/sdk/protocol/src/transport.ts中,把共享类收窄到有消费者的方向——入站请求/出站响应(服务端)与出站请求/入站响应及入站通知(TypeScript SDK 客户端)——只删除服务端发起的request()用法与客户端发起的通知分发,或把该类拆分为服务端与客户端两个传输。请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。 - 在
python/sdk/src/deepseek_harness/client.py、models.py和__init__.py中,删除IncomingRequest、_requests、notify()、next_request()、respond()和respond_error()。新增公开且经过校验的SessionPromptResponse来携带状态与原因,由session_prompt()返回该对象,并保留明确的读取保护:忽略意外的服务端请求帧,避免它们命中响应等待器。 - 在
python/sdk/src/deepseek_harness/api.py中,根据SessionPromptResponse构造TurnResult.status和新增的TurnResult.reason,再删除session.finished分支与第二个完成循环。请求期间保持订阅打开,并保留_request_raw()最后的通知排空步骤,确保写在响应前的最后一条turn/end事件与任何 subagent 通知,都会在Session.run()重建最终助手消息之前被收集。 - 用按方向的覆盖替换
packages/sdk/protocol/tests/transport.spec.ts中的对称传输对用例,并更新server.spec.ts、plugin-apply.spec.ts和built-scope-carrier.e2e.ts,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现;同步更新 TypeScript SDK 客户端(packages/sdk/client)及其套件以采用基于响应的结束流程。更新python/sdk/tests/test_client.py,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、scripts/smoke-python-runtime.py和 Python 单可执行文件快照。
备选方案
为未来方法保留通用的对称 JSON-RPC 对等端。 服务端发起的请求将来可能用于交互式权限,但当前没有类型化方法或生产消费方。该功能完成设计后,预发布协议可以增加所需的最小方向,无需提前保留未使用的对等端能力。
为流式客户端保留 session.finished。 轮次结束不是增量数据:请求响应已经标识同一个边界,并且在有序流中位于先前所有通知之后。第二条终止通知会产生两种结果表示,迫使客户端进行协调。
验收标准
- TypeScript 端点无法发起请求,也不消费通知。
- Python 端点无法发起通知,也不消费服务端请求。
- 轮次结束后,
session/prompt返回权威的ok、error或aborted状态及其原因。 - 轮次中发出的会话事件与 subagent 生命周期通知都先于响应到达。
- 同一会话的重叠拒绝、分帧、多字节输入、处理器错误、flush、关闭顺序与最终响应重建保持原有行为。
- TypeScript 桥接测试、Python SDK 测试、构建后 JSON-RPC 覆盖、快照和生成的 API 文档全部通过。
风险
本提案会刻意收窄预发布协议格式。仅监听 session.finished 的原始客户端,以及使用未使用对称传输方法的嵌入方,都必须改为读取请求响应。未来若需要服务端发起请求,应新增类型化协议,而不是复用休眠的通用机制。