跳到主要内容
路径文档

从程序驱动 DSH:SDK

一句话版:把 DeepSeek Harness 运行时(一个你要自己指定的可执行文件 + cordis.yml)作为子进程拉起,通过 stdio 上的 newline JSON-RPC 协议从你自己的程序里逐轮驱动 agent——发提示、订阅会话事件、直到整台 agent 变空闲,最后优雅关停并回收子进程。

DSH 不只是交互式 Web UI。packages/sdk/ 提供了一套面向进程外调用的协议栈:调用方提供运行时可执行文件与它的 cordis.yml,SDK 负责把运行时当作一个可嵌入的"引擎"来启动、驱动与回收。这套栈不创建、不配置、不构建、不启动开发者工程——那是开发者侧的事;SDK 只负责"把运行时拉起来,然后跟它对话"。

组成与分工

职责
@deepseek-ai/dsh-sdk-protocol定义 SDK 运行时通信协议:一个 newline-delimited JSON-RPC 传输类 + 双方都说的命名请求/结果/通知类型
@deepseek-ai/dsh-sdk-clientTypeScript 客户端,含高层 DeepSeekHarness 与低层 HarnessClient
@deepseek-ai/dsh-sdk-jsonrpc-server服务端插件:在 stdio 上以 newline JSON-RPC 服务进程外 SDK 客户端
deepseek-harness-sdkPython 版(deepseek_harness),镜像同一协议与分层,但不 import TS 类型

服务端:sdk-jsonrpc-server 插件与应用边界

运行时侧由一个名叫 sdk-jsonrpc-server 的插件承担。它声明 inject: ['agents']packages/sdk/server/src/index.ts):

export const name = 'sdk-jsonrpc-server'
// 只需 agent 工厂;initialize 用 ctx.get() 读可选的 LLM 能力缝。
export const inject = ['agents']

核心是 HarnessSdkJsonRpcServer:构造时订阅会话、agent、subagent 生命周期事件并发通知;initialize 配置路由、必要时挂载 DeepSeek 适配器回落;session/prompt 按 sessionId 取/建一次会话并排队一条用户消息,立即返回 { messageId } 入队回执;shutdown 处置所有 SDK 自有 agent、适配器与订阅到静默,然后由插件 flush 响应、dispose 根上下文并 exit(0)handleRequest 是方法分发(packages/sdk/server/src/server.ts):

async handleRequest(method: string, params: Record<string, unknown> | undefined): Promise<unknown> {
switch (method) {
case 'initialize': return this.initialize(params as InitializeParams)
case 'session/prompt': return this.prompt(params as SessionPromptParams)
case 'shutdown': return this.shutdown()
default: throw new Error(`unknown DeepSeek Harness SDK runtime method: ${method}`)
}
}

stdout 就是协议。插件 README 明确:stdout 只承载 JSON-RPC 帧,部署不得组合 stdout 日志器;诊断必须走 stderr(packages/sdk/server/README.md)。因此运行时的 cordis.yml 必须不含 stdout logger,外部 jsonrpc-demo 应用把"脊柱 + 后端 + 服务插件"组合在一起。

传输层:JsonRpcLineTransport

协议用一个传输类 JsonRpcLineTransport 把 JSON-RPC 2.0 帧在调用方持有的字节流上按每行一个紧凑 JSON 帧传输(packages/sdk/protocol/src/transport.ts):

  • id + method 的是请求;只有 id 是响应;只有 method 是通知;格式错误的 JSON 行被忽略
  • start() 挂监听、close() 摘监听并在不销毁流的情况下拒绝 pending 请求。
  • 缺失请求处理器回 -32601;处理器抛错回 -32603 带错误消息。错误响应以保留 wire code 与可选 dataJsonRpcResponseError 形式拒绝 pending 的 request()
private write(message: Record<string, unknown>): void {
this.output.write(`${JSON.stringify(message)}\n`)
}

wire 方法表

协议命名所有载荷(packages/sdk/protocol/src/types.tsHarnessSdkRequestMap / HarnessSdkNotificationMap):

方向方法载荷说明
client→serverinitializeInitializeParamsInitializeResult进程级握手:工作目录 + provider/model 路由 + 可选正 maxTokens
client→serversession/promptSessionPromptParamsSessionPromptResult排队一条用户消息,立即返回持久的 { messageId } 入队回执
client→servershutdown无参 → {}处置 SDK 自有 agent/适配器/订阅到静默,再退出
server→clientsession.eventSessionEventNotification运行时内每个会话的完整会话日志事件(不筛选),流式记录即发送
server→clientsession.statusSessionStatusNotification整台 agent 的 running / idle 跃迁
server→clientsubagent.startedSubagentStartedNotification运行时内创建了一个子会话
server→clientsubagent.finishedSubagentFinishedNotification一次进程内子 agent 运行结束(仅本地运行;远端运行不上报)

