16.3 ACP 协议深度解析

模型: claude-opus-4-6 (anthropic/claude-opus-4-6) 生成日期: 2026-02-18


前两节我们分析了 OpenCode 在 VSCode 和 Zed 中的集成方式——前者通过终端和 HTTP 实现轻量级通信,后者通过声明式配置和 ACP 协议实现深度集成。本节将深入剖析 ACP(Agent Client Protocol)的实现细节,这是 OpenCode 实现 IDE 集成的核心协议层。

16.3.1 ACP 协议简介

什么是 ACP

ACP(Agent Client Protocol)是一个标准化的协议,定义了 IDE(客户端)AI Agent(服务端) 之间的通信规范。它使用 @agentclientprotocol/sdk 库实现,通信格式基于 JSON-RPC(类似 LSP),传输层使用 stdio(标准输入/输出)。

ACP 定义了以下核心操作:

操作
方向
说明

initialize

Client → Agent

能力协商,交换版本和支持的功能

authenticate

Client → Agent

认证请求

newSession

Client → Agent

创建新对话 Session

loadSession

Client → Agent

加载已有 Session

prompt

Client → Agent

发送用户输入

cancel

Client → Agent

取消当前操作

sessionUpdate

Agent → Client

实时更新(消息、工具调用、进度等)

requestPermission

Agent → Client

请求用户批准敏感操作

ACP 与 MCP 的区别

衍生解释——ACP 与 MCP:两个互补的协议

读者在第 8 章已经详细了解了 MCP(Model Context Protocol)。ACP 和 MCP 是 AI Agent 生态中两个方向相反但互补的协议:

┌──────────┐       ACP        ┌──────────────┐       MCP        ┌──────────────┐
│   IDE    │ ◄──────────────► │  AI Agent    │ ◄──────────────► │  外部工具    │
│ (Client) │   IDE 是消费者   │  (中间层)     │  Agent 是消费者  │  (Provider)  │
│ Zed,     │   Agent 是提供者 │  OpenCode    │  Tool 是提供者   │  文件系统,   │
│ VSCode   │                 │              │                  │  数据库, API │
└──────────┘                 └──────────────┘                  └──────────────┘
  • MCP(Model Context Protocol):Agent 作为客户端,连接到外部工具/服务(如文件系统、数据库、Web API)。Agent 消费工具提供的能力。我们在第 8 章看到,OpenCode 通过 MCP 连接到 Grep、Glob、Read 等工具服务器。

  • ACP(Agent Client Protocol):IDE 作为客户端,连接到 AI Agent。IDE 消费 Agent 提供的能力——发送 prompt、接收回复、审批权限请求、查看工具调用进度。

两者的关系可以类比为:

  • MCP 就像 Agent 的"手"——它通过 MCP 操作外部世界

  • ACP 就像 Agent 的"脸"——它通过 ACP 与用户(通过 IDE)交互

一个完整的 AI 编程助手工作流是:用户通过 IDE(ACP Client)发送请求 → Agent 接收请求 → Agent 通过 MCP 调用工具完成任务 → Agent 通过 ACP 将结果返回给 IDE。

协议生命周期

一个完整的 ACP Session 生命周期如下:

16.3.2 acp/agent.ts:Agent 接口实现

acp/agent.ts 是整个 ACP 实现的核心文件,共 1677 行代码。它定义了 ACP.Agent 类,实现了 @agentclientprotocol/sdk 中的 ACPAgent 接口。

类结构与初始化

ACP 使用 TypeScript 的 namespace 组织代码,通过工厂模式暴露 init() 函数。核心成员变量:

成员
类型
作用

connection

AgentSideConnection

ACP SDK 提供的连接对象,用于向 IDE 发送消息

config

ACPConfig

配置信息,包含 SDK 实例和默认模型

sdk

OpencodeClient

OpenCode 的 SDK 客户端,用于调用内部 API

sessionManager

ACPSessionManager

ACP Session 管理器

eventAbort

AbortController

用于取消事件订阅的控制器

permissionQueues

Map<string, Promise<void>>

按 Session 排队的权限请求

permissionOptions

PermissionOption[]

固定的三个权限选项

构造函数在创建后立即调用 startEventSubscription() 启动事件监听循环。

initialize():能力协商

衍生解释——能力协商(Capability Negotiation)

