第六部分 · 界面与协议
第 18 章:协议面——ACP、JSON-RPC 与 Python SDK
当对话的另一端不是人而是机器:三个协议的定位与实现
界面的另一极:机器客户端
第 17 章的人机界面靠浏览器;这一章的「界面」是给机器用的——三套协议回答三个不同的问题:
| 协议 | 回答的问题 | 实现位置 |
|---|---|---|
| ACP(Agent Client Protocol) | 另一个 agent 产品(如 Claude Code、Codex)如何把 dsh 当子代理调用? | packages/acp/acp |
| JSON-RPC SDK | 脚本与程序如何用类型化客户端驱动一个 dsh 会话? | packages/sdk/* |
| Python SDK | Python 世界如何零配置地使用 dsh? | python/ |
ACP:把 dsh 变成别人的子代理
Agent Client Protocol 是一个开放协议(由 Zed 提出、Claude Code 采用,dsh 基于外部 @agentclientprotocol/sdk 实现)——它定义了「agent 客户端」与「agent 服务端」之间如何对话。dsh 的 ACP server(packages/acp/acp)是自动化优先的服务端:方法只有五个——
| 方法 | 作用 |
|---|---|
initialize | 协议协商与能力声明 |
authenticate | 认证(密钥或免认证) |
newSession | 创建会话(带 cwd、mcp 服务器列表等) |
prompt | 提交 prompt(纯文本;结构化输出不是 ACP 的能力,由请求方自己解析文本) |
cancel | 取消进行中的 prompt |
事件只有两个:session/update(仅 committed 文本——流式半成品不上行,机器客户端拿到的每一条都是「已提交」的)与 session/request_permission(allow-once/reject-once 两态)。ACP 把第 11 章的进程外子代理变成现实:subagent-acp provider 就是 ACP 客户端的姿态,让另一个 dsh(或任何 ACP 服务端)成为子代理传输后端。自动化优先意味着「不需要人」:权限请求是协议内的一等事件,由请求方裁决。
JSON-RPC:换行帧的类型化客户端
内部 SDK(packages/sdk/)是一个 JSON-RPC 2.0 传输:protocol/src/transport.ts 用换行分帧(每行一个 JSON 消息,stdout 纯净是部署强制的——周边配置不能加载 stdout logger 污染信道),types.ts 定义 3 个请求 + 4 个通知;server 惰性建会话、shutdown → flush → exit(0);client 是子进程客户端,带会话树过滤与关闭阶梯。
第 11 章的 subagent-dsh-sdk provider 与第 2 章的 headless 都是它的消费者;examples/ 里有把另一个 dsh 当子代理的完整示例(jsonrpc-agent)。与 ACP 的定位区别一句话:ACP 是「对外兼容的自动化协议」(面向生态),JSON-RPC 是「内部的高效协议」(面向自家工具链)——两者都建立在「stdin/stdout 干净信道 + 逐行 JSON 帧」的同一套假设上。
SDK 是三包分层:protocol(唯一的线协议真源:命名类型 HarnessSdkRequestMap/HarnessSdkNotificationMap,wire 身份 deepseek-harness-sdk-runtime 恒为 wire-stable)、server(把 RPC 方法接到 ctx.agents 与会话事件)、client(TS/Python 两端的镜像实现)。传输层小到可以整段展示——帧判别就一个函数:
private async handleLine(line: string): Promise<void> {
let message: unknown
try {
message = JSON.parse(line)
} catch {
// Only JSON syntax errors reach this catch; malformed peer lines are ignored.
return
}
if (!message || typeof message !== 'object') return
const frame = message as Record<string, unknown>
const id = frame.id
const method = frame.method
if ((typeof id === 'string' || typeof id === 'number') && typeof method === 'string') {
await this.handleIncomingRequest(id, method, objectParams(frame.params))
return
}
if (typeof id === 'string' || typeof id === 'number') {
this.handleIncomingResponse(id, frame)
return
}
if (typeof method === 'string') {
this.notificationHandler?.(method, objectParams(frame.params))
}
}
三态判别(有 id+method = 请求、只有 id = 响应、只有 method = 通知)、-32601/-32603 错误码、AbortSignal 放弃——40 行的传输层支撑整个 SDK 栈,是「协议是接口的尘埃」的样本。
Python SDK:一等公民的另一种语言
python/ 是 Python 世界的入口。结构上分两层:
python/sdk/src/deepseek_harness/:api.py+client.py——类型化的 Python 客户端,通信方式与 Node SDK 相同:JSON-RPC over stdio(不是 HTTP)。python/sdk-runtime/:把 TS worker 运行时打进 Python SDK 的 exe 里(resolve_bundled_launch_args、bundled_default_config_path、runtime/cordis.yml默认组合)——Python 用户pip install后无需 Node、无需配置即可驱动一个 dsh。
「Python 一等公民」在 dsh 里有两层含义:模型侧,代码运行时(第 8 章)把 'python' 作为语言可移植契约预留值;工具侧,Python SDK 让数据科学家与脚本作者用母语驱动同一套 harness——同一棵插件树,两种语言的驾驶舱。
Python 进程 ── JSON-RPC over stdio ──► dsh(bundled runtime / 外部进程) │ │ └─ api.py / client.py └─ session · agent · tools(同一棵插件树)
两个实现细节值得展开。其一是惰性会话与收据语义:SDK server 的 initialize 配置全进程的 cwd/provider/model/maxTokens,session/prompt 遇到未知 sessionId 才创建 agent+session(sessionCreations 去重并发创建);prompt 只返回 durable 收据(messageId),不把后续活动因果归因给某次 prompt——「run 区间」的锚点是「收据(agent/inbox/spliced)→ whole-agent idle」两个事件,而不是某个内部状态。其二是客户端侧会话树过滤:server 广播所有 session 的事件(不按会话隔离下发),TS 与 Python 客户端各自用 subagent.started 边构建 parent 链、subscribeSessionTree 过滤根会话+后代——协议保持简单,职责放在两端镜像实现。
三个协议面(加上第 17 章的 api 包)恰好覆盖 agent/session 的三种消费形态:
| 面 | 会话所有权 | 生命周期 | 典型场景 |
|---|---|---|---|
| ACP | 一次性「建即拥有」 | 连接关闭即整体 teardown(含子代回收) | 另一个 agent 把 dsh 当子代理 |
| SDK server | 惰性「按 sessionId 复用」 | shutdown 才整体 dispose | 脚本/程序驱动多个会话 |
| TS/Python client | 完全在 harness 上下文之外 | spawn 子进程,关闭走协议 shutdown → EOF → SIGTERM → SIGKILL 阶梯 | pip 装即用、独立运行时 |
| api(Typert) | 浏览器侧远程视图 | 随 Web 会话 | Web UI(第 17 章) |
注意 SDK/ACP 与 api 包没有直接依赖:前者是进程内直连 ctx.agents + session 事件的「自有协议面」,后者是 Web 用的远程 BFF/Typert 网关——两条协议栈并列,共享同一内核。
把第 2 章(配置层)、第 16 章(自修改)、第 17 章(浏览器)、第 18 章(协议)放在一起,能看到 dsh 的「开放」是分层递进的:配置开放给部署者(cordis.yml + patch)、插件开放给模型(cordis 工具族)、界面开放给人(浏览器)、协议开放给机器(ACP/JSON-RPC/Python)。每一层都是同一棵树的对外接口——没有一套「公共 API」,只有按受众不同的多个表面。
实践应用
- 自动化协议要「已提交才上行」:ACP 的
session/update只发 committed 文本——机器客户端不需要流式半成品,需要的是「可信任的最终态」。 - stdout 纯净是部署契约:JSON-RPC 走换行帧,周边配置不得污染 stdout——「信道干净」是进程内协议的第一工程约束。
- 子代理即协议客户端:ACP/JSON-RPC/dsh-sdk 都是第 11 章的 provider——协议面与子代理面共享同一个抽象,外部系统无缝进入委托链。
- 零配置的第二语言:bundled runtime 把运行时打进 SDK——「pip install 即用」让非 Node 生态零摩擦接入。
总结
这一章拆完了协议面:面向生态的 ACP、面向工具链的 JSON-RPC、面向 Python 的 SDK——三个表面共享同一棵树、同一套事件与同一套能力。第六部分(界面与协议)到此闭环。最后一章,我们把全书散落的赌注收拢起来:dsh 到底赌了什么,哪些模式值得带走,哪些代价它还没有付清。