【笔记】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_request、control_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_file、fs/write_text_file。 - Client 可以支持
terminal/create、terminal/output、terminal/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/close 和 session/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_onceallow_alwaysreject_oncereject_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_name、input、tool_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 sessionId | ACP | 路由 prompt、update、permission |
| Claude SDK/CLI session ID | Claude 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. 参考资料与源码锚点
- Agent Client Protocol
- ACP v1 Overview
- claude-agent-acp
- Claude Agent SDK
src/cli/structuredIO.ts:Claude Code 结构化输入输出与控制请求。src/entrypoints/sdk/controlSchemas.ts:当前 control message schema。src/main.tsx:stream-json、permission mode、resume 等 CLI options。