文章

【笔记】ACP 与 Claude Code Agent SDK 的控制链路

【笔记】ACP 与 Claude Code Agent SDK 的控制链路

1. 一句话心智模型

把 Claude Code 接入编辑器时,实际存在两层协议桥接:ACP 统一编辑器与 Coding Agent 的交互,Claude Agent SDK 再把 Agent 能力映射到 Claude Code CLI 子进程。

flowchart LR
    A[Editor / IDE<br/>ACP Client] <-->|ACP JSON-RPC| B[claude-agent-acp<br/>ACP Agent / Proxy]
    B <-->|Agent SDK API| C[Claude Agent SDK]
    C <-->|stream-json + control messages| D[Claude Code CLI]

这条链路最重要的不是消息转发,而是控制权转移:

  • 编辑器拥有 UI、工作区和最终用户交互。
  • ACP Agent 负责把统一协议翻译成具体 Agent SDK。
  • Claude Agent SDK 管理 CLI 进程并暴露异步消息流。
  • Claude Code CLI 负责模型循环、工具执行和内部权限判断。

工具请求授权时,决策需要从最内层一路传到编辑器,再沿原路返回。

2. 先区分三种接口

2.1 ACP:编辑器与 Coding Agent 的标准协议

Agent Client Protocol(ACP)把编辑器称为 Client,把 Coding Agent 进程称为 Agent。它类似 LSP 的定位:编辑器无需为每一种 Agent 单独实现会话、流式消息、工具调用和权限 UI。

ACP 定义的是:

  • 初始化与能力协商。
  • Session 的创建、加载、提示和取消。
  • Agent 向 Client 流式报告消息、工具调用、计划和模式变化。
  • Agent 反向请求文件、终端和用户权限。

2.2 Claude Agent SDK:宿主程序调用 Claude Code 的编程接口

@anthropic-ai/claude-agent-sdk 对外提供 query() 等 API。调用方传入 prompt、工具、MCP、权限模式和回调,再通过 async iterator 消费 Agent 消息。

它不是 Anthropic Messages API 的薄封装。它运行的是 Claude Code Agent:包含 Agent loop、内置工具、CLAUDE.md、hooks、MCP 和会话恢复等能力。

2.3 Claude Code control protocol:SDK 与 CLI 的内部控制消息

SDK 需要控制独立的 Claude Code CLI 进程。普通 assistant 消息和工具事件通过 stream-json 输出;权限、模式变更和中断等双向操作则使用 control_requestcontrol_response 等内部消息。

这套消息是当前实现锚点,不是 ACP,也不是承诺稳定的公共 API。外部集成应依赖 Agent SDK,而不是直接仿造 control message。

3. ACP 的连接和 Session 模型

3.1 stdio 上的双向 JSON-RPC

本地编辑器通常按需启动 Agent 子进程:

1
2
3
4
5
6
7
Editor                              Agent
  │                                  │
  ├── spawn process ────────────────►│
  ├── stdin: JSON-RPC requests ─────►│
  │◄─ stdout: JSON-RPC responses ────┤
  │◄─ stdout: notifications ─────────┤
  │                                  ├── stderr: logs

stdio transport 使用换行分隔的 JSON-RPC 消息。每条消息压成一行,stdout 只能承载协议数据;日志应写到 stderr,否则一行普通文本就可能破坏协议解析。

JSON 字符串里的换行会被转义成 \n,因此“消息不能包含原始换行字节”和“内容可以表达多行文本”并不矛盾。

一条 ACP connection 可以承载多个 Session。连接是进程和传输通道,Session 才是独立的对话与工作目录上下文。

3.2 初始化负责协商,不创建对话

连接建立后,Client 先调用 initialize。双方交换协议版本、Client 能力和 Agent 能力。

能力协商很重要,因为部分反向方法是可选的:

  • Client 可以支持 fs/read_text_filefs/write_text_file
  • Client 可以支持 terminal/createterminal/outputterminal/wait_for_exit 等终端方法。
  • 新版本还可能协商更细的 MCP transport 或 elicitation 能力。