几个关键语义,都来自协议 README / 类型 JSDoc:

  • SessionPromptResult.messageId 只标识被排队的 UserMessage不代表后续某条 assistant 消息、回合结束或 prompt 结果。
  • 客户端自行结合开放式的 session.event 流与整台 agent 的 session.status 来划分"活动区间"。
  • SubagentFinishedNotification.lastAssistantMessage 含子的最后非空 assistant 消息;都没有则该字段缺席。
  • InitializeParams.maxTokens 是可选正整数,封顶 SDK 创建 agent(及进程内后代)的每次对话模型输出。
  • serverInfo.name 保持线上稳定值 deepseek-harness-sdk-runtime(当前版本 0.0.1,未校验)。

协议 README 也坦承当前边界:无协议版本协商serverInfo.version 未校验)、无 cancel / session 关闭方法(放弃某回合需关闭运行时进程)、服务器→客户端请求是"死能力"(传输支持但服务器从不发送,为未来审批流预留)。

客户端:两层 API

高层 DeepSeekHarness(自带运行生命周期)

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)

packages/sdk/client/README.mdsrc/api.ts)要点:

  • 子进程在首次使用时惰性启动,并在多次 run() 间由本实例持有;close()(或 await using)是必须的,否则子进程不会被回收。
  • start() 记忆化 initialize 握手;握手失败会回收运行时并换成新 client,以便下次调用用新子进程重试(直到 close() 终结)。
  • run(input, { sessionId?, onNotification? }) 拥有一次活动区间:排队 prompt → 等其 MessageId 出现在持久的 agent/inbox/spliced 回执里 → 收满到下一次整台 agent idle。返回 RunResult { sessionId, finalResponse, events, notifications }
  • finalResponse 是区间内最后一条已提交的根会话 assistant 文本,不是因果上分配给这条 prompt 的输出——转向、注入上下文、其他排队工作都可能在 idle 前贡献内容。events 只含根会话事件,notifications 另含从 subagent.started 发现的全部后代,均按 wire 顺序。
  • session(id?) 打开一个命名或全新会话句柄。

src/api.tsHarnessSession.run 是这条"收到回执 → idle"收集循环:

const messageId = await client.prompt(this.id, contentBlocks)
let received = false
while (true) {
const notification = await subscription.next()
if (!received) {
if (notification.method !== 'session.event'
|| notification.params.sessionId !== this.id
|| !isInboxReceipt(notification.params.event, messageId)) continue
received = true
}
collect(notification)
if (notification.method === 'session.status'
&& notification.params.sessionId === this.id
&& notification.params.status === 'idle') break
}

低层 HarnessClient(精细控制)

协议客户端(src/client.ts):显式 start() / initialize() / prompt() / request() / close(),加通知订阅。

  • prompt(sessionId, contentBlocks) 一旦运行时接受就返回排队消息 id,从不等待 agent 活动。
  • subscribe(filter?) 返回 NotificationSubscription(可 await 的 next()、非阻塞 tryNext()、async 迭代);subscribeSessionTree(id) 把作用域收窄到一个会话及 subagent.started 血统边推出来后发现的后代——运行时对整个 context 里的每个会话都发通知,收窄是客户端侧做的,与 Python SDK 相同。
  • 类型化错误(src/client.ts):JsonRpcResponseError(wire 错误,保留 code/data)、RequestTimeoutError(超时)、SdkProtocolError(协议外响应)、TransportClosedError(运行时没了——消息带退出码与有界的 stderr 尾)。
  • close() 先发协议 shutdownshutdownTimeoutMs,默认 1000ms),再走 stdin-EOF → SIGTERM → SIGKILL 阶梯(disposeEofGraceMs 默认 6000、disposeGraceMs 默认 3000)直到进程真正退出。阶梯在 harness context 之外运行,因此不走 dsh-subprocess 服务——这是该缝文档化的异常。它幂等,且已经关闭的 client 拒绝复用。
  • HarnessClientOptions.env 给定时整体替换子进程环境(undefined 则继承);凭据策略由调用方负责(隔离型启动可用 dsh-subprocessscrubbedParentEnv 作为共享 scrubbed 基础)。

两种 client 都是纯净库:在 Cordis context 上什么都不注册。它们拉起的运行时进程是完整 harness,其组合由它自己的 cordis.yml 决定。

Python SDK

deepseek-harness-sdk(模块 deepseek_harness)是 TypeScript client 的设计孪生,共享同一运行时 peer、同一协议、同一分层(python/README.mdpython/sdk/README.mdpython/sdk/src/deepseek_harness/)。

python -m pip install deepseek-harness-sdk
from deepseek_harness import DeepSeekHarness

