客户端 SDK 是消费进程所启动并与之对话的部分。它封装了线路协议,调用者无需手写 JSON-RPC 帧:创建 DeepSeekHarness,调用 run('…'),即可拿到带最终答案及其产生过程事件流的 RunResult。它是 Python SDK(deepseek_harness)的设计孪生,共享同一运行时对端、协议与分层。本页引用 packages/sdk/client/src。
包
@deepseek-ai/dsh-sdk-client 是一个纯库:它不会在任何 Cordis 上下文上注册任何东西。它启动的子进程是一个完整 harness,其组合由它的 profile(默认 sdk)决定——启动会解析一个同版本的 dsh 可执行文件并运行 --profile <profile>(外加任何补丁)。
| 字段 | 值 |
|---|---|
| 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({
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
maxTokens: 49_152,
})
const result = await harness.run('say hi')
console.log(result.finalResponse)启动选项(packages/sdk/client/src/types.ts):dshBin?(绝对或相对调用方 cwd 的 dsh CLI 模块;省略时解析本包同版本的 @deepseek-ai/dsh 依赖)、profile?(提供 SDK 协议的具名 profile,默认 'sdk')、patches?(有序的逐启动 profile 补丁)、dshHome?(显式 harness 主目录;相对路径在启动前解析)、processCwd?(dsh 进程自身的工作目录),以及超时旋钮 initializeTimeoutMs?(默认 10 秒)、requestTimeoutMs?、shutdownTimeoutMs?、disposeEofGraceMs?(默认 6000)、disposeGraceMs?(默认 3000)。cwd、provider、model、maxTokens 是会话路线:cwd 默认取进程 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 {
dshBin?: string // 省略时:解析本包同版本的 dsh 依赖
profile?: string // 默认 'sdk'
patches?: string[] // 有序的逐启动 profile 补丁
dshHome?: string // 显式 Harness 主目录;相对路径在启动前解析
processCwd?: string // dsh 进程自身的工作目录
env?: NodeJS.ProcessEnv // 传入时完全替换子环境
initializeTimeoutMs?: number // 默认 10000(初始 profile 握手的界限)
requestTimeoutMs?: number // undefined = 无限等待
shutdownTimeoutMs?: number // 默认 1000
disposeEofGraceMs?: number // 默认 6000
disposeGraceMs?: number // 默认 3000
}env 传入时替换环境、undefined 时继承父环境;凭据策略由调用者负责——dsh-subprocess 的 scrubbedParentEnv 是面向隔离启动的共享擦洗基座。
运行时解析
不再要求调用者点名可执行文件:省略 dshBin 时,客户端自动解析同版本的 @deepseek-ai/dsh bin 并校验其版本是否与自身清单一致——resolveDshBinFromManifests(packages/sdk/client/src/launch.ts)在 dsh 版本不等于客户端版本时抛错,否则返回绝对可执行路径。resolveDshLaunch 把 argv 拼成 <node> <dsh bin> --profile <profile> [--patch <path>…],并通过 DSH_HOME 喂入显式 dshHome。调用者仍可用 dshBin/processCwd/dshHome 钉住某个具体运行时。
与 SDK 生态的关系
客户端正是 subagent-dsh-sdk(见 子代理) 后端用来把每个子 agent 作为全新子进程中的完整运行时来运行的载体——是继 subagent-acp 之后第二个进程外子 agent 后端。该 provider 通过 DeepSeekHarness spawn、完成 initialize 握手,然后从子 agent 的会话事件中读取回答。线路与分层与 Python SDK 1:1 共享;Python 侧跑同样的 profile 驱动启动(经它自己的绑定运行时解析)。
已知局限
| 局限 | 影响 |
|---|---|
| 没有中途取消 | 线上没有提示词取消防法;放弃一轮对话意味着关闭运行时 |
| 没有针对提示词的结果 | 底层 prompt() 只返回入队回执;高层 run() 负责回执→idle 的收集 |
| 客户端→服务端通知与服务端→客户端请求 | 两端都未实现;传输层为将来的审批流程承载它们 |
| 没有面向模型的面 | 客户端不贡献提示词/工具/会话事件;模型运行在 spawn 出来的运行时里 |
| 要求同版本运行时 | 自动解析的 @deepseek-ai/dsh 会与客户端做版本校验;混用发行版本不在契约内 |
包
| 包 | name | version |
|---|---|---|
| SDK 客户端 | @deepseek-ai/dsh-sdk-client | |
| SDK 线路协议(对端) | @deepseek-ai/dsh-sdk-protocol | |
| Python 镜像 | deepseek-harness-sdk(PyPI) | 与运行时 bin 同一发布通道 |
延伸阅读
- SDK 协议——本客户端所驱动的帧格式与具名类型。
- SDK 服务端——回应
initialize/session/prompt/shutdown的运行时侧插件。 - 消费本客户端的子 agent 后端:
packages/subagent/subagent-dsh-sdk/README.md。 - 发行版的工作组合:
packages/bundle/sdk-app(dsh --profile sdk)与bundle/sdk-minimal。 - 仓库内相对路径:
packages/sdk/client/src/api.ts、packages/sdk/client/src/client.ts、packages/sdk/client/src/types.ts、packages/sdk/client/src/launch.ts。