从程序驱动 DSH:SDK
一句话版:SDK 将
dshprofile 作为子进程启动,经 stdio newline JSON-RPC 驱动并回收它。源码基线:0.1.5-alpha.1/5dda764ed3;npmlatest/next为0.1.2-rc.1,alpha通道为0.1.5-alpha.1(状态页)。
TypeScript client 默认解析匹配版本的 @deepseek-ai/dsh 依赖,也可指定 dshBin。组合通过 profile(默认 sdk)与有序 patches 选择,不再使用旧 launch: { command, args } 或整份外部 cordis.yml 启动契约。被开发的工程仍是独立工作区。
组成与分工
| 包 | 职责 |
|---|---|
@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 unknown as InitializeParams)
case 'session/prompt': return this.prompt(params as unknown as SessionPromptParams)
case 'shutdown': return this.shutdown()
default: throw new Error(`unknown DeepSeek Harness SDK runtime method: ${method}`)
}
}
stdout 就是协议:SDK profile 将 stdout 留给 JSON-RPC 帧,诊断走 stderr;自定义插件应保持这一边界。运行时组合由 profile 与 patches 决定。
传输层: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({
profile: 'sdk',
dshHome: '/absolute/path/to/isolated-dsh-home',
cwd: '/absolute/path/to/workspace',
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,其组合由选定 profile 与 patches 决定。
Python SDK
安装时应让 SDK 与运行时版本匹配;下列源码契约不代表 alpha wheel 已发布。
from deepseek_harness import DeepSeekHarness
with DeepSeekHarness(
dsh_home="/absolute/path/to/isolated-dsh-home",
cwd="/absolute/path/to/workspace",
profile="sdk",
provider="deepseek-official",
model="deepseek-v4-flash",
max_tokens=49_152,
) as harness:
result = harness.run("概述这个项目。")
print(result.final_response)
- 包依赖精确同版本
deepseek-harness-runtime-binwheel,启动绑定的dsh --profile sdk,旧dsh-jsonrpc-agent入口已退出。 - 显式提供
dsh_home或子环境里的非空DSH_HOME;Python SDK 不自动发现~/.dsh。 cwd是 Agent 工作区,runtime_cwd是子进程目录;patches是有序 overlay,dsh_bin替换 CLI 可执行文件但保留 profile 语法。- Python 初始化超时默认 30 秒(
initialize_timeout_seconds);普通 turn 默认无超时,除非设置request_timeout_seconds。TypeScript 初始化默认 10 秒。 - 源码中的 runtime wheel 目标包括 Windows x64、Linux x64/arm64、macOS 14+ arm64 与 x64(0.1.3-alpha.1 新增 macOS x64);具体发行版是否已发布应另查包仓库。
run()拥有持久 inbox 回执到整台 Agent idle 的活动区间。final_response/finish_reason是该区间最后的根回复 / turn 结束原因,不等于专属于这一条 prompt 的因果响应。- 使用 context manager 或
close()回收子进程。
完整 profile 与最小 profile
sdk 继承 base 组合。sdk-minimal 是独立完整树,提供平台对应的持久 shell、str_replace_editor、一个 DeepSeek adapter 与未压缩 JSONL 会话;省去 settings、受管凭据、telemetry、Web 工具、compaction、工作区 instructions、skills、jobs 和 subagents。
最小不等于受限:sdk-minimal 固定 danger-full-access,shell / editor 可访问进程有权限的任意路径。应使用一次性隔离环境,单独换一个工作目录并不构成隔离。
为什么这满足了"把 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。
验证 / 试一试
构建匹配的官方源码后:
pnpm dsh --profile sdk --help
pnpm dsh --profile sdk-minimal --help
pnpm dsh --profile sdk --dump-config
真实会话还需要配置模型路由与凭据。使用上方 TypeScript 示例,或源码中的 Python 示例:
python python/sdk/examples/minimal.py "Say hi" \
--workspace /absolute/path/to/disposable-workspace \
--dsh-home /absolute/path/to/isolated-dsh-home
Python 示例显式选择 sdk-minimal,访问策略见上节。可用订阅或 run(input, { onNotification }) 观察 session.event / session.status。旧 demo:jsonrpc 与整份外部配置启动示例现已由 profile 替代。
下一步
- 想看这层 API 在 harness 内部怎么当子 agent 后端用,见 子 Agent 与并行。
- 属性与限制完整引用见源码
packages/sdk/{protocol,client,server}/README{.zh}.md与python/sdk/README{.zh}.md。