Agent 不能仅凭协议版本假定所有 UI 和环境能力存在,必须依据 initialize 结果降级。

3.3 稳定 v1 的 Session 主链路

当前稳定 v1 的核心 Agent 方法是:

方法作用
session/new基于 cwd、额外目录和 MCP 配置创建 Session
session/load恢复已有 Session,并通过 update 重放历史
session/prompt向 Session 发送一轮内容,等待本轮 stop reason
session/cancel取消正在执行的 prompt turn;它是 notification
session/set_mode修改 Agent 暴露的会话模式

原始材料中提到的 session/closesession/resume 不属于当前稳定 v1 方法集。恢复历史由 session/load 承担;资源释放由 Agent 实现和进程生命周期处理,不能编造一个不存在的标准方法。

3.4 session/update 是统一输出流

Agent 使用 session/update notification 把本轮增量变化推给 Client,内容可以包括:

  • Agent、用户或 thought 消息块。
  • 工具调用的创建和状态更新。
  • 执行计划。
  • 可用命令变化。
  • 当前模式或配置变化。

session/prompt 的 response 只需给出本轮 stopReason。丰富的中间过程通过 notification 流传送,Client 不必等整个 Agent turn 结束后再渲染。

session/load 也复用这条 update 流重放历史。这样实时输出和恢复渲染使用同一个 reducer,不需要两套消息模型。

4. 为什么 Agent 会反向调用 Client

ACP 不是简单的 Client request / Agent response。编辑器掌握用户界面和工作区环境,所以 Agent 也需要调用 Client。

4.1 权限请求

稳定 v1 要求 Client 实现 session/request_permission。Agent 提交:

  • 当前 sessionId
  • 待授权的 toolCall
  • 若干 PermissionOption

标准 option kind 包含:

  • allow_once
  • allow_always
  • reject_once
  • reject_always

Client 可以展示对话框,也可以依据组织策略自动选择。返回结果要么是用户选中了某个 option,要么是当前 turn 被取消。

权限“由 Client 展示”不等于“Agent 放弃安全判断”。Agent 仍可先应用自身的 deny 规则;Client 决策是额外授权入口,不应覆盖不可绕过的本地安全边界。

4.2 文件系统请求

Agent 可以请求 Client 读写文本文件。这样远端或沙箱中的 Agent 不必假设自己和编辑器拥有完全相同的文件系统视图。

这些方法是 capability-gated。Client 没声明能力时,Agent 应使用自己的工具或报告不支持,而不是照发请求。

4.3 终端请求

Client 也可以为 Agent 创建和管理终端。终端生命周期被拆成 create、output、wait、kill、release,是因为“启动命令”“读取增量输出”“等待退出”和“释放资源”不是同一个动作。

这让编辑器可以把真实终端展示给用户,也能在权限与资源策略下托管命令。但具体 Agent 是否使用 Client terminal,取决于适配器;Claude Code 自身也有内置 Bash 工具,两者不能混为一谈。

5. claude-agent-acp 怎样桥接 Agent SDK

@agentclientprotocol/claude-agent-acp 同时扮演两个角色:对编辑器是 ACP Agent,对 Claude Agent SDK 是宿主应用。

5.1 创建 Session

收到 session/new 后,适配器收集:

  • cwd 和 additional directories。
  • Client 传入的 MCP servers。
  • 当前模式、模型和 Claude Code 专有选项。
  • Client 是否支持 form elicitation 等能力。

它再为这个 ACP Session 创建或准备 Agent SDK query。ACP sessionId 是外层路由键,SDK/Claude Code 还可能拥有自己的 session ID;适配器负责维护对应关系。

5.2 Prompt 与 update 翻译

收到 session/prompt 后,适配器把 ACP ContentBlock[] 转成 SDK 接受的输入,消费 SDK async iterator,并把不同 SDKMessage 翻译为 ACP update:

