DeepSeek Harness 源码解析 · 第 36 讲
DeepSeek Harness 源码解析系列 第 36 讲:ACP 自动化协议 基于 DeepSeek Harness 源码 · 2026-09-19 💡 本讲一句话: ACP(Agent Client Protocol)是 harness 的"自动化门面"——程序化客户端通过 stdin/stdout 上的 JSON-RPC 创建全新 agent、发文本 prompt、收已提交的 assistant 文本、按策略回答一次性权限请求;读完你能看懂为什么 stdout 被独占给协议帧、prompt 为什么要等整个 agent idle 才结算、以及拆除时为什么必须先 drain 子代理森林再释放顶层 agent。 一、ACP 是什么:和 SDK 的分工 上一讲我们看了 harness 自己的 SDK——那是"自家客户端驱动自家 runtime"。而 ACP(Agent Client Protocol,agentclientprotocol.com)是 开放协议 :任何实现了 Agent 侧的进程都能被任意 Client 驱动。DeepSeek Harness 在 packages/acp/acp/ 下实现了一个"仅面向自动化"的 Agent 侧服务器,模块注释(index.ts 第 1-9 行)把边界划得非常死: 🔹 桥接层只带四样东西: prompt 文本、已提交的 assistant 文本、取消、一次性权限决策。展示与人类交互(编辑器导航、transcript 回放、命令、模式选择器、提问)全部留在 harness 的 UI 模块里——README.zh.md 原话:"此包是传输适配器,而非 UI 集成或能力 seam"。 仓库里的主要客户端是 dsh-subagent-acp :父 harness 通过它 spawn 一个子进程 ACP agent,把"委托子代理"这件事跨进程化。整个包只有三个源文件—— index.ts(436 行)、codec.ts(66 行)、invariant.ts(30 行) ,表面积小得惊人,但每一处都有明确的取舍。 文件 职责 关键内容 src/index.ts(436 行) 桥接层主体:cordis 函数插件,把 ctx.agents 暴露成 ACP Agent 会话记录、事件路由、prompt 生命周期、权限桥、quiesce 拆除 src/codec.ts(66 行) 纯翻译层:harness 生命周期 ↔ ACP wire 词汇表 turnEndToStopReason、acpPromptToText、promptHasUnsupportedContent src/invariant.ts(30 行) 包级 invariant 伴生插件(仓库强制每个包都有) 空安装器 + "No runtime invariant" 理由:传输层不拥有持久事件流 还有一个容易被忽略的硬约束: stdout 专用于协议帧 。demo 应用(packages/examples/acp-demo)的诊断全部走 stderr,README 甚至把"兄弟插件可能污染 stdout"列为已知限制——因为一旦有非协议字节混进 stdout,JSON-RPC 管道就废了。 二、插件骨架:一个注入点 + 两个错误助手 先看 index.ts 的头部(第 42-77 行)。这是一个标准的 cordis 函数插件——命名导出 name / inject / Config / apply ,没有默认导出(仓库 postmortem 0001 专门记录过:混用两种形态会让 Loader 丢掉函数插件的命名空间): 📄 packages/acp/acp/src/index.ts (第 42-77 行) export const name = 'acp' // 插件名:cordis 组合图里的唯一标识 /** The bridge creates and owns agents; every other concern is carried by the agent composition. */ // 注释:桥接层只负责"创建并拥有 agent",其余全部交给 agent 自身的组合 export const inject = ['…