第六部分 · 界面与协议

第 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 SDKPython 世界如何零配置地使用 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_permissionallow-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_argsbundled_default_config_pathruntime/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」,只有按受众不同的多个表面。

实践应用

  1. 自动化协议要「已提交才上行」:ACP 的 session/update 只发 committed 文本——机器客户端不需要流式半成品,需要的是「可信任的最终态」。
  2. stdout 纯净是部署契约:JSON-RPC 走换行帧,周边配置不得污染 stdout——「信道干净」是进程内协议的第一工程约束。
  3. 子代理即协议客户端:ACP/JSON-RPC/dsh-sdk 都是第 11 章的 provider——协议面与子代理面共享同一个抽象,外部系统无缝进入委托链。
  4. 零配置的第二语言:bundled runtime 把运行时打进 SDK——「pip install 即用」让非 Node 生态零摩擦接入。

总结

这一章拆完了协议面:面向生态的 ACP、面向工具链的 JSON-RPC、面向 Python 的 SDK——三个表面共享同一棵树、同一套事件与同一套能力。第六部分(界面与协议)到此闭环。最后一章,我们把全书散落的赌注收拢起来:dsh 到底赌了什么,哪些模式值得带走,哪些代价它还没有付清。