客户端 SDK 是消费进程所启动并与之对话的部分。它封装了线路协议,调用者无需手写 JSON-RPC 帧:创建 DeepSeekHarness,调用 run('…'),即可拿到带最终答案及其产生过程事件流的 RunResult。它是 Python SDK(deepseek_harness)的设计孪生,共享同一运行时对端、协议与分层。本页引用 packages/sdk/client/src。
包
@deepseek-ai/dsh-sdk-client 是一个纯库:它不会在任何 Cordis 上下文上注册任何东西。它启动的子进程是一个完整 harness,其组合由它自己的 cordis.yml 决定。
| 字段 | 值 |
|---|---|
| name | @deepseek-ai/dsh-sdk-client |
| version | |
| 角色 | TypeScript 客户端 SDK——驱动 Harness 运行时子进程 |
| 协议对端 | @deepseek-ai/dsh-sdk-protocol |
| peer 依赖 | dsh-invariants、dsh-llm、dsh-sdk-protocol、dsh-session、cordis |
两层结构
包根(src/index.ts)刻意暴露一个很小的对外面:
| 层 | 符号 | 职责 |
|---|---|---|
| 高层运行 API | DeepSeekHarness、HarnessSession | 拥有一个运行时进程;入队一个提示词;收集到 idle 为止 |
| 底层协议客户端 | HarnessClient | 显式 start/initialize/prompt/request/close + 通知订阅 |
| 错误 | JsonRpcResponseError、RequestTimeoutError、SdkProtocolError、TransportClosedError | 来自线路的类型化失败 |
规范化辅助(normalizeInput、finalResponse、isRecord、validatedSessionEvent)以及订阅投递机制是内部实现,不是消费者导入。
最小用法(真实 API)
按 src/api.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-official',
model: 'deepseek-v4-flash',
maxTokens: 49_152,
})
const result = await harness.run('say hi')
console.log(result.finalResponse)关于 launch 规格的说明:launch.command/launch.args 是完全显式的(本包面向仓库相邻、明确知道自己要启动哪个运行时的 TypeScript 消费者)。绑定运行时的解析(找到打包好的可执行文件)仍是 Python 发行版的职责。cwd、provider、model、maxTokens 是会话路线:cwd 默认取 launch cwd 再取 process.cwd(),provider 默认 deepseek-official,model 默认 deepseek-v4-flash。
DeepSeekHarness:自有运行 API
DeepSeekHarness(src/api.ts)是一个 AsyncDisposable,在多个会话之间拥有一个运行时子进程。
- 懒启动。
start()会记忆化initialize握手,并在首次使用时调用。失败时会回收运行时(HarnessClient.close)并换上一个全新客户端,因此后续调用会用新子进程重试——直到close()让该实例终结。 session(sessionId?)打开一个具名或全新的会话句柄(session-<uuid>)。这不会产生任何线上流量;运行时在首次提示时才会创建会话。run(input, { sessionId?, onNotification? })转发到this.session(sessionId).run(...)。close()置closed = true并拆掉客户端;await using会自动调用它。get client(): HarnessClient暴露底层客户端。
client getter 是唯一的缓存注意点:一次失败的 start() 之后实例会被替换,所以绝不要跨“失败的 start()”缓存它。
HarnessSession.run
run 在一个会话上拥有一个活动时间区间:
入队提示词 → 等待该
MessageId出现在一条持久的agent/inbox/spliced回执里 → 收集所有通知,直到下一次整个 agent 的idle。
async run(input: string | ContentBlock[], options?): Promise<RunResult> {
await this.harness.start()
const client = this.harness.client
const contentBlocks = normalizeInput(input) // 字符串 -> [{type:'text',text}]
// subscribeSessionTree 把范围限制到本会话及其后代
const subscription = client.subscribeSessionTree(this.id)
const messageId = await client.prompt(this.id, contentBlocks)
// ... 等待收件箱回执,然后收集到 session.status === 'idle' 为止
}返回的 RunResult(src/types.ts):
export interface RunResult {
sessionId: string
finalResponse: string // 区间内最后一条 assistant/message 的拼接文本
events: SessionEvent[] // 根会话的 session.event 载荷,按线上顺序
notifications: HarnessNotification[] // 根会话 + 后代(来自 subagent.started),按线上顺序
}重要的语义:finalResponse 是区间内最后一条已提交的根会话 assistant 文本,并不是因果地归属于该提示词的响应——转向(steering)、注入的上下文与其他已入队的工作都可能在其到达 idle 之前作出贡献。events 只保存根会话事件;notifications 还会包含从 subagent.started 发现的后代。结果不携带提示词级状态或对话原因。
HarnessClient:协议客户端
HarnessClient(src/client.ts)直接拥有子进程——它在任何 harness 上下文之外运行,因此用 node:child_process 直接 spawn,而非通过 dsh-subprocess 服务(这是该 seam 文档为 SDK 管理的传输层记录在案的例外)。
export class HarnessClient {
start(): void // spawn + 开始读帧
async initialize(params: InitializeParams) // 进程级握手
async prompt(sessionId, contentBlocks): Promise<string> // 返回 messageId
async request(method, params?, timeoutMs?): Promise<unknown>
subscribe(filter?): NotificationSubscription
subscribeSessionTree(sessionId): NotificationSubscription
close(): Promise<void> // 有界的 shutdown + 处置阶梯
}prompt() 只要运行时接收了就会立刻返回所入队消息的 id;它绝不等待 agent 活动。
异步事件处理
subscribe(filter?) 返回一个 NotificationSubscription(NotificationSubscriptionImpl 是内部生产者):
export interface NotificationSubscription extends AsyncIterable<HarnessNotification> {
next(): Promise<HarnessNotification> // 等待下一条匹配的通知
tryNext(): HarnessNotification | undefined // 立即取出一条已投递的,不等待
close(): void
}由于它是 AsyncIterable,你可以 for await (const n of subscription),直到该订阅或运行时关闭为止。抛异常的过滤器只会让这一个订阅失败(被分离后,这个异常成为它的终止错误);它绝不会打扰兄弟订阅或传输层的读循环。
subscribeSessionTree(id) 把范围限制到一条会话以及由 subagent.started 血缘边发现的所有后代。运行时会对上下文中的每条会话发出通知;范围过滤在客户端进行,与 Python SDK 完全一致。客户端维护一个 sessionParents 映射(child -> parent,由 subagent.started 构建),并通过 isDescendantOf 遍历判断成员关系。
错误处理
客户端把所有线路、传输与超时失败都规整为类型化错误。
| 错误 | 时机 |
|---|---|
JsonRpcResponseError | 对端以 JSON-RPC 的 error 响应;保留线上 code 与 data |
RequestTimeoutError | 配置的每请求上限超时({method} timed out after …) |
SdkProtocolError | 响应超出文档规定的协议(例如 session/prompt 没有 messageId) |
TransportClosedError | 运行时没了——消息携带退出码和有界(400 行)stderr 尾部 |
线上没有取消机制:一个超时的请求在服务端可能仍在运行,直到运行时被关闭。超时使用 AbortController,其 abort 会移除传输层的 pending 条目,因此对某个挂起方法的重复有界请求不会保留每次调用的状态。
关闭与处置阶梯
close() 先发起协议 shutdown(受 shutdownTimeoutMs 限制,默认 1000 ms),然后走 stdin-EOF → SIGTERM → SIGKILL 阶梯,直到进程真正退出:关闭之后 disposeEofGraceMs(默认 6000)等待 EOF,POSIX 下 disposeGraceMs(默认 3000)在 SIGTERM 后、SIGKILL 前等待退出确认。该阶梯是客户端私有的——它在任何 harness 上下文之外运行,因此无法借用 dsh-subprocess 服务。它是幂等的,且关闭后的客户端拒绝复用。
export interface HarnessClientOptions {
command: string // 运行时可执行文件(dsh-jsonrpc-agent、打包 exe 或 node)
args?: string[]
cwd?: string
env?: NodeJS.ProcessEnv // 传入时完全替换子环境
requestTimeoutMs?: number // undefined = 无限等待
shutdownTimeoutMs?: number // 默认 1000
disposeEofGraceMs?: number // 默认 6000
disposeGraceMs?: number // 默认 3000
}env 传入时替换环境、undefined 时继承父环境;凭据策略由调用者负责——dsh-subprocess 的 scrubbedParentEnv 是面向隔离启动的共享擦洗基座。
与 SDK 生态的关系
客户端正是 subagent-dsh-sdk(见 子代理) 后端用来把每个子 agent 作为全新子进程中的完整运行时来运行的载体——是继 subagent-acp 之后第二个进程外子 agent 后端。该 provider 通过 DeepSeekHarness spawn、完成 initialize 握手,然后从子 agent 的会话事件中读取回答。线路与分层与 Python SDK 1:1 共享;只有 launch 规格不同(这里是显式的 command/args,Python 是绑定运行时解析)。
已知局限
| 局限 | 影响 |
|---|---|
| 没有绑定运行时解析 | 调用者显式命名运行时可执行文件;打包可执行文件的发现逻辑在 Python 侧 |
| 没有中途取消 | 线上没有提示词取消防法;放弃一轮对话意味着关闭运行时 |
| 没有针对提示词的结果 | 底层 prompt() 只返回入队回执;高层 run() 负责回执→idle 的收集 |
| 客户端→服务端通知与服务端→客户端请求 | 两端都未实现;传输层为将来的审批流程承载它们 |
| 没有面向模型的面 | 客户端不贡献提示词/工具/会话事件;模型运行在 spawn 出来的运行时里 |
包
| 包 | name | version |
|---|---|---|
| SDK 客户端 | @deepseek-ai/dsh-sdk-client | |
| SDK 线路协议(对端) | @deepseek-ai/dsh-sdk-protocol | |
| Python 镜像 | deepseek-harness-sdk(PyPI) | 与运行时 bin 同一发布通道 |