Skip to content

所在组:互操作 | 上一组出口:能构建可追溯的检索链和更新路径 | 本组出口:能把一个编码 Agent 接进任意 ACP 编辑器,并说清它与 MCP/A2A 的分工 前置:协议地图 | MCP | 下一步:AG-UI | A2A

1. 概述 ​

消歧(先读这段):ACP 至少指四个东西。本页讲 Agent Client Protocol(agentclientprotocol.com),编辑器/IDE ↔ 编码 Agent 的开放协议,Zed 主导(站点 metadata 指向 zed.dev,2026-09-01 检索)。历史上 IBM 也提出过同名 Agent Communication Protocol,后并入 A2A 轨道(时间与细节未验证,见 A2A)。AGNTCY 生态里还有一个 Agent Connect Protocol(REST 调用远程 Agent 的接口,见 协议观察清单)。OpenClaw 内部另有一个同名私有协议(产品实现,见 Products)。检索或选型时永远先确认全称与 canonical URL。

结论先讲:Agent Client Protocol(ACP)标准化「代码编辑器 ↔ 编码 Agent」这一条边。没有它,每个编辑器要为每个 Agent 写一次定制集成;有了它,实现 ACP 的 Agent 能被所有 ACP 编辑器使用——官方原话:就像 LSP(Language Server Protocol)之于语言服务那样。它的信任模型很特别:Agent 是客户端的子进程(本地场景),文件系统、终端、权限审批全部由客户端把关,Agent 只是请求方。

心智模型:编辑器坐镇,Agent 是协作者 ​

  • 本地 Agent:作为编辑器子进程运行,JSON-RPC over stdio(换行分隔、UTF-8)。
  • 远程 Agent:云端或其他基础设施,HTTP / WebSocket(官方页提及;标准传输一节目前只规范 stdio,Streamable HTTP 是进行中的草案)。
  • 内容复用 MCP 的 JSON 表示(ContentBlock),并新增编码 UX 类型(如 diff 展示);用户可读文本默认 Markdown。文件路径必须绝对路径,行号 1-based。

协议生命周期(官方 Message Flow) ​

text
① initialize(协商 protocolVersion + 能力)→(如需)authenticate
② session/new 或 session/load(需要 loadSession 能力)
③ session/prompt ↔ session/update(消息块/计划/工具调用/命令更新)
   ├─ Agent → Client: session/request_permission(工具授权)
   └─ Client → Agent: session/cancel(打断当前 turn)
④ session/prompt 响应带回 stopReason(end_turn/max_tokens/max_turn_requests/refusal/cancelled)

能力协商规则:initialize 里省略的能力 = 不支持;新增能力不算破坏性变更;protocolVersion 是单个整数(当前 1),只在破坏性变更时递增。

何时使用 / 何时不用 ​

  • 用:给编码 Agent 做编辑器集成(或给编辑器做 Agent 支持);需要权限审批与文件/终端资源受控共享的本地 Agent 场景。
  • 不用:Agent ↔ 工具(→ MCP);Agent ↔ Agent 对等协作(→ A2A);Agent backend ↔ 前端应用的事件流(→ AG-UI)。

决策表:ACP 与相邻协议 ​

维度MCPACP(本页)A2A
方向host → 工具/数据编辑器 → 编码 Agent(client-server)Agent ↔ Agent(对等)
控制权host 拥有工具暴露客户端拥有环境:文件、终端、权限双方各自所有
状态server 会话session(cwd + mcpServers + 历史)Task/context 由 server 拥有
信任域host 信任的工具源用户桌面(Agent 是受控子进程)跨供应商/跨组织
最低复杂度工具 schema + 传输initialize + session + prompt turn发现 + 认证 + 异步任务

与 MCP 的协同(同一客户端、双协议):session/new 的参数就带 mcpServers 列表——编辑器把用户配置的 MCP 服务器交给 Agent 连接;Agent 侧声明 mcpCapabilities(http/sse)。官方还有 "MCP over ACP" 的 RFD 讨论稿(未定稿)。

历史版本里程碑 ​

protocolVersion 为整数主版本;v1 是当前稳定版。自 v1 发布以来,官方经 RFD 流程并入 15+ 个特性——session/resume(免重放重连)、session/close、session/list、session/delete、elicitation、additionalDirectories 等已陆续 stabilize(站点 announcements 页,2026-06 至 2026-07;未逐条核验)。

