跳到主要内容

两个角色、双向请求与路由 enum

本章回答两个「为什么」:为什么 ACP 要分「两套方法」而不是一套?SDK 收到一坨 JSON 后,怎么知道该路由到哪个处理函数?

1. 双向,不是单向

传统「客户端→服务端」是单向的。ACP 不是——两边都能发起请求。这是它和很多协议的关键区别,也是「UX-first」的必然结果:agent 要改文件、跑命令、问用户授权,都得反过来求编辑器。文档直说:「ACP 用 JSON-RPC 的双向请求,让 agent 能向编辑器发请求,例如请求工具调用的权限」(architecture.mdx:24)。

于是方法天然分成两组,按「谁实现/谁处理」划分:

Client (编辑器实现这些) Agent (agent 实现这些)
──────────────────────── ────────────────────────
session/update (流式输出) initialize
session/request_permission (要授权) authenticate / logout
fs/read_text_file session/new · load · resume
fs/write_text_file session/list · delete · close
terminal/create · output · kill session/set_mode
terminal/release · wait_for_exit session/prompt
▲ ▲
└──── Agent 调这些(反向) ───────────────┘ Client 调这些(正向)

2. Agent 侧方法 —— 编辑器调,agent 实现

方法名是协议层稳定的常量(agent.rs:4881-4917),挑核心的:

线缆方法名干什么
initialize握手协商
authenticate / logout登录 / 登出
session/new · session/load · session/resume建 / 重放加载 / 续上会话
session/list · session/delete · session/close列举 / 删除 / 关闭会话
session/set_mode切换 agent 模式(ask/architect/code…)
session/prompt发起一轮对话(核心)
session/cancel取消当前轮(通知)

命名规约(AGENTS.md:6-18):线缆名 noun/verb 时,Rust 侧用 verb_noun。例如线缆 session/new → 结构体 NewSessionRequest;线缆 terminal/output(noun/noun)→ TerminalOutputRequest。这解释了为什么类型名和线缆名看起来「调了个顺序」。

3. Client 侧方法 —— agent 反向调,编辑器实现

这些让 agent 能借编辑器的「手脚」干活(client.rs:2305-2322):

线缆方法名干什么
session/updateagent 往 UI 流式推进度(通知,非请求)
session/request_permission敏感操作前问用户要授权
fs/read_text_file · fs/write_text_file借编辑器读写文件(受 fs 能力位控制)
terminal/create · output · release · wait_for_exit · kill借编辑器开/读/收终端(受 terminal 能力控制)

为什么让 agent 通过编辑器读写文件,而不是自己 open?因为编辑器可能有未保存的缓冲区、有权限边界,且这样 UI 能「跟随」agent 的改动。ReadTextFileRequest 还支持 line/limit 分段读(client.rs:1002-1016),CreateTerminalRequest 支持 outputByteLimit 并规定「超限从头截断、且必须在字符边界截」(client.rs:1156-1163)——都是为真实编辑器体验设计的细节。

4. 核心机制:路由 enum 怎么把 JSON 分发对

它要解决的小问题。 进程从 stdin 收到一行 JSON,里面是个 {method, params}。SDK 怎么把它反序列化成对的 Rust 类型,再交给对的处理函数?

思路。 ACP 给每个「方向 × 种类」定义了一个聚合 enum,把该方向所有可能的消息列全。收到消息时,serde 按 untagged 一个个试着反序列化,命中即是。一共六个(三种 × 两方向):

方向RequestResponseNotification
Client→AgentClientRequestAgentResponseClientNotification
Agent→ClientAgentRequestClientResponseAgentNotification

ClientRequest 就是「所有 client 能发给 agent 的请求」的全集(agent.rs:4930),每个变体的文档注释直接就是该方法的语义说明。它标了 #[serde(untagged)](agent.rs:4926)——序列化时不加外层 tag,反序列化时挨个试。

原理演示(示意,非源码): 把「分发」想成这样一段伪逻辑——

# 示意,非源码:SDK 收到一条 JSON-RPC 后大致怎么用聚合 enum 分发
def handle_incoming(raw): # raw 是收到的一行 JSON
msg = parse_json(raw)
if "id" in msg and "method" in msg: # 有 id + method = 请求
req = ClientRequest.deserialize(msg["params"], method=msg["method"])
result = dispatch_to_handler(req) # 按变体类型调对应处理器
send(Response.result(msg["id"], result))
elif "method" in msg: # 只有 method,没 id = 通知
note = ClientNotification.deserialize(msg["params"])
dispatch_notification(note) # 通知不回响应
# 重点看:enum 变体 → 处理函数 是一对一的,路由表就藏在 enum 定义里

真实实现。 每个聚合 enum 都配一个 method(),把变体映射回线缆方法名(agent.rs:5107-5142 ClientRequest::method),这就是「类型 ↔ 方法名」的权威对照表;反过来 SDK 也靠它给出去的消息填 method 字段。文档明说这些 enum「内部用于路由 RPC,你通常不用直接碰」(agent.rs:4920-4922)。

5. 关键细节:协议自身的方法不走能力门,但走 $/ 约定

绝大多数可选方法受能力位控制(见 04 章)。但有一类「协议级通知」是所有实现都该认的,它们以 $/ 打头(如 $/cancel_request)。约定是:收到 $/ 打头却处理不了的通知,可以直接忽略——比如单线程同步语言根本无法响应取消(protocol_level.rs:84-92)。这是从 LSP 借来的成熟约定。

6. 代码地图(本章)

主题文件符号
Agent 方法名常量表…/src/v1/agent.rsAGENT_METHOD_NAMES
Client 方法名常量表…/src/v1/client.rsCLIENT_METHOD_NAMES
Client→Agent 请求全集…/src/v1/agent.rsClientRequest / ClientRequest::method
Agent→Client 响应全集…/src/v1/agent.rsAgentResponse
Agent→Client 请求全集…/src/v1/client.rsAgentRequest
Agent→Client 通知全集…/src/v1/client.rsAgentNotification
Client→Agent 通知全集…/src/v1/agent.rsClientNotification(含 CancelNotification)
反向文件读写…/src/v1/client.rsReadTextFileRequest / WriteTextFileRequest
反向终端…/src/v1/client.rsCreateTerminalRequest
协议级 $/ 通知…/src/v1/protocol_level.rsProtocolLevelNotification