Skip to content

客户端 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-invariantsdsh-llmdsh-sdk-protocoldsh-sessioncordis

两层结构

包根(src/index.ts)刻意暴露一个很小的对外面:

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

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

最小用法(真实 API)

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

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 发行版的职责。cwdprovidermodelmaxTokens 是会话路线:cwd 默认取 launch cwd 再取 process.cwd()provider 默认 deepseek-officialmodel 默认 deepseek-v4-flash

DeepSeekHarness:自有运行 API

DeepSeekHarnesssrc/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' 为止
}

返回的 RunResultsrc/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:协议客户端

HarnessClientsrc/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?) 返回一个 NotificationSubscriptionNotificationSubscriptionImpl 是内部生产者):

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 响应;保留线上 codedata
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 {
  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-subprocessscrubbedParentEnv 是面向隔离启动的共享擦洗基座。

与 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 出来的运行时里

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
  • 发行版的工作组合:examples/jsonrpc-agent/cordis.yml
  • 仓库内相对路径:packages/sdk/client/src/api.tspackages/sdk/client/src/client.tspackages/sdk/client/src/types.ts