v2 已于 2026-07-20 进入 Draft(公告原文,2026-09-01 检索):schema 以 v2.0.0-alphaX 发布,五大主题——① 超越 turn:session/update 可在会话任意时刻发出,prompt 响应只表示「消息已被 Agent 确认」,不再是 turn 的终点;② 消息与工具调用按稳定 ID 流式更新、可替换(含撤回重发);③ diff 重构为结构化文件变更(add/delete/modify/move/copy + 二进制场景,可选 git_patch);④ 权限请求携带独立的 title/description 与可扩展 subject,不再硬绑 tool call;⑤ 枚举值接受 _ 前缀未知变体,默认前向兼容。官方对 Draft 的使用边界说得很清楚:各处仍会变,实现必须用版本协商 + feature flag 门控,稳定前不上生产;支持 v2 不等于弃 v1——v1-only 对端将长期存在,双版本并存是官方建议。更早的历史与发布日期:未验证。

本章 DoD 自检 ​

  • [ ] 15 分钟跑通 §2 的双进程 fixture(编辑器 ↔ Agent mock)
  • [ ] 能画出 initialize → session/new → prompt → update → 权限 → stopReason 的时序
  • [ ] 能说出客户端 cancel 后 Agent 必须返回哪个 stopReason
  • [ ] 能解释 ACP 里「文件/终端/权限在客户端」与 A2A「opaque 对等」的差异

2. 使用 ​

最小实战:两个零依赖 Node 脚本——一个 mock 编码 Agent(stdin/stdout JSON-RPC),一个 mock 编辑器(child_process 拉起它)。Node ≥ 18,无 npm 依赖,无 API key。

acp-agent.mjs(Agent 侧 mock:处理 initialize / session/new / session/prompt;中途向客户端发起 session/request_permission):

javascript
// acp-agent.mjs — ACP Agent mock:换行分隔 JSON-RPC over stdio(Node ≥ 18)
import { createInterface } from "node:readline";
import { randomUUID } from "node:crypto";

let nextId = 100;
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
const notify = (method, params) => send({ jsonrpc: "2.0", method, params });
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
const request = (method, params) =>
  new Promise((resolve) => {
    const id = nextId++;
    pendingRequests.set(id, resolve);
    send({ jsonrpc: "2.0", id, method, params });
  });
const pendingRequests = new Map();

let sessionId = null;
let cancelled = false;

const onUpdate = (update) =>
  notify("session/update", { sessionId, update });

const handlePrompt = async (id, text) => {
  cancelled = false;
  onUpdate({ sessionUpdate: "agent_message_chunk", messageId: randomUUID(),
    content: { type: "text", text: `分析请求:${text}` } });
  onUpdate({ sessionUpdate: "tool_call", toolCallId: "call_001",
    title: "Reading configuration file", kind: "read", status: "pending" });

  // Agent → Client:请求工具授权(客户端会转给用户)
  const perm = await request("session/request_permission", {
    sessionId,
    toolCall: { toolCallId: "call_001" },
    options: [
      { optionId: "allow_once", kind: "allow_once", name: "允许一次" },
      { optionId: "reject_once", kind: "reject_once", name: "拒绝一次" },
    ],
  });
  if (perm.outcome === "cancelled" || cancelled) {
    return reply(id, { stopReason: "cancelled" });
  }
  onUpdate({ sessionUpdate: "tool_call", toolCallId: "call_001",
    title: "Reading configuration file", kind: "read", status: "completed" });
  onUpdate({ sessionUpdate: "agent_message_chunk", messageId: randomUUID(),
    content: { type: "text", text: `完成(授权:${perm.optionId ?? "n/a"})。` } });
  reply(id, { stopReason: "end_turn" });
};

const methods = {
  initialize: (id) => reply(id, {
    protocolVersion: 1, // 版本协商:支持则原样返回
    agentCapabilities: {
      loadSession: false,
      promptCapabilities: { image: false, audio: false, embeddedContext: false },
    },
    agentInfo: { name: "mock-coding-agent", title: "Mock Agent", version: "0.1.0" },
    authMethods: [],
  }),
  "session/new": (id) => {
    sessionId = "sess_" + randomUUID().slice(0, 12);
    reply(id, { sessionId });
  },
  "session/prompt": async (id, { prompt }) => {
    const text = prompt.map((b) => b.text ?? "").join(" ");
    if (text.startsWith("wait")) {
      // 慢任务:演示客户端 session/cancel 打断
      onUpdate({ sessionUpdate: "agent_message_chunk", messageId: randomUUID(),
        content: { type: "text", text: "长时间分析中…" } });
      for (let i = 0; i < 20 && !cancelled; i++)
        await new Promise((r) => setTimeout(r, 100));
      return reply(id, { stopReason: cancelled ? "cancelled" : "end_turn" });
    }
    await handlePrompt(id, text);
  },
  "session/cancel": () => { cancelled = true; }, // 通知:无响应
};

