巧妙之处、边界与横向对比
本章是给「想把精华带走」的人:ACP 类型层最值得抄的几招、它刻意不做什么、以及它在协议货架上的位置。
1. 巧妙之处:为「前向兼容」专门造的 serde 管线
协议会演进,旧实现会收到带新字段、或带它读不懂的值的消息。处理不好,一个坏字段就能让整条消息反序列化失败、整个 agent 崩掉。ACP 在 serde 层做了三件事专门防这个——这是全仓库工程含量最高的部分。
招 1:MaybeUndefined<T> —— 区分「没这个 key」和「key 是 null」
Option<T> 只有两态,但 JSON 有三态:键缺失、键为 null、键有值。对「部分更新」语义这很要命:null 通常意为「清空」,缺失意为「不动」。ACP 造了三态枚举(serde_util.rs:368):
// 真实定义,serde_util.rs:368
pub enum MaybeUndefined<T> {
Undefined, // 键不存在 —— "不要动这个字段"
Null, // 键存在且为 JSON null —— "清空它"
Value(T), // 键有值 —— "设成这个"
}
用在 SessionInfoUpdate.title 这种地方(client.rs:241-245),注释直接写「Set to null to clear」——配合 skip_serializing_if = "MaybeUndefined::is_undefined",只有真正要改的字段才上线缆。这个设计借鉴自 async-graphql(serde_util.rs:14)。
招 2:DefaultOnError —— 坏字段不毒死整条消息
几乎每个可选字段都标了 #[serde_as(deserialize_as = "DefaultOnError")](例如 agent.rs:62-64 的 clientCapabilities)。语义:这个字段反序列化失败时,退回默认值,而不是让整条消息失败。
为什么重要:新版 agent 给 clientCapabilities 加了个旧 client 不认识的子结构、或值类型变了,旧 client 不会因此崩——它把不认识的能力当「没有」处理,继续跑。这是「老实现遇到新消息也别死」的关键。
招 3:VecSkipError —— 丢坏元素,不丢整列表
列表字段(如 prompt: Vec<ContentBlock>、mcpServers)标的是 DefaultOnError<VecSkipError<_, SkipListener>>(agent.rs:3279-3280)。语义:列表里某一项反序列化失败,就跳过那一项,保留其余——而不是整列表清空。
配套的 SkipListener(serde_util.rs:29-58)是个观测钩子:开了 tracing feature 时,每丢一个坏元素就 tracing::warn! 一条;不开时它直接是 (),serde_with 提供的 no-op,零运行时开销。既要容错、又要可观测、还不想给默认路径加负担——三者兼顾。
一句话总结这三招: 它们共同实现了协议的「鲁 棒性原则」——严格地生成、宽容地接收(be strict in what you send, liberal in what you accept)。新增能力 / 新增内容类型 / 字段演进,旧实现都能不崩地降级处理。
2. 其它值得一看的小设计
- 强类型 id 而非裸字符串。
SessionId、ToolCallId、PermissionOptionId、TerminalId都是 newtype 包Arc<str>(如v1/mod.rs:55)。Arc让 id 到处克隆几乎免费,newtype 防止把 session id 误传给要 tool call id 的地方。 #[non_exhaustive]几乎遍布所有类型。 这让上游以后给 enum 加变体、给 struct 加字段时,不算 Rust API 的破坏性变更——和「能力优先、少 bump 版本」的演进哲学一致。- 错误码沿用 JSON-RPC + 少量自定义。 标准码(
-32700ParseError…)加上 ACP 自己的AuthRequired(-32000)、ResourceNotFound(-32002)等(error.rs:162-219);未知码落到ErrorCode::Other(i32)而非报错——又是「宽容接收」。
3. 边界与局限(诚实)
- 这个仓库不含运行时。 没有事件循环、没有传输实现、没有
Agent/Clienttrait——那些在上层agent-client-protocolcrate(lib.rs:12-16明说)。本文讲「协议形状」,不是「怎么跑一个 agent」。 - 远程 agent 仍是 work in progress。 协议设计上支持本地(stdio)和远程(HTTP/WebSocket),但 introduction 明说远程支持「正在推进中」(
introduction.mdx:31-35)。当下成熟路径是本地子进程 + stdio。 - 信任模型是「本地、可信模型」。 设计前提是「你在编辑器里跟一个你信任的模型对话」(
architecture.mdx:14)。权限请求给的是 UX 层的「人在回路」确认,不是对抗恶意 agent 的沙箱;真正的隔离要靠编辑器自己。 - 大量功能仍是 unstable。 NES、elicitation、provider 配置、session fork、MCP-over-ACP、v2 等都在 feature gate 后,随时可能变(各处的 UNSTABLE 警告)。生产只应依赖稳定面。
4. 横向对比
和 LSP
ACP 是自觉地照着 LSP 学的(introduction.mdx:17):同样基于 JSON-RPC、同样「一次实现接整个生态」、同样用 $/ 前缀标记「可忽略的实现相关通知」。区别是 LSP 标准化「编辑器 ↔ 语言智能」,ACP 标准化「编辑器 ↔ 自主编码 agent」——后者多了双向请求(agent 反向要文件/终端/授权)和流式回合(prompt turn)。
和 MCP(Model Context Protocol)
两者互补,不竞争。MCP 标准化「LLM/agent ↔ 工具与数据源」;ACP 标准化「编辑器 ↔ agent」。ACP 刻意复用 MCP 的内容类型(ContentBlock 兼容 MCP,content.rs:30),好让 agent 把 MCP 工具输出原样转发。架构上常见组合:编辑器把它配置的 MCP 服务器透传给 agent,让 agent 直连(architecture.mdx:26-34);编辑器自己要导出工具时,可起一个小代理把 MCP 隧道回自己。
┌── ACP ──┐ ┌── MCP ──┐
编辑器 ◀──────────▶ Agent (LLM) ◀──────────▶ 工具 / 数据源
(本协议) "编排者" (MCP 协议)
一句话:ACP 管「人和编辑器怎么驱动 agent」,MCP 管「agent 怎么够到工具」。 同一个 agent 常常一头说 ACP、一头说 MCP。