flowchart LR
    A[ACP ContentBlock] --> B[SDK prompt]
    B --> C[SDKMessage stream]
    C --> D{message type}
    D --> E[agent message chunk]
    D --> F[tool call / update]
    D --> G[plan / command / mode]
    E --> H[session/update]
    F --> H
    G --> H

适配层必须保留关联 ID。工具调用的开始、参数、结果和权限请求都要落在同一个 ACP toolCallId 上,否则编辑器无法更新原来的工具卡片。

5.3 加载 Session

session/load 让适配器恢复 Claude Code 会话,并把历史重新映射成 ACP updates。返回值本身不携带完整历史;历史回放是 load 期间的通知副作用。

因此 Client 必须在等待 load response 的同时继续消费 notification,不能认为“response 到了才开始有历史”。

6. 权限如何穿过四层

权限桥接是这条架构最能说明问题的一条调用链:

sequenceDiagram
    participant CLI as Claude Code CLI
    participant SDK as Agent SDK
    participant Proxy as claude-agent-acp
    participant IDE as ACP Client

    CLI->>SDK: control_request(can_use_tool)
    SDK->>Proxy: canUseTool(tool, input, context)
    Proxy->>IDE: session/request_permission
    IDE-->>Proxy: selected option / cancelled
    Proxy-->>SDK: allow / deny
    SDK-->>CLI: control_response

6.1 canUseTool 是 SDK 侧的桥接点

Agent SDK 允许宿主提供 canUseTool callback。Claude Code 即将执行需要外部判断的工具时,SDK 在宿主进程内调用它。

claude-agent-acp 的 callback 不直接弹 UI,而是构造 ACP session/request_permission,让编辑器展示统一权限界面。用户选择后,适配器再把 ACP option 转成 SDK 的 allow 或 deny 结果。

函数本身没有跨进程传输。跨进程的是结构化请求与响应:

  • CLI 到 SDK:control_request
  • SDK 进程内:调用 JavaScript canUseTool
  • SDK 到 CLI:control_response

6.2 三层判断各自负责什么

层次负责的判断
Claude Code内置权限模式、settings rules、路径安全、hooks 等本地约束
claude-agent-acp把工具和候选决策翻译成 ACP 请求,处理特殊交互
ACP Client应用组织策略并展示最终用户选择

“Allow Always” 也不是无条件永久放行。它能否持久化、写到什么作用域,取决于 SDK 返回的 permission suggestions、适配器实现以及 Claude Code 是否接受更新。

6.3 AskUserQuestion 不是普通权限

AskUserQuestion 需要展示结构化问题,而不是简单 Allow / Reject。当前 claude-agent-acp 会在 Client 支持 form elicitation 时把它转换为表单交互;若 Client 不支持,就不能假装成普通权限框,适配器会禁用或降级该工具。

ExitPlanMode 等特殊工具也可能携带模式切换语义。它们仍经过 canUseTool 桥接,但适配器需要额外发送 mode update,让编辑器 UI 与 Claude Code 当前状态一致。

7. Claude Agent SDK 与 CLI 的内部控制通道

7.1 进程模型

在本地 TypeScript SDK 的典型实现中,SDK 启动 Claude Code executable,并组合这些 CLI 能力:

  • print/headless 模式。
  • stream-json 输入和输出。
  • 模型、工具、MCP、permission mode、resume 等 options。
  • 需要时包含 partial message 和 hook event。

SDK 对外提供 async iterator,内部则持续读取 CLI stdout,将每行结构化消息解码为 SDKMessage

7.2 普通流与控制流复用同一通道

stdout 中既可能有 assistant、result、stream event,也可能有 control request。stdin 中既可能有用户消息,也可能有 control response。

1
2
stdout: assistant / result / control_request / stream_event
stdin:  user message / control_response / control_cancel_request

它们都使用逐行 JSON,但语义不同:普通消息进入 SDK 消费者,控制消息则需要按 request_id 找到等待中的操作。

