从 v1 迁移
面向熟悉 v1 的实现者的 ACP v2 完整指南:破坏性变更、新功能和分步迁移
ACP v2 是一个整合版本。它重新设计了提示生命周期并允许更灵活的会话状态模式,统一了流式和非流式更新,使 schema 默认向前兼容,并移除了生态系统已经弃用的协议面。
迁移并不意味着放弃 v1。仅支持 v1 的 Agent 和客户端在一段时间内仍将常见,因此实现者应该同时支持两个版本:按连接协商版本,保持 v1 支持正常工作,并在功能标志后面添加 v2 直到它稳定。参阅同时支持 v1 和 v2 了解如何构建此结构。
如果你只记住五件事,请记住这些:
session/prompt响应不再结束轮次。它确认接受。前台进度和完成以state_update通知到达,停止原因也移到了那里。- 更新是 upsert。消息、工具调用和计划按 ID 修补,具有统一的语义:省略字段 = 不变,null = 清除,值 = 替换,分块追加。
- 客户端文件系统、终端执行和会话模式 API 已移除。Agent 拥有的终端输出是单独的仅显示 v2 面;当 Agent 需要客户端工具时,使用客户端提供的 MCP 服务器。
- 能力已重组。双方各有一个
capabilities+ 必需的info字段,会话作用域组嵌套在session下,对象支持标记代替布尔值,以及必需的基线会话方法。 - 现在一切都是可扩展的。枚举和标签联合接受未知值。
_前缀的值是你的,其余保留给未来的 ACP 版本。
范围:稳定 v2 和草案面
本指南描述稳定 v2 基线(schema/v2/schema.json)。v2 协议面整体仍标记为草案,因此在稳定之前,请将 v2 支持置于显式版本协商和功能标志之后。
不稳定 schema(schema/v2/schema.unstable.json)在该基线之上叠加了可选的草案功能。协商 protocolVersion: 2 不 意味着它们中的任何一个。与 v1 一样,在每个功能后面加上其自己的能力或功能标志。
版本协商
机制未变:客户端在 initialize 中发送其支持的最新协议版本,Agent 如果支持则以相同版本响应,否则以其自己的最新版本响应。要使用 v2,发送 "protocolVersion": 2。仅支持 v1 的 Agent 将以 "protocolVersion": 1 回答,客户端决定是否继续使用 v1 或断开连接。
将 v2 支持视为增量。添加 v2 时继续为 protocolVersion: 1 对等方提供服务:放弃 v1 的 Agent 会切断与现有客户端的联系,放弃 v1 的客户端会失去对现有 Agent 的访问。每方根据协商的版本按连接选择其 v1 或 v2 面。
v2 的任何内容都不会改变底层 JSON-RPC 帧,因此单个连接在 initialize 之后始终只说一种协商版本。
概览
方法变更
| v1 方法 | v2 |
|---|---|
initialize | 名称不变。参数和结果已重构 |
authenticate | 重命名为 auth/login |
logout(由 agentCapabilities.auth.logout 门控) | 重命名为 auth/logout。authMethods 非空时必需;无 logout 能力标记 |
session/new | mcpServers 现在可选。响应不再包含 modes |
session/load | 已移除。使用 session/resume 配合 "replayFrom": { "type": "start" } |
session/resume | 新增可选 replayFrom。必需(无能力标记) |
session/list | 必需。无能力 |
session/close | 必需。无能力 |
session/delete | 不变。仍通过 session.delete 能力可选 |
session/prompt | 形状不变。响应语义已重新设计(参阅提示生命周期) |
session/cancel | 名称不变。完成现在通过 state_update 报告而非提示响应 |
session/set_mode | 已移除。使用 session/set_config_option |
session/set_config_option | 不变 |
session/request_permission | 名称不变。参数已重构(必需 title,可选 subject) |
session/update | 名称不变。更新变体已更改(见下) |
fs/read_text_file, fs/write_text_file | 已移除 |
terminal/create, terminal/output, terminal/release, terminal/wait_for_exit, terminal/kill | 已移除 |
$/cancel_request | 不变 |
session/update 变体变更
| v1 sessionUpdate | v2 |
|---|---|
user_message_chunk | 保留。messageId 现在必需 |
agent_message_chunk | 保留。messageId 现在必需 |
agent_thought_chunk | 保留。messageId 现在必需 |
| - | 新增:user_message、agent_message、agent_thought 整条消息 upsert |
| - | 新增:前台 state_update(running、idle、requires_action)附带停止原因 |
tool_call | 已移除。toolCallId 的第一个 tool_call_update 创建工具调用 |
tool_call_update | 保留。现在是显式 upsert,具有省略/null/值补丁语义 |
| - | 新增:tool_call_content_chunk 用于流式传输单个内容项目 |
| - | 新增:terminal_update 用于 Agent 拥有的显示终端状态和重放 |
| - | 新增:terminal_output_chunk 用于追加显示终端字节 |
plan | 替换为 plan_update,带 planId 和 type 判别字段 |
current_mode_update | 已移除。模式是配置选项。使用 config_option_update |
available_commands_update | 保留。命令输入现在带有必需的 type 判别字段 |
config_option_update | 不变 |
session_info_update | 不变 |
usage_update | 不变 |
初始化
角色无关的 info 和 capabilities
v1 使用角色特定的字段名:客户端发送 clientCapabilities 和可选的 clientInfo,Agent 返回 agentCapabilities 和可选的 agentInfo。v2 在两个方向使用相同的两个字段名(capabilities 和 info),且 info 现在双方都必需。
{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": 1,
"clientCapabilities": {
"fs": { "readTextFile": true, "writeTextFile": true },
"terminal": true
},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}
{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": 2,
"info": {
"name": "my-client",
"title": "My Client",
"version": "1.0.0"
},
"capabilities": {}
}
}
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": 1,
"agentCapabilities": {
"loadSession": true,
"promptCapabilities": { "image": true, "embeddedContext": true },
"mcpCapabilities": { "http": true, "sse": false },
"sessionCapabilities": {
"list": {},
"resume": {},
"close": {}
}
},
"authMethods": [],
"agentInfo": { "name": "my-agent", "version": "0.3.0" }
}
}
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": 2,
"info": {
"name": "my-agent",
"title": "My Agent",
"version": "0.3.0"
},
"capabilities": {
"session": {
"prompt": {
"image": {},
"embeddedContext": {}
},
"mcp": {
"stdio": {},
"http": {}
},
"delete": {},
"additionalDirectories": {}
}
},
"authMethods": []
}
}
以上两个响应都有空的 authMethods 数组。在 v2 中,省略 authMethods 具有相同的可用性含义:Agent 不声明认证面,客户端 不得 调用 auth/login 或 auth/logout。
支持标记是对象,不是布尔值
在 v1 中,能力支持是布尔值("image": true)和对象("list": {})的混合。在 v2 中,每个支持标记都是对象:提供 {}(或带字段的对象)表示支持,省略键或提供 null 表示不支持。
这意味着像 promptCapabilities.image === true 的检查变为存在性检查:capabilities.session.prompt.image != null。对象形状为每个能力增长字段或通过 _meta 扩展留出了空间,无需另一次破坏性变更。
能力重组
所有会话作用域能力组现在位于 capabilities.session 下。v1 的 agentCapabilities.promptCapabilities 现在是 capabilities.session.prompt,agentCapabilities.mcpCapabilities 现在是 capabilities.session.mcp。
capabilities.session 本身是可选的,因此不进行基于会话提示的 Agent(例如,仅提供专用扩展面的 Agent)可以完全省略它。目前还没有对此的协议支持,但有几个实验正在进行中,我们正在确保非会话组件可以通过 capabilities 表达。
loadSession 随 session/load 一起移除(参阅会话设置)。
单独的 list、resume 和 close 标记已移除:声明 capabilities.session 现在要求支持基线方法 session/new、session/list、session/resume、session/close、session/prompt、session/cancel 和 session/update。可选额外功能如 session.delete、session.additionalDirectories、session.prompt 和 session.mcp 保留各自的标记。
v1 客户端能力 fs 和 terminal 已完全移除(参阅客户端文件系统和终端执行)。稳定 v2 目前未定义标准客户端能力字段。Agent 拥有的终端显示是基线行为,不是客户端执行能力。
认证
认证方法归组在 auth/ 前缀下:
authenticate->auth/login。参数不变(methodId选择一个声明的认证方法)。描述符的标识符字段从id重命名为methodId以匹配,其类型判别字段现在是必需的。logout->auth/logout。在 v1 中,logout 支持通过能力标记可选。在 v2 中,在authMethods中返回一个或多个有效条目即声明了认证面,Agent 必须 同时实现auth/login和auth/logout。如果authMethods被省略或为空,客户端 不得 调用任一方法。没有 logout 支持标记;capabilities.auth保持正交,仅声明认证相关扩展。
{
"jsonrpc": "2.0",
"id": 1,
"method": "auth/login",
"params": { "methodId": "agent-login" }
}
认证方法
authMethods 中的每个条目现在使用 methodId 而非 id,并带有必需的类型判别字段,以便可以在不破坏现有实现的情况下引入未来的认证流程:
{
"methodId": "agent-login",
"type": "agent",
"name": "Agent login",
"description": "Sign in using the agent's login flow"
}
在 initialize 响应中包含此条目要求 Agent 同时支持 auth/login 和 auth/logout。
稳定 v2 定义了 type: "agent"。自定义类型 必须 以 _ 开头。不带前导 _ 的未知值保留给未来的 ACP 版本。
新的提示生命周期
这是 v2 中最重要的语义变更。即使你略读其余部分,也要阅读本节。简而言之,v1 通过待处理的 session/prompt 请求表达的一切都移到了 session/update 通知中:
| 前台信号 | v1 | v2 |
|---|---|---|
| 提示已接受 | 隐式 | session/prompt 响应 ({}) |
| 历史中的用户消息 | 隐式(请求本身) | user_message 更新,带 Agent 拥有的 messageId |
| 前台工作运行中 | session/prompt 仍待处理 | state_update,state: "running" |
| 前台工作等待 | 隐式(待处理权限请求) | state_update,state: "requires_action" |
| 前台工作结束 | session/prompt 响应带 stopReason | state_update,state: "idle" 和 stopReason |
| 取消已确认 | 提示响应带 stopReason: "cancelled" | 空闲 state_update 带 stopReason: "cancelled" |
v1:响应即轮次
在 v1 中,session/prompt 在整个轮次期间保持待处理。Agent 在工作时流式传输 session/update 通知,最终的响应携带结束轮次的 stopReason:
{ "jsonrpc": "2.0", "id": 2, "result": { "stopReason": "end_turn" } }
这将提示接受与轮次完成纠缠在一起,使得重放、多客户端会话、后台工作和排队消息难以表达。
v2:响应是确认
在 v2 中,Agent 必须 在接受提示后立即以空结果响应 session/prompt:
{ "jsonrpc": "2.0", "id": 2, "result": {} }
其他一切都通过 session/update 通知发生:
用户消息确认。 接受提示后,Agent 必须 报告用户消息在会话历史中的插入位置,要么作为带完整内容数组的 user_message 更新,要么作为流式 user_message_chunk 更新。此更新是 Agent 拥有的 messageId 的事实来源:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "sess_abc123",
"update": {
"sessionUpdate": "user_message",
"messageId": "msg_user_8f7a1",
"content": [{ "type": "text", "text": "Can you analyze this code?" }]
}
}
}
运行状态。 当前台工作开始或恢复时,Agent 必须 发送带 "state": "running" 的 state_update。
输出。 消息、思考、计划和工具调用像以前一样流式传输(具有以下部分描述的变更)。
完成。 当 Agent 准备好处理新提示时,它 必须 报告空闲。当转换结束前台工作时,它 必须 包含停止原因:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "sess_abc123",
"update": {
"sessionUpdate": "state_update",
"state": "idle",
"stopReason": "end_turn"
}
}
}
会话状态
state_update 是 v2 中的新功能,使用三种稳定状态之一(像所有 v2 枚举一样可扩展)报告前台工作:
running:前台工作正在进行中。idle:Agent 准备好处理新提示。当转换结束前台工作时携带stopReason。requires_action:前台工作被用户操作阻塞。Agent 应该 在等待权限响应时发送此状态,并在工作恢复时发送另一个running更新。
后台活动可以继续并在 Agent 报告空闲时发出其他 session/update 通知。这些通知不会改变状态。
停止原因本身与 v1 相比不变(end_turn、max_tokens、max_turn_requests、refusal、cancelled)。它们只是从 session/prompt 响应移到了空闲 state_update,并描述前台工作停止的原因。
取消
session/cancel 作为通知不变,但确认随生命周期的其余部分移动:Agent 不再以 "stopReason": "cancelled" 响应 session/prompt,而是 必须 完成发送任何待处理更新,然后发送带 cancelled 停止原因的空闲 state_update。客户端仍然以 cancelled 结果响应所有待处理的 session/request_permission 请求,并 应该 接受在发送 session/cancel 后到达的工具调用更新。
为什么这不仅仅是簿记问题
因为前台进度现在完全以通知表达,相同的消息流适用于 session/resume 上的历史重放、多个客户端观察一个会话,以及未来的 Agent 发起或排队工作。
消息和消息 ID
消息 ID 是必需的
v2 中的每个消息分块和消息更新 必须 携带 messageId。在 v1 中,messageId 在分块上是可选的。消息 ID 是 Agent 生成的不透明字符串:Agent 拥有会话历史,因此它是消息身份的唯一来源。一条消息的所有分块共享一个 messageId。messageId 变化表示新消息。
整条消息 upsert
除了分块更新外,v2 还添加了 user_message、agent_message 和 agent_thought 更新,携带完整内容数组(分块携带单个内容块)。这些是以 messageId 为键的 upsert,具有三态补丁语义:
- 省略 content 时保留现有内容不变(适用于在不重发内容的情况下更新
_meta或其他字段)。 content: null或content: []清除消息内容。- 具体数组替换该消息当前存储的所有内容,包括从先前分块累积的内容。
分块始终追加到当前内容。例如:agent_message 带 content: [A],然后 agent_message_chunk 带 B 渲染为 [A, B]。后续带 content: [C] 的 agent_message 替换为 [C];先前的完整内容和分块被替换。后续分块追加到 [C]。
会话设置
session/load 已移除
session/load 已从协议中移除。要加载会话并重放历史,使用带 replayFrom 的 session/resume:
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/resume",
"params": {
"sessionId": "sess_abc123",
"cwd": "/home/user/project",
"mcpServers": [],
"replayFrom": { "type": "start" }
}
}
当包含 replayFrom 时,Agent 必须 在响应请求之前通过 session/update 通知重放完整对话。这与旧的 session/load 语义一致。
不带 replayFrom 的 session/resume 在不重放的情况下恢复会话,与 v1 的 session/resume 一致。
session/resume 现在是必需的
在 v1 中,session/resume 是可选的(通过能力标记)。在 v2 中,它是必需的基线方法。不需要恢复会话的 Agent 可以在收到 session/resume 请求时返回错误,但仍 必须 实现该方法。
客户端文件系统和终端执行
v1 的客户端文件系统方法(fs/read_text_file、fs/write_text_file)和终端执行方法(terminal/create、terminal/output、terminal/release、terminal/wait_for_exit、terminal/kill)已完全移除。
需要客户端工具的 Agent 应使用客户端提供的 MCP 服务器。Agent 拥有的终端输出现在是 v2 中的单独仅显示面,通过 terminal_update 和 terminal_output_chunk 会话更新报告。
工具调用
tool_call 已移除,tool_call_update 创建工具调用
在 v1 中,tool_call 通知创建工具调用,后续的 tool_call_update 更新它。在 v2 中,tool_call 已移除。toolCallId 的第一个 tool_call_update 创建工具调用。
tool_call_update 是显式 upsert
v2 使补丁语义显式化:
- 省略字段保留先前值不变
null清除值- 具体值替换先前值
content和locations作为整个数组替换
新增:tool_call_content_chunk
用于流式传输单个内容项目,客户端追加每个项目而非替换整个 content 集合。
新增:终端更新
terminal_update 和 terminal_output_chunk 用于 Agent 拥有的仅显示终端状态和实时输出。
计划
plan 通知替换为 plan_update,带 planId 和 type 判别字段。计划条目以完整列表发送,客户端完全替换计划内容。
同时支持 v1 和 v2
实现者应该同时支持两个版本:
- 按连接协商:在
initialize中,发送你支持的最新版本。如果对等方以不同版本响应,决定是否继续或断开连接。 - 保持 v1 正常工作:不要放弃 v1 支持。现有的客户端和 Agent 仍将使用 v1。
- 在功能标志后添加 v2:在 v2 稳定之前,将其置于显式功能标志之后。
- 按连接选择面:根据协商的版本,使用 v1 或 v2 方法面。单个连接在
initialize之后始终只说一种版本。 - 共享代码:在可能的地方共享 JSON-RPC 帧、传输和消息路由代码。版本特定的逻辑隔离在版本协商和处理层中。