createInterface({ input: process.stdin }).on("line", (line) => {
  if (!line.trim()) return;
  const msg = JSON.parse(line); // Agent 也会收到客户端对 request_permission 的响应
  if (msg.id !== undefined && msg.method === undefined) {
    pendingRequests.get(msg.id)?.(msg.result); // resolve 挂起的授权请求
    return;
  }
  methods[msg.method]?.(msg.id, msg.params ?? {});
});
process.stderr.write("[agent] ready on stdio\n"); // stderr 只用于日志

acp-editor.mjs(客户端 mock:spawn 子进程、走完整个生命周期 + 一次取消):

javascript
// acp-editor.mjs — ACP Client(编辑器)mock:spawn Agent 子进程(Node ≥ 18)
import { spawn } from "node:child_process";
import { createInterface } from "node:readline";

const child = spawn(process.execPath, ["acp-agent.mjs"], { stdio: ["pipe", "pipe", "inherit"] });
let nextId = 1;
const pending = new Map();

const send = (msg) => child.stdin.write(JSON.stringify(msg) + "\n");
const request = (method, params) =>
  new Promise((resolve) => {
    const id = nextId++;
    pending.set(id, resolve);
    send({ jsonrpc: "2.0", id, method, params });
  });
const notify = (method, params) => send({ jsonrpc: "2.0", method, params });

const onServerRequest = (msg) => {
  if (msg.method === "session/request_permission") {
    // 真实编辑器这里弹 UI 给用户;mock 自动选 allow_once
    const option = msg.params.options.find((o) => o.kind === "allow_once");
    send({ jsonrpc: "2.0", id: msg.id,
      result: { outcome: "selected", optionId: option.optionId } });
  }
};

const rl = createInterface({ input: child.stdout });
rl.on("line", (line) => {
  const msg = JSON.parse(line);
  if (msg.id !== undefined && msg.method) return onServerRequest(msg); // 服务器→客户端请求
  if (msg.method === "session/update") {
    const u = msg.params.update;
    console.log(`[update] ${u.sessionUpdate}${u.status ? `:${u.status}` : ""}` +
      `${u.content?.text ? ` ${u.content.text}` : ""}`);
    return;
  }
  pending.get(msg.id)?.(msg.result); // 响应:resolve 对应请求
});

// ① initialize → ② session/new → ③ prompt(含授权往返)
const init = await request("initialize", {
  protocolVersion: 1,
  clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: false },
  clientInfo: { name: "mock-editor", title: "Mock Editor", version: "0.1.0" },
});
console.log(`[init] agent=${init.agentInfo.name} v${init.protocolVersion}`);
const { sessionId } = await request("session/new", {
  cwd: process.cwd(), mcpServers: [], // MCP 协同点:把 MCP 服务器交给 Agent
});
console.log(`[session] ${sessionId}`);
const r1 = await request("session/prompt", {
  sessionId,
  prompt: [{ type: "text", text: "检查这个项目的配置问题" }],
});
console.log(`[turn1] stopReason=${r1.stopReason}`);

// ④ 取消路径:慢任务 300ms 后打断
const slow = request("session/prompt", {
  sessionId, prompt: [{ type: "text", text: "wait: 全库慢分析" }],
});
setTimeout(() => notify("session/cancel", { sessionId }), 300);
console.log(`[turn2] stopReason=${(await slow).stopReason}`);
child.kill();

运行命令:

bash
node acp-editor.mjs

正常输出:

text
[agent] ready on stdio
[init] agent=mock-coding-agent v1
[session] sess_9f0c…
[update] agent_message_chunk 分析请求:检查这个项目的配置问题
[update] tool_call:pending
[update] tool_call:completed
[update] agent_message_chunk 完成(授权:allow_once)。
[turn1] stopReason=end_turn
[update] agent_message_chunk 长时间分析中…
[turn2] stopReason=cancelled

