从程序驱动 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-client | TypeScript 客户端,含高层 DeepSeekHarness 与低层 HarnessClient |
@deepseek-ai/dsh-sdk-jsonrpc-server | 服务端插件:在 stdio 上以 newline JSON-RPC 服务进程外 SDK 客户端 |
deepseek-harness-sdk | Python 版(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带错误消息。错误响应以保留 wirecode与可选data的JsonRpcResponseError形式拒绝 pending 的request()。
private write(message: Record<string, unknown>): void {
this.output.write(`${JSON.stringify(message)}\n`)
}
wire 方法表
协议命名所有载荷(packages/sdk/protocol/src/types.ts 的 HarnessSdkRequestMap / HarnessSdkNotificationMap):
| 方向 | 方法 | 载荷 | 说明 |
|---|---|---|---|
| client→server | initialize | InitializeParams → InitializeResult | 进程级握手:工作目录 + provider/model 路由 + 可选正 maxTokens |
| client→server | session/prompt | SessionPromptParams → SessionPromptResult | 排队一条用户消息,立即返回持久的 { messageId } 入队回执 |
| client→server | shutdown | 无参 → {} | 处置 SDK 自有 agent/适配器/订阅到静默,再退出 |
| server→client | session.event | SessionEventNotification | 运行时内每个会话的完整会话日志事件(不筛选),流式记录即发送 |
| server→client | session.status | SessionStatusNotification | 整台 agent 的 running / idle 跃迁 |
| server→client | subagent.started | SubagentStartedNotification | 运行时内创建了一个子会话 |
| server→client | subagent.finished | SubagentFinishedNotification | 一次进程内子 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.md 与 src/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.ts 的 HarnessSession.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()先发协议shutdown(shutdownTimeoutMs,默认 1000ms),再走 stdin-EOF → SIGTERM → SIGKILL 阶梯(disposeEofGraceMs默认 6000、disposeGraceMs默认 3000)直到进程真正退出。阶梯在 harness context 之外运行,因此不走dsh-subprocess服务——这是该缝文档化的异常。它幂等,且已经关闭的 client 拒绝复用。HarnessClientOptions.env给定时整体替换子进程环境(undefined则继承);凭据策略由调用方负责(隔离型启动可用dsh-subprocess的scrubbedParentEnv作为共享 scrubbed 基础)。
两种 client 都是纯净库:在 Cordis context 上什么都不注册。它们拉起的运行时进程是完整 harness,其组合由它自己的
cordis.yml决定。
Python SDK
deepseek-harness-sdk(模块 deepseek_harness)是 TypeScript client 的设计孪生,共享同一运行时 peer、同一协议、同一分层(python/README.md、python/sdk/README.md、python/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 的活动区间,返回上面的 RunResult。finish_reason 是该区间内最后一条根会话 turn/end 的 kind(如 completed / max-tokens / error;无回合结束时为 None);final_response 是区间内最后一条已提交的根会话 assistant 文本——两者都描述区间而非因果分配给该 prompt 的输出。run() 还接收并透传 on_notification,低层 session_prompt()(对应 session/prompt)只返回排队 MessageId,绕过 Session.run() 的调用方要自己拥有后续活动边界。
为什么这满足了"把 DSH 当嵌入运行时驱动"
- 边是纯进程边——运行时是子进程,stdio 是工厂协议行;不依赖任何 UI、网络端口或全局安装,可嵌进 CI、批处理、编排器。
- stdout 纯净——协议帧独占 stdout,诊断走 stderr,任何程序化解析都不会被日志污染。
- 明确的活动区间——
run()(或自己组合session.event+session.status)把"发一条提示 → 等到整台 agent 空闲"抽象成一次可 await 的调用,并归还finalResponse与完整事件/通知流。 - 生命周期完整——
await using/ context manager 保证子进程被回收;close()走协议shutdown+ EOF/SIGTERM/SIGKILL 阶梯到幂等终态。 - 双语言对称——TS 与 Python 走同一条 wire,选任意一门。跨进程子 agent 后端(
dsh-sdksubagent 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}.md与python/sdk/README{.zh}.md。