with DeepSeekHarness(
provider="deepseek-official",
model="deepseek-v4-flash",
max_tokens=49_152,
) as harness:
result = harness.run("Make the requested code change.")

Python 侧的关键差异:

  • 自带运行时分发包:装 deepseek-harness-sdk 会装同版本的 deepseek-harness-runtime-bin 平台 wheel;默认入口因此无需可执行参数,直接从绑定单文件 dsh-jsonrpc-agent 可执行程序启动。TS client 的启动参数是完全显式的(command/args),打包运行时的解析仍是 Python 分发侧的事。
  • 通过 DSH_CORDIS_CONFIG 注入一套默认组合(stdio JSON-RPC 服务器、agent core、预载 DeepSeek 适配器、JSONL 会话持久化 + 显式组合的语义 checkpoint 策略、本地 bash);要跑自己的插件组合,保留 dsh-sdk-jsonrpc-server 条目并传 Cordis 配置路径。

Python 高层 DeepSeekHarness 同样可复用(context manager 或显式 close())。

@dataclass(slots=True)
class RunResult:
session_id: str
final_response: str
finish_reason: str | None
events: list[JsonObject]
notifications: list[Notification]
session_root: str | None = None

Session.run() 拥有从 prompt 的持久 inbox 回执到下一次整台 agent idle 的活动区间,返回上面的 RunResultfinish_reason 是该区间内最后一条根会话 turn/endkind(如 completed / max-tokens / error;无回合结束时为 None);final_response 是区间内最后一条已提交的根会话 assistant 文本——两者都描述区间而非因果分配给该 prompt 的输出。run() 还接收并透传 on_notification,低层 session_prompt()(对应 session/prompt)只返回排队 MessageId,绕过 Session.run() 的调用方要自己拥有后续活动边界。

为什么这满足了"把 DSH 当嵌入运行时驱动"

  1. 边是纯进程边——运行时是子进程,stdio 是工厂协议行;不依赖任何 UI、网络端口或全局安装,可嵌进 CI、批处理、编排器。
  2. stdout 纯净——协议帧独占 stdout,诊断走 stderr,任何程序化解析都不会被日志污染。
  3. 明确的活动区间——run()(或自己组合 session.event + session.status)把"发一条提示 → 等到整台 agent 空闲"抽象成一次可 await 的调用,并归还 finalResponse 与完整事件/通知流。
  4. 生命周期完整——await using / context manager 保证子进程被回收;close() 走协议 shutdown + EOF/SIGTERM/SIGKILL 阶梯到幂等终态。
  5. 双语言对称——TS 与 Python 走同一条 wire,选任意一门。跨进程子 agent 后端(dsh-sdk subagent provider)正是用这套 TS client 把每个子 agent 跑成完整 peer harness。

验证 / 试一试

仓库提供 jsonrpc-demo 应用(packages/examples/jsonrpc-demo/README.md):一个只装 bin 的应用,启动一个外部 cordis.yml,其 jsonrpc 条目按 newline stdio 服务 SDK 客户端。

# 1) 在 DSH 源码仓库里跑 demo:组合树的服务器
pnpm --dir /path/to/deepseek-harness run demo:jsonrpc

配置发现顺序:$DSH_CORDIS_CONFIG 优先,其次位置参数 argv[2]。两者都没指向已存在的文件时,bin 向 stderr 打一行用法并 exit 1;没有工作目录或内置兜底。不含 dsh-sdk-jsonrpc-server 的配置合法但什么也不服务

# 2) 在会话里驱动它(换真实模型名)
node -e "import('@deepseek-ai/dsh-sdk-client').then(async ({ DeepSeekHarness }) => {
await using const h = new DeepSeekHarness({
launch: { command: 'dsh-jsonrpc-agent', args: ['cordis.yml'] },
provider: 'deepseek-official', model: 'deepseek-v4-flash',
});
const r = await h.run('say hi');
console.log(r.finalResponse);
})"

或直接发一条新行 JSON-RPC 帧到运行时的 stdin,看 stdout 只回协议帧(诊断在 stderr):

# 3) 手搓帧:initialize 握手后在 stderr 观察诊断、stdout 只见 JSON
printf '%s\n' \
'{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"cwd":"'$PWD'","provider":"deepseek-official","model":"deepseek-v4-flash"}}' \
'{"jsonrpc":"2.0","id":"2","method":"shutdown"}' \
| dsh-jsonrpc-agent cordis.yml 2>/dev/null

想自己观察活动区间,直接订阅 session.event / session.status 通知流(见上文 wire 方法表),或用高层 run(input, { onNotification }) 打印每条通知。

下一步

  • 想看这层 API 在 harness 内部怎么当子 agent 后端用,见 子 Agent 与并行
  • 属性与限制完整引用见源码 packages/sdk/{protocol,client,server}/README{.zh}.mdpython/sdk/README{.zh}.md