7.3 can_use_tool 请求

当前 harness 的控制 schema 中,权限请求包含:

  • request_id:匹配异步响应。
  • tool_nameinputtool_use_id
  • permission suggestions。
  • 可能存在的 blocked path、decision reason、agent id。

SDK callback 返回 allow 时还可以提供 updatedInput,让宿主在批准前安全地修正参数;返回 deny 时提供面向 Agent 的拒绝原因。

7.4 取消、竞速与重复响应

权限等待并不是理想的一问一答:

  • 用户可能取消整个 turn。
  • PermissionRequest hook 可能先于远端 UI 给出决策。
  • WebSocket 或桥接层可能重复投递 response。
  • 同一个 tool use 只能被一个最终决策关闭。

当前 StructuredIO 因而维护 pending request、已解决 tool use 和 cancel message。它的目标是把多个可能的决策源收敛成一次确定结果。

这些字段和竞速规则属于当前 Claude Code 实现。使用 Agent SDK 时可以依赖 callback 的公共类型,但不应让业务代码依赖内部 control_request 的完整 JSON 形状。

8. Session ID 为什么不能混用

这条链路至少可能出现三种身份:

ID所属层用途
ACP connection编辑器与 Agent 进程承载多个 Session
ACP sessionIdACP路由 prompt、update、permission
Claude SDK/CLI session IDClaude Agent SDK / Claude Code持久化和 resume Claude 对话

适配器可以让两个 session ID 取相同字符串,也可以维护映射;协议并不保证它们天然相同。

同理,ACP toolCallId、Anthropic tool_use ID 和 control protocol 的 request_id 解决的是不同关联问题:

  • toolCallId 关联 UI 中的一次工具调用。
  • tool_use ID 关联模型消息中的工具块。
  • request_id 关联一次控制请求与响应。

把它们强行合并会让重试、恢复和并发权限请求变得脆弱。

9. 设计边界与常见误区

9.1 ACP Proxy 不是 Claude Code 内置 ACP Server

claude-agent-acp 是外部适配器。Claude Code CLI 提供 Agent SDK 和结构化 I/O 能力,适配器负责实现 ACP 方法与消息翻译。

9.2 ACP stdio 和 SDK stream-json 只是分帧相似

两者都可以一行一个 JSON,但前者承载标准 ACP JSON-RPC,后者承载 Claude Code SDK message 和内部 control message。格式相似不代表可以直接互通。

9.3 Client 是 UI 权威,不是 Agent 状态权威

编辑器负责展示和用户选择;Agent/Claude Code 负责对话与执行状态。session/load 时由 Agent 重放历史,Client 不应拿本地残缺缓存覆盖 Agent 状态。

9.4 canUseTool 不是唯一安全层

它是宿主交互的桥接点,不是全部权限系统。Claude Code 的静态 deny、沙箱、路径限制和 hooks 仍可能阻止执行。

9.5 不要混用稳定 v1 与 v2 draft

ACP v2 正在演进 transport 和 capability 结构。本文的方法名与主链路以当前稳定 v1 为基准;使用 v2 draft 时必须重新对照对应 schema。

10. 复习索引

  • 四层链路:Editor → ACP Proxy → Agent SDK → Claude Code CLI。
  • ACP 角色:Client 管 UI 与环境,Agent 管对话与执行。
  • 稳定 Session 方法:new、load、prompt、cancel、set_mode;没有 v1 标准 close/resume。
  • 统一输出:实时回复和历史回放都走 session/update
  • 双向能力:Agent 可以反向请求 permission、filesystem 和 terminal。
  • 权限链路:CLI control request → SDK canUseTool → ACP permission → 用户选择 → control response。
  • 三个关联键:ACP sessionId、toolCallId、control request_id 不属于同一层。
  • 稳定性边界:ACP 是公开协议,Agent SDK 是公开接口,control protocol 是当前内部实现。

11. 参考资料与源码锚点

本文由作者按照 CC BY 4.0 进行授权