负例(把 acp-editor.mjs 中 initialize 的 protocolVersion 改为 99):Agent 按规范返回它支持的版本(1),客户端若不支持则应断开并告知用户——mock 里 init.protocolVersion 打印为 1,即为协商证据。 验收命令:输出末两行为 stopReason=end_turn 与 stopReason=cancelled。 清理:Ctrl+C(若有残留),删除两个脚本。

场景矩阵 ​

场景输入 / 动作输出适用不适用
标准 turn普通文本 promptupdate 流 + end_turn日常问答/改码—
工具授权Agent 发 request_permission用户选择 allow/reject写文件、执行命令前只读分析(可不问)
打断prompt 后发 session/cancelstopReason=cancelled用户反悔/超时已结束的 turn
会话恢复session/load(重放历史,需 loadSession:true)或 session/resume(免重连重放,需 sessionCapabilities.resume)重放/恢复会话上下文跨重启续聊本 fixture 未实现
资源回调Agent 调 fs/read_text_file 等客户端代读写Agent 无直接磁盘权限时本 fixture 未实现

3. 原理 ​

双向 JSON-RPC 与角色不对称 ​

ACP 的两条方法轴(简表,字段以官方 schema 为准):

方向方法 / 通知说明
Client → Agentinitialize / authenticate / session/new / session/load / session/resume / session/close / session/list / session/delete / session/prompt / session/set_mode / logout生命周期与输入;load/resume/close/list/delete 均为能力门控,以 initialize 声明为准
Client → Agent(通知)session/cancel打断当前 turn,无响应
Agent → Clientsession/request_permission工具授权请求(请求-响应)
Agent → Client(通知)session/updateagent/user/thought 消息块、tool_call、plan、命令列表、模式变更
Agent → Client(能力门控)fs/read_text_file、fs/write_text_file、terminal/*、elicitation/create需 initialize 声明对应客户端能力

关键不变量:

  • stopReason 枚举:end_turn(模型自然结束)/ max_tokens / max_turn_requests / refusal / cancelled。取消不是错误:客户端 session/cancel 后,Agent 必须以 cancelled stopReason 响应 session/prompt,把底层库抛出的中断异常转成语义结果,否则客户端会把取消当错误弹给用户(官方 Warning 原意)。
  • 权限选项:allow_once / allow_always / reject_once / reject_always——「记住选择」由客户端落地,Agent 每次仍应请求。
  • 取消时序:客户端发出 cancel 后应抢先本地把未完成 tool_call 标为 cancelled,并对所有挂起的 request_permission 回 cancelled outcome;Agent 在响应 prompt 前仍可补发 update。
  • stdio 纪律:消息以 \n 分隔、不得内嵌换行;stdout 只准写 ACP 消息,日志走 stderr;双方都得遵守。
  • 能力=可用性:fs.readTextFile、terminal、elicitation、布尔配置项等能力省略即不支持;Agent 不得调用未声明能力。

数据流与 MCP 复用 ​

session/prompt 的 prompt 是 ContentBlock[](与 MCP ContentBlock 同构:text / resource / image / audio…),Agent 的输出 chunk 也是 ContentBlock。Agent 端能力 promptCapabilities 声明可接受的输入类型;基线是 Text 与 ResourceLink。会话对象携带 cwd 与 mcpServers——客户端决定工作目录和工具源,Agent 负责连接。

规范要求 vs 本地实测 ​

断言规范(agentclientprotocol.com)本地 fixture 实测
initialize 返回相同 protocolVersion(支持时)必须原样返回一致
省略能力 = 不支持必须一致(terminal:false 未被触碰)
cancel 后 stopReason=cancelled必须(含异常转换)一致
权限 outcome:selected+optionId / cancelled必须一致
stdout 只写 ACP 消息必须一致(日志走 stderr)
fs/*、terminal/* 回调需客户端能力声明fixture 声明了 fs 能力但未触发回调(未实测)
session/load 重放历史需 loadSession:true未实现

4. 开发 ​

集成要点 ​

  • 版本 pin:protocolVersion 整数协商——客户端发最新支持值,Agent 不同意则回自己最新值,客户端不支持就断开。发布前固定测试矩阵(你的编辑器版本 × Agent 版本)。
  • 权限策略:把 allow_always 的记忆放在客户端账户层并可视化(用户要能撤销);默认走 allow_once。
  • 超时:session/prompt 是长请求——客户端 UI 要可取消;Agent 对模型/工具调用要有自己的超时与 stopReason 归因。
  • stderr:Agent 日志写 stderr,编辑器转发/展示;不要混入 stdout。

调试 runbook ​

markdown
### 症状 → 证据 → 处理 → 完成标准
**症状**:编辑器里 Agent 没有任何输出
**证据**:子进程是否存活;stderr 是否有 Agent 日志;stdout 是否有非 JSON 行
**处理**:先查 spawn 失败/路径错误;再查 stdout 污染(框架 banner 打进 stdout 会破坏 JSON-RPC 流,必须重定向);用 rl 逐行 dump 原始行
**完成标准**:initialize 请求-响应在日志中成对出现;stderr 可见 Agent 启动日志
markdown
### 症状 → 证据 → 处理 → 完成标准
**症状**:点了取消但 UI 报错
**证据**:session/prompt 的最终响应是 error 还是 result.stopReason
**处理**:Agent 侧把中断异常捕获并转成 stopReason=cancelled(规范 Warning 场景);客户端侧对挂起的 request_permission 全部回 cancelled
**完成标准**:取消路径最终 result.stopReason=cancelled,无 error 弹窗
markdown
### 症状 → 证据 → 处理 → 完成标准
**症状**:Agent 调 fs/terminal 报 method not found
**证据**:initialize 响应里 clientCapabilities 是否声明
**处理**:编辑器补能力声明,或 Agent 改为在 prompt 里产出内容而非回调;能力协商失败要在 initialize 阶段暴露而非运行中
**完成标准**:能力清单与实际调用一致;未声明能力的调用不再发生

反模式清单 ​

  • 把 banner/进度条打进 Agent stdout——直接打断 JSON-RPC 流。
  • 客户端吞掉 session/update 只看最终 stopReason——丢掉流式 UX 与工具可见性。
  • 用 allow_always 当默认值且无撤销入口。
  • 在 Agent 进程内自行读写磁盘,绕过 fs/* 回调与权限模型。

5. 资料库 ​

四级阅读路线 ​

  • Beginner:官方 Introduction(LSP 类比、为什么需要 ACP)→ 本页 §1。
  • Builder:Protocol Overview(方法总表)→ Initialization / Session Setup / Prompt Turn → 本页 fixture 换 TypeScript/Rust 官方 SDK 重写。
  • Operator:Tool Calls(权限模型)→ File System / Terminals / Session Modes → Extensibility(_meta 与 _ 前缀自定义方法)。
  • Researcher:官方 JSON Schema → RFD 列表(mcp-over-acp、request-cancellation、session-fork 等,均为讨论稿)。

资源表 ​

名称层级canonical URL用途支持的断言下一步
ACP 官方文档L0https://agentclientprotocol.com/规范入口(Introduction/Protocol/Schema)本页生命周期、方法、stopReason、权限选项读 protocol/overview
Protocol SchemaL0https://agentclientprotocol.com/protocol/schema字段级 SSOTContentBlock/ToolCall/权限对象形状写类型时对照
TransportsL1https://agentclientprotocol.com/protocol/transportsstdio 规则(换行分隔/UTF-8/stderr)「stdout 只写 ACP 消息」实现自定义传输前读
Libraries(TS/Rust/Python/Kotlin)L1https://agentclientprotocol.com/libraries/typescript 等各页官方 SDK语言支持清单用 SDK 替换手写 JSON-RPC
ACP RegistryL2https://agentclientprotocol.com/get-started/registry兼容 Agent/客户端生态—选型时查兼容矩阵

以上条目均于 2026-09-01 检索(站点 sitemap 显示 protocol/v1/schema 更新于 2026-08-20)。

主动证伪与未决问题 ​

  • 证伪入口:本页任何方法/字段断言对照官方 schema 页;不一致以 schema 为准并提 issue。
  • 未决 1:Streamable HTTP 传输仍标「draft proposal in progress」,远程 Agent 部署形态未定稿。
  • 未决 2:治理/所有权细节(Zed 主导到什么程度、是否基金会化)在 governance 页,未逐字核验。
  • 未决 3:RFD(mcp-over-acp、session-resume、logout 等)均为讨论稿,不作为能力承诺。
  • 未决 4:v2 的稳定时间表未公布(Draft 自 2026-07-20 起);本页 fixture 与生命周期描述基于 v1 稳定面,接 v2 前先读官方迁移指南。

learn-ai 到此为止:协议契约、编辑器-Agent 边界、可运行 fixture。继续去哪:模型内部 → Learn LLM;具体编辑器/Agent 产品的接入命令 → Products;评估 → evals 站点。

为前端工程师打造 · 基于 VitePress 构建