数据截至 (上游 commit 6fb43a175495)
Agent Client Protocol(ACP):把 agent 当子进程
这一章讲 CodeCompanion 的第二条接入路。前面四章的对象都是「发 HTTP 给一个模型 API」;ACP 完全不同——它把一个已经是 agent 的 CLI 程序(Claude Code、Codex、Gemini CLI…)拉起 来当子进程,用 JSON-RPC 跟它对话。
1. 要解决的小问题:有些 agent 不是「模型」,是「程序」
Claude Code、Codex 这类工具本身就是完整的 agent——它们有自己的工具、自己的循环、自己的权限模型。你不会想用 HTTP 重新实现一遍它们的能力。
ACP(Agent Client Protocol) 是个标准协议(agentclientprotocol.com),让编辑器(client)和这些 agent 通过 stdin/stdout 上的 JSON-RPC 通信。CodeCompanion 当 client,把 agent 进程接进同一个聊天 buffer——对用户来说,和 Claude Code 对话的体验,和跟 Anthropic API 对话长得一样。
注:
acp/init.lua顶部注释致谢「Inspired by Zed's ACP implementation patterns」(acp/init.lua:11)。
2. 顶层:HTTP 路 vs ACP 路
01 章的 submit 末尾按 adapter.type 分流(chat/init.lua:1405):
Chat:submit
│
adapter.type?
├── "http" ──▶ _submit_http ──▶ http.lua Client ──▶ curl 发 REST 请求
│
└── "acp" ──▶ _submit_acp ──▶ acp/handler ──▶ JSON-RPC 写进子进程 stdin
│
session/update 通知 ◀──────┘ (流式回话、工具调用、权限请求)
两条路最终都把内容写回同一个聊天 buffer,共用 01 章的 buffer 逻辑。
3. 连接的生命周期
Connection(acp/init.lua:58)管一个 agent 子进程的整条命。它得先「握手」才能聊:
spawn 子进程(adapter.commands.default,如 "claude-agent-acp")
│
▼
initialize ── 交换协议版本 + 能力(agentCapabilities)
│
▼
authenticate ── 如 Claude Code 用 CLAUDE_CODE_OAUTH_TOKEN
│
▼
session/new ── 拿到 session_id
│
▼
现在可以 prompt 了(is_connected 全绿,acp/init.lua:107)
is_connected(acp/init.lua:107)要求四个条件同时满足:进程句柄在、已 initialize、已 authenticate、有 session_id。connect_and_authenticate(acp/init.lua:119)负责把这条链跑通。
Claude Code 的适配器(adapters/acp/claude_code.lua)就是一张配置表:启动命令(commands.default = { "claude-agent-acp" })、auth 方式(handlers.auth 读 OAuth token,:51)、client 能力(clientCapabilities.fs 声明能读写文件,:33)。还有一个 yolo 命令变体加 --yolo 跳过审批(:19)。
4. 发一轮 prompt:PromptBuilder 的流式回调
_submit_acp(chat/init.lua:1297)把活交给 ACP handler,底层是 PromptBuilder(acp/prompt_builder.lua)——一个流式 API,你挂一串回调,它把 agent 推来的各类 session/update 通知分发到对应回调(prompt_builder.lua:37):
| 回调 | agent 推来了什么 |
|---|---|
on_message_chunk | 一段回话文本 |
on_thought_chunk | 一段思考过程 |
on_tool_call / on_tool_update | agent 要调/更新一个工具的状态 |
on_permission_request | agent 请求权限(等价 HTTP 路的审批框) |
on_write_text_file | agent 要写文件(client 代为执行) |
on_plan | agent 给出的计划 |
on_complete / on_error / on_cancel | 收尾 |
这段演示「挂回调发 prompt」的用法(# 示意,非源码):
PromptBuilder.new(connection, messages)
:on_message_chunk(function(text) chat:add_buf_message(text) end) -- 文本上屏
:on_tool_call(function(call) render_tool(call) end) -- 渲染工具调用
:on_permission_request(function(req) ask_user(req) end) -- 弹审批
:on_complete(function() chat:ready_for_input() end)
:send()
和 HTTP 路最大的不同:工具执行发生在 agent 进程里,不在 CodeCompanion 里。CodeCompanion 只负责渲染工具调用、在 agent 请求权限时弹框、以及在 agent 要读写文件时代为操作(因为 client 声明了 fs 能力)。02/03 章那套工具系统在 ACP 路上不参与。
5. 底层:JSON-RPC over stdio
通信是行分隔的 JSON-RPC。Connection 维护一个 IdGenerator(给请求编号)和 LineBuffer(把 stdout 字节流切成完整 JSON 行),都来自 utils/jsonrpc(acp/init.lua:99)。请求-响应靠 pending_responses / _pending_callbacks 按 id 配对;agent 主动推的 session/update 是无 id 的通知,直接路由到当前 _active_prompt 的回调。
本章对
acp/init.lua的握手与状态字段读得完整,但没有逐一追踪每个 JSON-RPC method 的收发实现(该文件约 1005 行,本文档读了前 120 行 + 关键符号)。完整 method 常量见acp/methods.lua。(inferred:method 路由细节)
6. 巧妙之处
- 同一个聊天 buffer 装两种后端:用户感知不到「这是 API 还是子进程 agent」,因为两条路都收口到 01 章的 buffer 渲染。
- 能力协商:
initialize时交换agentCapabilities,client 也声明自己能干什么(读写文件),双方据此决定谁干哪部分——比写死假设更稳。 - 权限请求统一成「问用户」:不管是本地工具审批还是远端 agent 的 permission_request,最终都落到同一种「弹框问人」的交互。
7. 边界与局限
- 依赖外部 CLI 已安装且在 PATH 里(如
claude-agent-acp);装不上就用不了。 - agent 进程崩了或协议不匹配,连接整条失效,得重连。
- 工具/编辑的真正执行在 agent 侧,CodeCompanion 的 7 级模糊匹配(03 章)在这条路上用不上——edit 由 agent 自 己做。
8. 横向对比
HTTP 路是「我自己当 agent,模型只出脑子」;ACP 路是「别人已经是 agent,我只当它的 UI/宿主」。前者把 agent 智能握在自己手里(工具、edit、审批全自研),后者复用成熟 agent 的全部能力但让出控制权。CodeCompanion 两条都支持,是它区别于「纯 API 聊天插件」和「纯 agent CLI」的定位:一个既能自营、也能托管别家 agent 的编辑器宿主。
9. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 连接生命周期 | lua/codecompanion/acp/init.lua | Connection.new、Connection:connect_and_authenticate、Connection:is_connected |
| 流式 prompt | lua/codecompanion/acp/prompt_builder.lua | PromptBuilder.new、PromptBuilder:send、各 on_* 回调 |
| JSON-RPC 方法 | lua/codecompanion/acp/methods.lua | (method 常量表) |
| Claude Code 适配器 | lua/codecompanion/adapters/acp/claude_code.lua | (整表)handlers.auth |
| 聊天侧分流 | lua/codecompanion/interactions/chat/init.lua | Chat:_submit_acp |