能力协商是网络协议设计中的经典模式。当两个系统开始通信时,它们首先交换各自支持的功能列表,然后在后续通信中只使用双方都支持的功能。这种模式出现在许多协议中:

  • HTTP 内容协商:通过 AcceptContent-Type 头协商数据格式

  • TLS 握手:客户端和服务器协商加密算法

  • LSP:语言服务器和编辑器协商支持的语言特性

ACP 的能力协商告诉 IDE:

  • loadSession: true:Agent 支持加载已有 Session(实现了对话历史恢复)

  • mcpCapabilities: { http: true, sse: true }:Agent 支持 HTTP 和 SSE(Server-Sent Events)类型的 MCP 服务器

  • promptCapabilities: { embeddedContext: true, image: true }:Agent 支持嵌入式上下文引用和图片输入

  • sessionCapabilities: { fork: {}, list: {}, resume: {} }:Agent 支持 Session 的分叉、列表和恢复操作

initialize() 返回的能力集决定了 IDE 可以调用哪些后续方法。如果 Agent 不声明 loadSession: true,IDE 就不会尝试调用 loadSession() 方法。

newSession()loadSession():Session 管理

newSession() 的流程:

  1. 解析默认模型:调用 defaultModel() 确定使用哪个 LLM 模型

  2. 创建 Session:通过 sessionManager.create() 在 OpenCode 内部创建一个新 Session

  3. 加载 Session 模式:调用 loadSessionMode() 获取可用的模型列表、Agent 模式(如 "code"、"architect")和 MCP 服务器配置

  4. 返回 Session 信息:包括 sessionId、可用模型列表、可用模式列表

loadSession() 更加复杂,因为它需要重播历史消息

"重播"意味着 Agent 将历史消息逐条通过 sessionUpdate 发送给 IDE,IDE 据此重建对话界面。这使得用户可以在关闭 IDE 后重新打开时恢复之前的对话状态。

此外,ACP 还支持其他 Session 操作:

  • unstable_listSessions():列出所有 Session,支持分页(cursor-based pagination)

  • unstable_forkSession():分叉 Session——从某个对话节点创建分支,类似 Git 的分支

  • unstable_resumeSession():恢复 Session——重新加载状态并发送 usage 更新

注意 unstable_ 前缀——这些 API 尚在开发中,接口可能在后续版本中发生变化。

prompt():核心对话处理

prompt() 是 ACP Agent 最核心的方法,处理用户的每一次输入:

ACP 的 prompt 内容是多部分的(multi-part),每个部分可以是文本、图片、资源链接或嵌入式资源。OpenCode 需要将这些 ACP 格式的内容转换为自己内部的消息格式。

特别值得注意的是 audience 注解的处理:

ACP audience
OpenCode 内部标记
含义

["assistant"]

synthetic: true

合成消息,仅对 AI 可见(如系统指令)

["user"]

ignored: true

仅对用户可见,AI 不处理(如 UI 提示)

无或其他

无标记

正常消息,双方都可见

prompt() 还检测斜杠命令(/compact 等):

如果输入以 / 开头,它被解析为命令而不是普通 prompt。支持的命令包括自定义命令(通过 sdk.command.list() 获取)和内置命令(如 /compact 用于压缩 Session 上下文)。

最终 prompt() 返回使用量统计:

事件订阅系统

ACP Agent 的实时更新通过事件订阅机制实现。Agent 在构造时启动一个无限循环,监听 OpenCode 内部事件并转发给 IDE:

衍生解释——事件驱动架构与发布-订阅模式

事件驱动架构(Event-Driven Architecture, EDA)是一种软件设计范式,系统组件通过事件进行通信而非直接调用。ACP Agent 的事件订阅就是典型的发布-订阅(Pub/Sub)模式:

  • 发布者:OpenCode 核心系统(通过 sdk.global.event() 发布事件流)

  • 订阅者:ACP Agent(通过 for await...of 消费事件流)

  • 事件permission.askedmessage.part.updated

与直接函数调用相比,事件驱动的优势在于解耦——OpenCode 核心不需要知道谁在监听事件,ACP Agent 也不需要了解事件是如何产生的。这使得系统可以灵活地添加新的事件消费者(比如未来可能有其他 IDE 的扩展)。

事件处理器 handleEvent() 处理两大类事件:

