Skip to content

客户端 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)刻意暴露一个很小的对外面:

层符号职责
高层运行 APIDeepSeekHarness、HarnessSession拥有一个运行时进程;入队一个提示词;收集到 idle 为止
底层协议客户端HarnessClient显式 start/initialize/prompt/request/close + 通知订阅
错误JsonRpcResponseError、RequestTimeoutError、SdkProtocolError、TransportClosedError来自线路的类型化失败

规范化辅助(normalizeInput、finalResponse、isRecord、validatedSessionEvent)以及订阅投递机制是内部实现,不是消费者导入。

最小用法(真实 API) ​

按 src/api.ts 的用法,最小的一次运行是:

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。

ts
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):

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 管理的传输层记录在案的例外)。

ts
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 是内部生产者):

ts
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 服务。它是幂等的,且关闭后的客户端拒绝复用。

ts
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 会与客户端做版本校验;混用发行版本不在契约内

包 ​

包nameversion
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。