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:
6
packages/sdk/sdk-client/README.i18n.yaml
Normal file
6
packages/sdk/sdk-client/README.i18n.yaml
Normal 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
|
||||
README.md: 82e344014ac01120986ee2b4e07ac21192506ff7
|
||||
README.zh.md: 6e67f57ab92a75be3600cbdd0ce6bf21832fd2c8
|
||||
50
packages/sdk/sdk-client/README.md
Normal file
50
packages/sdk/sdk-client/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# @deepseek-ai/dsh-sdk-client
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The TypeScript client SDK for driving a DeepSeek Harness runtime as a subprocess over stdio JSON-RPC — the design twin of the [Python SDK](../../../python/README.md) (`deepseek-harness`), sharing the same runtime peer, protocol, and layering: `DeepSeekHarness` is the high-level turns API, `HarnessClient` the lower-level protocol client. A pure library: it registers nothing on a Cordis context; the runtime process it spawns is a complete harness whose composition its own `cordis.yml` decides.
|
||||
|
||||
Unlike the Python SDK, the launch spec is fully explicit (`command`/`args`): this package is for repo-adjacent TypeScript consumers — the [`dsh-subagent-sdk`](../../subagent/subagent-sdk/README.md) backend, tests, automation — which know which runtime they are launching. Bundled-runtime resolution (finding a packaged executable) remains the Python distribution's concern.
|
||||
|
||||
## DeepSeekHarness
|
||||
|
||||
```ts
|
||||
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
|
||||
|
||||
await using harness = new DeepSeekHarness({
|
||||
launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-v4-flash',
|
||||
})
|
||||
const result = await harness.run('say hi')
|
||||
console.log(result.status, 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 (cwd + provider/model route); a failed handshake closes the runtime and resets, so a later call may retry. `session(id?)` opens a named or fresh session handle; `run(input, { sessionId?, onNotification? })` sends one prompt turn and settles when the paired `session.finished` arrives, returning a `TurnResult`: `status` (`ok`/`error` as the deployment maps it), the structured `reason` (`TurnEndReason`), `finalResponse` (last assistant message text), plus every `session.event` envelope and raw notification observed for that session tree, in wire order. Model-level failure is a `status: 'error'` result, never a rejection; rejections mean transport loss, timeout, or protocol violation.
|
||||
|
||||
## HarnessClient
|
||||
|
||||
The protocol client under the turns API: explicit `start()`/`initialize()`/`prompt()`/`request()`/`close()`, plus notification subscriptions. `subscribe(filter?)` returns a `NotificationSubscription` (awaitable `next()`, non-blocking `tryNext()`, async iteration); `subscribeSessionTree(id)` scopes to one session and the descendants discovered from `subagent.started` lineage edges — the runtime notifies for every session in its context, and scoping is client-side, exactly like the Python SDK. Error surfaces are typed: `JsonRpcResponseError` (wire error response, code/data preserved), `RequestTimeoutError` (a configured bound elapsed; there is no wire-level cancel, so the request keeps running server-side until close), `SdkProtocolError` (a response outside the documented protocol), `TransportClosedError` (the runtime is gone — message carries the exit code and a bounded stderr tail).
|
||||
|
||||
`close()` requests protocol `shutdown` (bounded by `shutdownTimeoutMs`, default 1000 ms), then walks the shared stdin-EOF → SIGTERM → SIGKILL [dispose ladder](../../subagent/subagent-subprocess/README.md) (`disposeEofGraceMs` default 6000, `disposeGraceMs` default 3000) until the process has actually exited. It is idempotent, and a closed client refuses reuse.
|
||||
|
||||
`HarnessClientOptions.env` replaces the child environment entirely when given (`undefined` inherits the parent's); callers own credential policy — `buildChildEnv` from `dsh-subagent-subprocess` is the scrub-then-inject helper for isolation-minded launches.
|
||||
|
||||
## Testing
|
||||
|
||||
Keyless unit tests drive a scripted fake runtime subprocess (`tests/fake-runtime.ts`, protocol-only, env-scripted) over real stdio: turn loop, session-tree scoping, timeout/death/malformed-response surfaces, and the dispose ladder. The [SDK snapshot suite](../../../examples/jsonrpc-agent/tests/sdk.snapshot.ts) drives the real `dsh-jsonrpc-agent` runtime through this client keylessly via `llm-replay`, pinning the notification stream, the turn result, and the persisted logs; `DSH_SNAPSHOT=record` re-records against the live API.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as this is a client-process library; the model runs in the spawned runtime, whose experience is owned by the plugins its `cordis.yml` composes.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No bundled-runtime resolution** — callers name the runtime executable explicitly; packaged-executable discovery stays Python-side until a TypeScript distribution consumer exists.
|
||||
- **No mid-turn cancel** — the wire has no prompt-cancel method; abandoning a turn means closing the runtime (see the protocol's [Known Limitations](../sdk-protocol/README.md)).
|
||||
- **One in-flight prompt per session** — a server-side rule this client surfaces as a `JsonRpcResponseError`; independent sessions run concurrently on one runtime.
|
||||
- **Client→server notifications and server→client requests are unimplemented** on both wire ends; the transport carries them for future approval flows.
|
||||
50
packages/sdk/sdk-client/README.zh.md
Normal file
50
packages/sdk/sdk-client/README.zh.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# @deepseek-ai/dsh-sdk-client
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
以子进程方式驱动 DeepSeek Harness 运行时、走 stdio JSON-RPC 的 TypeScript 客户端 SDK——[Python SDK](../../../python/README.md)(`deepseek-harness`)的设计孪生,共享同一个运行时对端、协议与分层:`DeepSeekHarness` 是高层回合 API,`HarnessClient` 是低层协议客户端。纯库:不在任何 Cordis 上下文注册;它所生成的运行时进程是一个完整 harness,其组成由自己的 `cordis.yml` 决定。
|
||||
|
||||
与 Python SDK 不同,启动规格完全显式(`command`/`args`):本包面向仓库近旁的 TypeScript 消费者——[`dsh-subagent-sdk`](../../subagent/subagent-sdk/README.md) 后端、测试、自动化——它们知道自己要启动哪个运行时。捆绑运行时解析(寻找打包可执行文件)仍归 Python 发行版负责。
|
||||
|
||||
## DeepSeekHarness
|
||||
|
||||
```ts
|
||||
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
|
||||
|
||||
await using harness = new DeepSeekHarness({
|
||||
launch: { command: 'node', args: ['lib/bin.js', 'cordis.yml'] },
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-v4-flash',
|
||||
})
|
||||
const result = await harness.run('say hi')
|
||||
console.log(result.status, result.finalResponse)
|
||||
```
|
||||
|
||||
子进程在首次使用时惰性启动,并在多次 `run()` 之间持续归实例所有;必须 `close()`(或 `await using`),子进程才总能被收割。`start()` 记忆化 `initialize` 握手(cwd + provider/model 路由);握手失败会关闭运行时并复位,后续调用可以重试。`session(id?)` 打开具名或全新的会话句柄;`run(input, { sessionId?, onNotification? })` 发送一个 prompt 回合,在配对的 `session.finished` 到达时尘埃落定,返回 `TurnResult`:`status`(按部署映射的 `ok`/`error`)、结构化 `reason`(`TurnEndReason`)、`finalResponse`(最后一条助手消息文本),以及该会话树内按线序观察到的全部 `session.event` 封套与原始通知。模型层失败是 `status: 'error'` 的结果,绝不是拒绝;拒绝意味着传输丢失、超时或协议违例。
|
||||
|
||||
## HarnessClient
|
||||
|
||||
回合 API 之下的协议客户端:显式 `start()`/`initialize()`/`prompt()`/`request()`/`close()`,外加通知订阅。`subscribe(filter?)` 返回 `NotificationSubscription`(可等待的 `next()`、非阻塞 `tryNext()`、异步迭代);`subscribeSessionTree(id)` 把范围限定到一个会话及从 `subagent.started` 血缘边发现的后代——运行时对上下文内每个会话都发通知,范围限定在客户端完成,与 Python SDK 完全一致。错误表面有类型:`JsonRpcResponseError`(线上错误响应,保留 code/data)、`RequestTimeoutError`(配置的时限已到;线上没有取消方法,请求在服务端继续运行直到 close)、`SdkProtocolError`(响应超出文档化协议)、`TransportClosedError`(运行时已消失——消息携带退出码与有界 stderr 尾部)。
|
||||
|
||||
`close()` 先请求协议 `shutdown`(受 `shutdownTimeoutMs` 约束,默认 1000 毫秒),然后走共享的 stdin-EOF → SIGTERM → SIGKILL [处置阶梯](../../subagent/subagent-subprocess/README.md)(`disposeEofGraceMs` 默认 6000,`disposeGraceMs` 默认 3000)直到进程真正退出。幂等,已关闭的客户端拒绝复用。
|
||||
|
||||
`HarnessClientOptions.env` 给定时整体替换子环境(`undefined` 原样继承父环境);凭据策略归调用方——`dsh-subagent-subprocess` 的 `buildChildEnv` 是面向隔离启动的先擦除后注入助手。
|
||||
|
||||
## 测试
|
||||
|
||||
免密钥单元测试通过真实 stdio 驱动一个脚本化伪运行时子进程(`tests/fake-runtime.ts`,纯协议、环境变量脚本化):回合循环、会话树范围限定、超时/死亡/畸形响应表面、处置阶梯。[SDK 快照套件](../../../examples/jsonrpc-agent/tests/sdk.snapshot.ts)经由 `llm-replay` 免密钥地通过本客户端驱动真实 `dsh-jsonrpc-agent` 运行时,钉住通知流、回合结果与持久化日志;`DSH_SNAPSHOT=record` 对真实 API 重录。
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as this is a client-process library; the model runs in the spawned runtime, whose experience is owned by the plugins its `cordis.yml` composes.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **无捆绑运行时解析** —— 调用方显式指定运行时可执行文件;打包可执行文件的发现留在 Python 侧,直到出现 TypeScript 发行版消费者。
|
||||
- **无回合中取消** —— 线上没有 prompt 取消方法;放弃回合意味着关闭运行时(见协议的 [Known Limitations](../sdk-protocol/README.md))。
|
||||
- **每会话同时只有一个在途 prompt** —— 服务端规则,本客户端将其呈现为 `JsonRpcResponseError`;相互独立的会话可在同一运行时上并发。
|
||||
- **client→server 通知与 server→client 请求**在线两端都未实现;传输层为未来审批流保留了承载能力。
|
||||
@@ -107,13 +107,19 @@ export class NotificationSubscription implements AsyncIterable<HarnessNotificati
|
||||
this.fail(new TransportClosedError('notification subscription closed'))
|
||||
}
|
||||
|
||||
/** Reject pending and future waits with `error` (delivery stops). */
|
||||
/**
|
||||
* Reject pending and future waits (delivery stops; the first failure wins).
|
||||
* @param error - the terminal failure delivered to waiters.
|
||||
*/
|
||||
fail(error: Error): void {
|
||||
this.state.failure ??= error
|
||||
for (const waiter of this.state.waiters.splice(0)) waiter.reject(this.state.failure)
|
||||
}
|
||||
|
||||
/** Deliver one notification to a waiter or the queue when the filter matches. */
|
||||
/**
|
||||
* Deliver one notification to a waiter or the queue when the filter matches.
|
||||
* @param notification - the wire notification to deliver.
|
||||
*/
|
||||
push(notification: HarnessNotification): void {
|
||||
if (this.state.filter !== undefined && !this.state.filter(notification)) return
|
||||
const waiter = this.state.waiters.shift()
|
||||
@@ -413,7 +419,11 @@ export class HarnessClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `value` is a plain JSON object (the wire-boundary shape probe). */
|
||||
/**
|
||||
* Whether `value` is a plain JSON object (the wire-boundary shape probe).
|
||||
* @param value - the wire value to probe.
|
||||
* @returns `true` iff `value` is a non-null, non-array object.
|
||||
*/
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user