权限请求事件(permission.asked

权限处理的关键设计:

  1. 按 Session 串行化permissionQueues 使用 Promise 链确保同一个 Session 的权限请求按顺序处理,避免并发冲突

  2. 三种选择:Allow once(允许一次)、Always allow(始终允许)、Reject(拒绝)

  3. 编辑预览:对于文件编辑权限,Agent 会将 diff 应用到文件并通过 writeTextFile 通知 IDE 显示文件变更。getNewContent() 函数使用 diff 库的 applyPatch() 将 unified diff 应用到原始文件内容上

消息部分更新事件(message.part.updated

这个事件在 LLM 生成回复时持续触发,ACP Agent 将其转换为 IDE 可理解的更新:

这里有几个值得关注的细节:

工具调用的生命周期映射:OpenCode 内部工具的四个状态(pending → running → completed/error)被精确映射到 ACP 的工具调用更新协议。IDE 可以据此显示工具执行的实时进度。

Todo 工具的特殊处理:当 todowrite 工具完成时,ACP Agent 将 todo 列表转换为 ACP 的 "plan" 更新,这让 IDE 可以在专门的面板中显示任务计划:

工具分类映射

toToolKind() 函数将 OpenCode 的工具名称映射到 ACP 的标准工具类别:

ACP 定义了六种标准工具类别:

ToolKind
含义
对应工具

execute

执行系统命令

bash

fetch

网络请求

webfetch

edit

文件编辑

edit, patch, write

search

搜索操作

grep, glob, context7_*

read

文件读取

list, read

other

其他

所有未匹配的工具

IDE 可以根据工具类别显示不同的图标和交互方式——例如 edit 类工具可能显示 diff 视图,execute 类工具可能显示终端输出。

toLocations() 函数则从工具的输入参数中提取文件路径,让 IDE 知道哪些文件正在被操作:

processMessage():历史消息重播

当用户恢复一个已有的 Session 时,ACP Agent 需要将历史消息"重播"给 IDE。processMessage() 函数处理了所有可能的消息部分类型:

文件类型的处理尤为精细——OpenCode 内部使用统一的 { type: "file", url, filename, mime } 格式存储文件,但 ACP 协议区分了多种内容类型。processMessage() 需要根据 URL 方案和 MIME 类型将文件转换为正确的 ACP 内容块:

URL 方案
MIME 类型
ACP 类型

file://

任意

resource_link(URI 引用)

data:

image/*

image(内联图片)

data:

text/*application/json

resource(文本资源)

data:

其他

resource(二进制 blob)

loadSessionMode():Session 模式加载

这是一个内部辅助方法,负责加载 Session 的完整上下文信息:

值得注意的第 4 步——IDE(如 Zed)可以在创建 Session 时传递自己的 MCP 服务器配置。OpenCode 会将这些外部 MCP 服务器注册到自己的 MCP 系统中,使 Agent 可以使用 IDE 提供的工具。这是 ACP 和 MCP 协同工作的一个精妙设计:IDE 不仅消费 Agent 的能力,还可以向 Agent 注入额外的工具。

defaultModel():模型选择策略

defaultModel() 函数实现了一套多级回退的模型选择策略:

这个六级回退策略确保了在各种配置场景下都能找到可用的模型。

衍生解释——适配器与外观模式

ACP.Agent 类本质上是一个适配器(Adapter)——它将 OpenCode 的内部 API 适配为 ACP 协议的标准接口。同时它也是一个外观(Facade)——它将 OpenCode 内部多个子系统(Session、Provider、Permission、Command、MCP 等)的复杂交互隐藏在一个简洁的接口后面。

  • 适配器模式:将一个类的接口转换为客户端期望的另一个接口。ACP 的 ToolKind 需要 "execute"、"edit" 等分类,而 OpenCode 内部用的是具体工具名称。toToolKind() 就是一个典型的适配器函数。

  • 外观模式:为子系统中的一组接口提供一个统一的高层接口。IDE 只需要与 ACP.Agent 交互,不需要了解 OpenCode 内部的 Session 管理、Provider 选择、Permission 处理等复杂逻辑。

16.3.3 acp/session.ts:ACP Session 到 OpenCode Session 的映射

acp/session.ts 实现了 ACPSessionManager 类,它是 ACP Session 与 OpenCode 内部 Session 之间的桥梁:

ACPSessionManager 维护了一个 Map<string, ACPSessionState>,以 session ID 为键存储 ACP 层的 Session 状态。它提供两种获取方式:

  • get():找不到时抛出 RequestError.invalidParams 异常(用于必须存在的场景)

  • tryGet():找不到时返回 undefined(用于可选检查的场景,如事件处理中检查事件是否属于当前管理的 Session)

注意 create() 方法使用 crypto.randomUUID() 生成 Session 标题——这确保每个 ACP Session 有唯一的可识别标题。

16.3.4 acp/types.ts:类型定义

acp/types.ts 定义了 ACP 层使用的核心类型:

ACPSessionState 是 ACP Session 的状态表示:

字段
类型
说明

id

string

Session 唯一标识符(与 OpenCode 内部 Session ID 相同)

cwd

string

工作目录

mcpServers

McpServer[]

IDE 传递的 MCP 服务器列表

createdAt

Date

创建时间

model?

{ providerID, modelID }

当前使用的模型(可选,可能使用默认)

variant?

string

模型变体(如 "high"、"low" 质量等级)

modeId?

string

Agent 模式 ID(如 "code"、"architect")

ACPConfig 是 ACP Agent 的配置:

字段
类型
说明

sdk

OpencodeClient

OpenCode SDK 客户端实例

defaultModel?

{ providerID, modelID }

命令行指定的默认模型

这两个类型很简洁,但它们定义了 ACP 层和 OpenCode 核心层之间的契约——ACPSessionState 包含了 ACP 需要但 OpenCode 内部 Session 不直接提供的信息(如 mcpServersvariant),而 ACPConfig 则封装了 Agent 初始化时的全局配置。

16.3.5 当前限制与未来规划

当前限制

  1. 认证未完全实现authenticate() 方法直接抛出异常:

当前的认证流程依赖 initialize() 中返回的 authMethods,引导用户通过终端执行 opencode auth login。IDE 可以检测到认证需求(通过 RequestError.authRequired()),但实际的认证流程在 ACP 协议之外完成。

  1. 权限串行化是每 Session 的:虽然 permissionQueues 实现了按 Session 的顺序处理,但跨 Session 的权限请求是并行的。如果多个 Session 同时请求权限,IDE 需要自行管理多个权限对话框的显示。

  2. 部分 API 标记为 unstablelistSessionsforkSessionresumeSession 都使用 unstable_ 前缀,表明接口可能在后续版本中变化。

  3. Zed URI 的特殊处理parseUri() 函数包含对 zed:// URI 方案的特殊处理,这是一个与特定 IDE 耦合的实现细节:

理想情况下,ACP 协议应该标准化 URI 方案,而不是在 Agent 端处理 IDE 特定的 URI 格式。

未来方向

  1. 协议版本演进:当前 protocolVersion 为 1,随着 ACP 规范的成熟,未来版本可能添加更丰富的能力——如内联代码建议(Inline Suggestions)、代码操作(Code Actions)、诊断信息(Diagnostics)等。

  2. 更丰富的权限模型:当前的三选项模型(Allow once / Always / Reject)较为简单。未来可能支持更细粒度的权限控制,如"允许读取但不允许写入"、"允许在特定目录内操作"等。

  3. 多 Agent 协同:当前每个 ACP 连接对应一个 Agent 实例。未来可能支持一个 IDE 连接多个 Agent,实现多 Agent 协同工作(类似 oh-my-opencode 的多 Agent 架构,但在协议层面标准化)。

16.3.6 本章总结

本章分析了 OpenCode 的三种 IDE 集成方案:

层次
方案
复杂度
功能深度

轻量级

VSCode 扩展(终端 + HTTP)

138 行代码

基本的上下文传递

声明式

Zed 扩展(TOML + ACP)

0 行代码

完整的 Agent 交互

协议层

ACP 实现(agent.ts + session.ts + types.ts)

~1800 行代码

Session、权限、工具、流式响应

三者形成了一个递进的架构:

VSCode 扩展直接与 TUI 通信(通过 HTTP),绕过了 ACP 层;Zed 扩展通过 ACP 协议与 Agent 通信,获得了更丰富的交互能力。两种方案都最终与 OpenCode 核心系统交互,只是路径不同。

ACP 协议代表了 AI Agent 与 IDE 集成的未来方向——标准化的协议使得任何支持 ACP 的 IDE 都可以无缝接入任何支持 ACP 的 Agent,就像 LSP 让任何编辑器都能支持任何语言的智能补全一样。OpenCode 的 ACP 实现虽然目前仍在迭代中,但已经展示了这种标准化集成的完整蓝图。

Last updated