跳转到内容

08 - ACP 协议与 JSON-RPC 声明式路由

学习目标

  • 理解 ACP(Agent Client Protocol)是什么,以及它与 MCP 协议的差异
  • 掌握 Goose 的两种 ACP 传输模式:HTTP/WebSocket 与 Stdio
  • 理解 #[custom_methods] 过程宏如何生成 handle_custom_request 分发器和 custom_method_schemas JSON Schema 生成器
  • 掌握方法名匹配机制 <RequestType>::matches_method 的工作方式
  • 识别方法名冲突、类型不匹配、未知方法处理等常见陷阱
  • 评估过程宏路由 vs 手动匹配、ACP JSON-RPC vs REST API 等设计取舍

前置知识

  • JSON-RPC 2.0 协议(method、params、id、result、error)
  • Rust 过程宏(proc_macro)基础
  • Axum HTTP 框架与 WebSocket 升级
  • 参考:/common/api-gateway/04-acp-jsonrpc-routing(ACP 路由专题)
  • 参考:/common/mcp/01-overview(MCP 协议与生态集成)

项目实践

ACP 协议概述

ACP(Agent Client Protocol)是 Goose 内部使用的 JSON-RPC 2.0 协议,用于客户端(Goose CLI 或 Electron UI)与 Agent 后端(goosed 服务器)之间的通信。ACP 在 MCP 协议之上扩展了会话管理、配置管理、工具调用、资源管理等 Agent 专属能力。

ACP 与 MCP 的核心差异

