能力协商、扩展位与协议演进
协议一定会变。ACP 的核心难题是:怎么加功能而不把现有实现搞挂。本章讲它的三层答案——能力协商、
_meta扩展位、版本与 feature gate。
1. 为什么需要能力协商
不是所有编辑器都支持终端,不是所有 agent 都支持加载历史。如果协议「要么全支持要么不合规」,生态就长不起来。
ACP 的办法:initialize 时双方互报能力(capabilities),之后只用对 方宣告支持的功能。这条贯穿全协议——几乎每个可选方法的文档都写着「only available if … capability」。
2. 三套能力
Agent 能力(agent.rs:3987 AgentCapabilities)
| 字段 | 宣告什么 |
|---|---|
loadSession | 是否支持 session/load |
promptCapabilities | prompt 里能收哪些非基线内容(图片/音频/内嵌资源) |
mcpCapabilities | 支持的 MCP 接入方式 |
sessionCapabilities | 会话生命周期能力(list/resume/close…) |
auth | 鉴权相关能力 |
Client 能力(client.rs:1732 ClientCapabilities)
| 字段 | 宣告什么 |
|---|---|
fs | 文件系统能力——决定 agent 能否请求 fs/read_text_file / fs/write_text_file |
terminal | 一个布尔:是否支持全部 terminal/* 方法(client.rs:1739) |
Prompt 能力(在 §3 的基线之上做加法)
PromptRequest 的注释把规则说死了:agent 必须支持 Text 和 ResourceLink;其它内容类型由 PromptCapabilities 选择性开启;而且 Client 必须按 agent 宣告的 prompt 能力来调整自己的界面(agent.rs:3268-3271)。这是双向契约,不是单方声明。
心智模型: 把能力协商想成「握手时交换的一张功能清单」,之后所有交互都先查这张清单——清单上没有的功能,谁都不准用。
3. _meta —— 无处不在的扩展逃生舱
几乎每个结构体末尾都有一个 _meta 字段(类型 Meta = serde_json::Map,ext.rs:15)。它的契约写在每处注释里:「ACP 保留 _meta 让 client 和 agent 附加额外元数据;实现绝不能对这些 key 的值做假设」(例如 agent.rs:73-77)。
这给了「在不改协议结构的前提下塞自定义数据」的官方口子。配套的还有 ExtRequest / ExtNotification(ext.rs),让实现能跑完全自定义的方法而仍是合法 ACP——ClientRequest::ExtMethodRequest 的 method 字段直接透传(agent.rs:5140)。
4. 版本策略:能力优先,版本号克制
ProtocolVersion 是个 u16,注释一句话定了基调:「只在破坏性变更时才 bump;非破坏的变化应通过能力引入」(version.rs:6-8)。
所以你看到的版本很少:
| 版本 | 状态 |
|---|---|
V0 | 预发布,基本当作不支持(version.rs:26-30) |
V1 | 当前稳定版(README 也确认「current stable ACP protocol version is 1」) |
V2 | unstable 草案,需显式开 unstable_protocol_v2 feature(version.rs:35-41) |
README 还区分了两个「版本」概念:线缆协议版本(initialize 里协商的 protocolVersion,决定消息形状)vs crate / schema 工件版本(影响 SDK 代码生成,但不一定改线缆)。两者可以一个变一个不变——消费者应只用协商出的 protocolVersion 判断线缆兼容性。
5. unstable feature gate:草案与稳定 schema 隔离
Rust 侧用 Cargo feature 把「还在迭代的协议草案」和「稳定面」隔开(agent-client-protocol-schema/Cargo.toml:24-50)。一堆 unstable_*:unstable_nes(下一步编辑建议)、unstable_elicitation、unstable_llm_providers、unstable_session_fork、unstable_mcp_over_acp…
源码里这些功能全被 #[cfg(feature = "unstable_…")] 包住(例如 agent.rs:4957 的 ListProvidersRequest),且文档统一加 UNSTABLE 警告(「not part of the spec yet, may be removed or changed at any point」)。
值得注意:unstable_protocol_v2 故意不在 unstable 总开关里(Cargo.toml:37-40),因为它引入一整个平行 v2 模块和(最终)不同的线缆版本,必须显式选入——避免有人「开了 unstable 就意外切到 v2」。
6. schema 怎么从 Rust 生成
这些类型都派生 JsonSchema(schemars),schema-generator/src/main.rs 把六个聚合 enum(AgentRequest/ClientRequest/…)组装成 AgentOutgoingMessage / ClientOutgoingMessage(main.rs:63-78),再吐出 schema/v1/schema.json 等工件,供 Kotlin/Java/Python/TS 等 SDK 生成各自的类型。每个请求/响应结构体上的 #[schemars(extend("x-side" = …, "x-method" = …))](如 agent.rs:55)把「这消息属于哪一侧、对应哪个方法」也写进 schema,给代码生成 器当元数据用。
7. 代码地图(本章)
| 主题 | 文件 | 符号 |
|---|---|---|
| Agent 能力 | …/src/v1/agent.rs | AgentCapabilities |
| Client 能力 | …/src/v1/client.rs | ClientCapabilities |
| Prompt 基线 + 能力规则 | …/src/v1/agent.rs | PromptRequest(prompt 字段注释) |
| 扩展位类型 | …/src/v1/ext.rs | Meta / ExtRequest / ExtNotification |
| 自定义方法透传 | …/src/v1/agent.rs | ClientRequest::ExtMethodRequest |
| 版本号与策略 | …/src/version.rs | ProtocolVersion |
| feature 列表 | agent-client-protocol-schema/Cargo.toml | [features](unstable_*、unstable_protocol_v2) |
| schema 生成入口 | schema-generator/src/main.rs | AgentOutgoingMessage / ClientOutgoingMessage |