特性MCPACP
定位模型-工具协议,标准化 AI 模型与外部工具的交互Agent-客户端协议,标准化客户端与 Agent 后端的交互
传输Stdio / HTTP / SSEHTTP + WebSocket(goose serve)/ Stdio(goose acp
核心方法tools/callresources/readprompts/getsession/promptsession/loadsession/new + 扩展方法
扩展机制工具/资源/提示注册JSON-RPC custom_methods + #[custom_methods] 过程宏
会话模型无状态(工具调用)有状态(session 生命周期:新建、加载、关闭、分叉)

两种传输模式

HTTP/WebSocket 模式(goose serve

通过 goose serve 命令启动后端服务,在 axum Router 上挂载 /acp 路由:

GET /acp → WebSocket 升级 或 HTTP 事件流
POST /acp → JSON-RPC 请求处理
DELETE /acp → 连接清理
GET /health, /status → 健康检查

传输层关键细节:

  • WebSocket 连接通过 Acp-Connection-IdAcp-Session-Id 头部标识
  • HTTP POST 请求以 application/json 提交 JSON-RPC 请求
  • 支持 SSE 事件流(text/event-stream)用于服务端推送
  • CORS 层暴露 Acp-Connection-IdAcp-Session-Id 头部

Stdio 模式(goose acp

通过 goose acp 命令以 Stdio 传输启动,进程的标准输入读取 JSON-RPC 请求,标准输出写入响应。适用于嵌入式场景(如被其他 IDE 或编辑器进程启动)。

#[custom_methods] 过程宏

Goose 的 ACP 扩展方法通过 goose-acp-macros crate 中的 #[custom_methods] 过程宏实现声明式路由。

使用方式

custom_dispatch.rs 中,#[custom_methods] 标记一个 impl 块,块内的方法通过 #[custom_method(RequestType)] 注解声明为 JSON-RPC 方法处理器:

#[custom_methods]
impl GooseAcpAgent {
#[custom_method(AddExtensionRequest)]
async fn dispatch_add_extension(
&self,
req: AddExtensionRequest,
) -> Result<AddExtensionResponse, agent_client_protocol::Error> {
self.on_add_extension(req).await
}
#[custom_method(GetExtensionsRequest)]
async fn dispatch_get_extensions(
&self,
) -> Result<GetExtensionsResponse, agent_client_protocol::Error> {
self.on_get_extensions().await
}
// ... 更多方法
}

生成的代码

过程宏在编译期生成两个方法,追加到 impl 块中:

1. handle_custom_request 分发器

async fn handle_custom_request(
&self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, agent_client_protocol::Error> {
// 为每个 #[custom_method(RequestType)] 生成一个 if 分支
if <AddExtensionRequest as JsonRpcMessage>::matches_method(method) {
let req = serde_json::from_value(params)
.map_err(|e| Error::invalid_params().data(e.to_string()))?;
let result = self.dispatch_add_extension(req).await?;
return serde_json::to_value(&result)
.map_err(|e| Error::internal_error().data(e.to_string()));
}
if <GetExtensionsRequest as JsonRpcMessage>::matches_method(method) {
let result = self.dispatch_get_extensions().await?;
return serde_json::to_value(&result)
.map_err(|e| Error::internal_error().data(e.to_string()));
}
// 所有方法都不匹配时
Err(Error::method_not_found())
}

2. custom_method_schemas Schema 生成器

pub fn custom_method_schemas(
generator: &mut schemars::SchemaGenerator,
) -> Vec<CustomMethodSchema> {
vec![
CustomMethodSchema {
method: <AddExtensionRequest as JsonRpcMessage>::method(&Default::default()).to_string(),
params_schema: Some(generator.subschema_for::<AddExtensionRequest>()),
params_type_name: Some("AddExtensionRequest".to_string()),
response_schema: Some(generator.subschema_for::<AddExtensionResponse>()),
response_type_name: Some("AddExtensionResponse".to_string()),
},
// ... 每个 custom_method 对应一个 entry
]
}

方法名匹配机制

每个请求类型实现 agent_client_protocol::JsonRpcMessage trait,其中 matches_method(&self, method: &str) -> bool 用于运行时方法名匹配。

请求类型通过 derive 宏获得 #[request(method = "goose/extensions/add")] 属性,方法名在编译期从类型属性中提取,消除了请求结构体与处理器之间的方法名重复。

方法分类

Goose 的自定义方法涵盖多个功能域:

功能域方法示例说明
扩展管理goose/extensions/add动态添加/删除/启用 MCP 扩展
工具调用goose/tools/call直接调用 Agent 工具
资源读取goose/resources/read读取扩展提供的资源
会话管理goose/sessions/delete删除/重命名/归档/分叉会话
配置管理goose/config/extensions/*持久化扩展配置开关
Provider 管理goose/providers/*列举/创建/更新/删除 LLM Provider
偏好设置goose/preferences/*读写用户偏好
默认配置goose/defaults/*读写默认配置
导入导出goose/sessions/import会话/源的导入导出
听写goose/dictation/*语音听写模型管理

请求分发管线

完整的请求从客户端到处理器的流:

关键流转步骤:

  1. Transport 层接收原始 JSON,解析为 JSON-RPC 格式(区分 request / notification / response)
  2. MatchDispatchFrom 链式匹配标准 ACP 方法(initialize、new_session、prompt 等)
  3. 标准方法未匹配时进入 .otherwise() 分支,调用 dispatch_custom_request
  4. dispatch_custom_request 透传 method 字符串和 params JSON Value
  5. handle_custom_request(过程宏生成)遍历所有 matches_method 分支
  6. 匹配到后反序列化 params,调用对应 handler,序列化返回值
  7. 均未匹配时返回 Error::method_not_found()

问题与规避

方法名冲突

如果两个不同的 RequestType 注册了相同的 method 字符串,先注册的会优先匹配,后注册的永远不会被调用。

规避措施

  • 方法名在 #[request(method = "...")] 属性中编译期定义,而非运行时注册
  • 代码审查时注意检查方法名唯一性
  • custom_method_schemas 生成的 Schema 列表可用于运行时检测方法名冲突

类型不匹配

Handler 参数的类型必须与客户端发送的 params JSON 结构兼容。如果 serde_json::from_value 反序列化失败,会返回 Error::invalid_params()

规避措施

  • 请求类型必须实现 Deserialize(通过 serde derive)
  • #[request(method = "...")] 属性确保类型同时实现 JsonRpcRequest / JsonRpcMessage
  • 反序列化错误信息包含具体原因,通过 Error.data() 传递给客户端

未知方法处理

handle_custom_request 中所有 matches_method 分支均不匹配时,返回 Error::method_not_found()。这是 JSON-RPC 2.0 标准错误码。

规避措施

  • 客户端应预先通过 custom_method_schemas 获取可用方法列表
  • 服务端返回 method_not_found 时客户端应记录警告日志

过程宏编译错误

#[custom_methods] 对 handler 签名有严格要求:

  • 返回类型必须是 Result<T, agent_client_protocol::Error>
  • 参数除了 &self 外最多一个(自动从 JSON params 反序列化)
  • #[custom_method(RequestType)] 中的 RequestType 必须实现 JsonRpcMessage

规避措施:编译期过程宏会尽早暴露签名不匹配问题,不会出现运行时 panic。

设计取舍

过程宏路由 vs 手动 match

方案优势劣势
过程宏路由(Goose 选择)声明式:方法名从类型属性提取,无重复
编译期检查:签名不匹配在编译时报错
自动生成 JSON Schema
宏代码调试困难;错误信息可能不够直观
手动 match代码透明,易于调试方法名在类型定义和 match 分支两处重复,容易不一致;需要手动维护 Schema

Goose 选择过程宏路由,核心动机是消除方法名重复。在拥有 30+ 自定义方法的项目中,手动维护 method 字符串与 handler 的对应关系极易出错。

ACP JSON-RPC vs REST API

方案优势劣势
JSON-RPC(Goose 选择)统一端点(/acp),方法名自描述
原生支持通知(notification)和批量请求
与 MCP 生态兼容(同为 JSON-RPC 家族)
不支持 REST 风格的资源路径语义
REST API资源语义清晰,HTTP 方法映射自然需要多个路由;Agent 操作(prompt、cancel)不是纯 CRUD,REST 语义牵强

ACP 选择 JSON-RPC 是因为 Agent 操作本质上是远程过程调用而非资源 CRUD。session/prompt(发送提示等待回复)、session/cancel(取消当前任务)等操作天然适合 RPC 语义。同时与 MCP 协议保持一致,降低学习成本。

链式分发 vs 集中式路由表

Goose 使用 MatchDispatchFrom 链式匹配(.if_request(...).await.if_request(...).await.otherwise(...)),而非集中式路由表。

优势

  • 每个处理器可以决定是否响应、是否 spawn 异步任务
  • 部分处理器(如 InitializeRequest)内联运行以确保状态初始化完成
  • 部分处理器(如 PromptRequest)spawn 后台任务后立即返回,不阻塞后续请求

取舍:链式分发在方法数量较少时清晰高效,但方法超过一定数量(当前 30+)后线性扫描可能成为瓶颈。未来可考虑引入哈希表路由。

参考来源

  • crates/goose-acp-macros/src/lib.rs#[custom_methods] 过程宏实现
  • crates/goose/src/acp/server/dispatch.rs — MatchDispatchFrom 请求分发链
  • crates/goose/src/acp/server/custom_dispatch.rs — custom_methods 使用示例,30+ 自定义方法路由
  • crates/goose/src/acp/transport/mod.rs — HTTP/WebSocket 传输层路由
  • /common/api-gateway/04-acp-jsonrpc-routing — ACP 路由专题文档
  • /common/mcp/01-overview — MCP 协议与生态集成