漫话 Agent Harness:ACP——终端和 Agent 之间怎么说话

写在前面
这一篇讲的是 ACP(Agent Client Protocol)——Zed 团队在 2025 年推出的一个开放协议。它干的事很简单:定义 IDE / Agent 网关跟 AI Agent 之间怎么说话。
但在讲它之前,先把协议生态里容易混淆的几个名字分清楚:
- MCP(Model Context Protocol,Anthropic):Agent 连工具(agent-to-tool)
- ACP(Agent Client Protocol,Zed):Client 连 Agent(client-to-agent)
- A2A(Agent-to-Agent,Google + Linux Foundation):Agent 连 Agent
这三个协议各管一段、互相正交,不是同一个东西的不同名字。
业界有时候会把"ACP"这个名字跟 IBM 捐给 Linux Foundation 的那套"Agent Communication Protocol"搞混——那个是 agent-to-agent 的,后来并进了 Google A2A。本文的 ACP 指 Zed 那个 client-to-agent 协议。
Codex Harness 的 app-server、Cursor 的 ACP 实现、Zed 自己的 ACP 实现——都属于这一套。
一、为什么需要 ACP
把 Agent 嵌入一个宿主环境(IDE、Agent 网关、Web 客户端),需要解决几个问题:
问题一:Agent 跟宿主怎么对话?
最早的做法是 HTTP 请求-响应:宿主 POST 一个 prompt 给 Agent,Agent 返回结果。但这套方案有几个硬伤:
- 长任务不好处理(LLM 生成动辄几十秒,HTTP 长轮询不优雅)
- 流式输出别扭(要么 SSE、要么 WebSocket,但协议栈变复杂)
- 最关键的:Agent 想反向调宿主的工具,做不到
问题二:Agent 怎么反向调宿主工具?
Agent 在宿主里跑,经常需要:
- 读宿主的文件(
fs/read_text_file) - 写宿主的文件(
fs/write_text_file) - 开宿主的终端执行命令(
terminal/create、terminal/output) - 让宿主帮它做鉴权(拿 OAuth token、查用户身份)
- 让宿主帮它查配额、计量计费
这些工具装在宿主身上,不是装在 Agent 身上。Agent 必须能反向"召唤"宿主来干活。
问题三:宿主怎么管控 Agent?
宿主不能让 Agent 想干嘛就干嘛:
- 危险工具需要宿主审批
- 文件写入需要宿主授权工作目录
- 工具调用需要宿主计量计费
- 长任务需要宿主能取消
这些都是宿主对 Agent 的管控能力,必须在协议层就支持。
ACP 就是为了解决这三个问题设计的。
二、ACP 在协议生态里的位置
先把 ACP 放进协议生态大图里看:

三种协议的边界:
| 协议 | 谁连谁 | 谁提出的 | 典型场景 |
|---|---|---|---|
| ACP | Client ↔ Agent | Zed | IDE 调 Codex CLI;Agent 网关调后端 Agent |
| MCP | Agent ↔ Tool | Anthropic | Agent 调数据库、调搜索、调日历 |
| A2A | Agent ↔ Agent | Google + Linux Foundation | 一个 Agent 调用另一个外部 Agent |
它们的共同点:都用 JSON-RPC 2.0 作为消息信封。 这不是巧合——JSON-RPC 已经成为 Agent 生态事实上的标准消息格式。
一个完整的多 Agent 系统长这样:
[IDE / Agent 网关]
│ ACP(双向)
▼
[Agent A]
│ MCP
▼
[Tool Server 1] [Tool Server 2] ...
同时 Agent A 可能通过 A2A 调用 [外部 Agent B]
Codex Harness 的 app-server 就是 ACP 协议的典型实现:IDE 通过 ACP 调 app-server,app-server 再通过 MCP 调外部工具,app-server 也能反向调 IDE 让它读文件、开终端。
三、ACP 协议本身:一根双向管子
ACP 的核心思想可以用一句话概括:JSON-RPC 2.0 + 长连接 + 双向。
3.1 传输:stdio + newline-delimited JSON
ACP 默认用 stdio 作为传输层:
- 宿主把 Agent 进程 spawn 出来当子进程
- 宿主跟 Agent 通过 stdin/stdout 通信
- 每条 JSON-RPC 消息占一行(newline-delimited)
这跟 LSP(Language Server Protocol)的设计一样——ACP 本质上是 LSP 在 Agent 领域的翻版。
为什么不直接用 HTTP?
- 简单:不用管 HTTP 服务、端口冲突、SSL 证书
- 安全:stdio 是进程间通道,外部不可达,天然沙箱
- 持久:子进程跟着宿主一起启停,不用管连接超时
- 双向:stdin/stdout 天然双向,没有"服务端/客户端"的不对称
3.2 消息信封:JSON-RPC 2.0
三种消息类型:
Request(请求):有 id、有 method、有 params,期望对端回复。
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "session/prompt",
"params": {"session_id": "abc", "prompt": "帮我修一下这个 bug"}
}
Response(响应):带对应的 id,要么 result 要么 error。
{
"jsonrpc": "2.0",
"id": "req-001",
"result": {"status": "ok"}
}
Notification(通知):没有 id,单向推送,不期望回复。
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {"kind": "agent_thought_chunk", "content": {"thought": "Analyzing..."}}
}
3.3 生命周期:五步走
一次典型的 ACP 会话长这样:

① Initialize(握手)
Client 和 Agent 交换支持的协议版本和能力:
// Client → Agent
{"method": "initialize", "params": {"protocolVersion": 1, "clientInfo": {"name": "Zed", "version": "0.165"}}}
// Agent → Client
{"result": {"protocolVersion": 1, "capabilities": {"fs": true, "terminal": true}}}
② Authenticate(鉴权,可选)
Client 给 Agent 提供凭证(API Key、OAuth Token 等)。
③ Session 管理
session/new:开一个新会话session/load:加载历史会话session/close:结束会话
④ Prompt & Streaming
Client 发 prompt,Agent 流式回:
- Client:
session/prompt请求 - Agent:
session/update通知流式推送思考过程、工具调用、最终答案
⑤ 取消
Client 随时可以 session/cancel 中断当前任务。
3.4 双向:Agent 反过来调 Client
这是 ACP 最精妙的一点——Agent 不只是被动响应,它也能反向调 Client 的方法。
Client 暴露的方法(Agent 可调):
fs/read_text_file:读宿主上的文件fs/write_text_file:写宿主上的文件terminal/create:在宿主上开一个终端terminal/output/terminal/kill:操作终端- 鉴权、配额、计费相关方法(看宿主实现)
Agent 想读文件时,直接反向发一个 JSON-RPC Request 给 Client:
// Agent → Client
{"id": "req-agent-001", "method": "fs/read_text_file", "params": {"path": "/project/main.py"}}
// Client → Agent
{"id": "req-agent-001", "result": {"content": "def main():\n pass\n"}}
这一来一回,Agent 就把宿主变成了它的"工具集"。
3.5 工具生命周期:四事件标准化
Agent 调用宿主工具的全过程,被标准化成四个事件:
tool_use_start → tool_input_delta → tool_use_stop → tool_result
tool_use_start:Agent 宣布"我要用 X 工具"tool_input_delta:流式传工具参数tool_use_stop:Agent 宣布"参数传完了"tool_result:宿主返回工具结果
宿主可以在 tool_use_start 和 tool_use_stop 之间插入审批流程(比如弹对话框让用户确认)。
3.6 协议分层:应用层 JSON-RPC,底层传输可换
ACP 跟 HTTP 一样是分层协议。把协议栈画清楚:

┌─────────────────────────────────────────┐
│ 应用层:ACP 方法 │
│ initialize / session/new / prompt / ... │
├─────────────────────────────────────────┤
│ 消息层:JSON-RPC 2.0 │
│ Request(id+method+params) │
│ Response(id+result/error) │
│ Notification(method+params) │
├─────────────────────────────────────────┤
│ 序列化:newline-delimited JSON │
│ 或 WebSocket text frame │
├─────────────────────────────────────────┤
│ 传输层:stdio / WebSocket / TCP / ... │
└─────────────────────────────────────────┘
应用层是固定的——所有 ACP 实现都得支持 initialize / session/new / session/prompt 这些方法。
传输层是可换的——只要满足"双向字节流"这个接口,跑 stdio 还是 WebSocket 还是 TCP 都行。dsh 的 ACP 就是这么设计的:默认 stdio,但 Stream 接口一抽象,底下换啥都行:
// 来自 @agentclientprotocol/sdk 的 Stream 接口
type Stream = {
writable: WritableStream<AnyMessage>;
readable: ReadableStream<AnyMessage>;
}
dsh 源码里那行:
const stream: Stream = config.stream ?? ndJsonStream(process.stdout, process.stdin)
config.stream 不为空就用自定义传输,为空就 fallback 到 stdio。这是典型的策略模式——应用层不关心底下用的是啥。
各种传输的使用场景:
| 传输 | 场景 | 优劣 |
|---|---|---|
| stdio | IDE spawn Agent 子进程(dsh、Codex、Cursor 默认) | 简单、安全、持久;但只能同机 |
| WebSocket | 跨机器、跨数据中心、浏览器接 Agent | 跨网、双向;但有握手开销 |
| TCP | Agent 网关、低延迟长连接 | 轻量、快;但没有 WS 的协议特性 |
| Unix Socket | 同机高性能 IPC | 比 TCP 快;但只能同机 |
| 进程间管道 | 测试、嵌入场景 | 简单可控 |
四、Agent 网关:ACP 比较有发挥潜力的工程场景
ACP 当前最成熟的场景就是 IDE ↔ Agent(Zed、Cursor、JetBrains 都在用)。但协议本身支持反向调用,这意味着如果未来要做’网关 ↔ 沙箱 Agent’这种分布式部署,ACP 在协议层是天然支持的
4.1 什么是 Agent 网关
一个 Agent 网关是一个独立部署的服务,作为 Client 跟 Agent 之间的中间层。它既代理会话、又提供工具能力、还做鉴权计费。

4.2 网关干什么
一个典型的 Agent 网关负责这几件事:
① 会话代理
Client(IDE、Web 客户端)不直接连 Agent,连网关:
[IDE] ──ACP── [网关] ──ACP── [Agent A]
──ACP── [Agent B]
网关根据请求路由到不同的 Agent,屏蔽后端 Agent 的异构性。
② 工具调用
网关自己挂了一堆工具,Agent 反向调网关来用这些工具:
- 数据库查询
- 内部 API 调用
- 第三方 SaaS 集成
Agent 不需要知道这些工具的鉴权细节,网关替它搞定。
③ 鉴权
Agent 想调某个工具时:
- Agent 反向问网关:“我能调 X 工具吗?”
- 网关检查 Agent 的身份、权限、配额
- 批准了才放行
这层鉴权是Agent 无法绕过的——因为工具根本不在 Agent 进程里,它只能反向求网关。
④ 计量计费
网关记录每一次工具调用:
- 谁调的(Agent 身份)
- 什么时候调的
- 调了多少次
- 消耗了多少 token / API 调用次数
这些数据可以用来计费、限流、审计。
⑤ 审批流
危险工具调用要过审批。审批逻辑在网关里:
- 读文件 → 直接放行
- 写文件 → 让用户确认
- 删除文件 → 必须二次确认
网关是审批策略的统一定义点,Agent 自己改不了。
4.3 为什么网关要用 ACP
为什么网关不用 HTTP,非要用 ACP 这种长连接?
因为 ACP 的双向性是关键。
HTTP 是"客户端发起、服务端响应"的不对称模型。Agent 想反向调网关的工具,HTTP 做不到(除非再加一套反向通道,复杂)。
ACP 是天然双向的:同一根管子,Client 可以推消息给 Agent,Agent 也可以推消息给 Client。这让 Agent 能很自然地"召唤"网关来干活。
还有流式的好处:Agent 的思考过程、工具调用过程都通过 Notification 流式推给网关,网关再流式推给最终用户。HTTP 做流式要么 SSE 要么 WebSocket,多一层协议栈。
五、ACP 跟 Codex Harness 的关系
聊到这里,你应该能看出来:Codex Harness 的 app-server 就是一个 ACP 协议的实现(或者说 ACP 类协议的实现,因为 Codex 有自己的方法命名)。
Codex app-server 的消息方法(摘自 codex-rs/app-server/README.md):
initialize/initialized:握手thread/start、thread/resume:会话管理(ACP 里叫 session)turn/start:发 prompt(ACP 里叫 session/prompt)item/started、item/completed:流式推送(ACP 里叫 session/update)fs/readFile、fs/writeFile:文件操作(ACP 里叫 fs/read_text_file)command/exec:执行命令(ACP 里叫 terminal/create)
方法名不一样,但模式完全一样:JSON-RPC + 双向 + 长连接 + 流式。
Zed 推 ACP 这件事的价值,是把 Codex 这种"私有 JSON-RPC 协议"抽象成一套开放标准。以后不管你用 Codex、Claude Code、还是别的 CLI Agent,IDE 接入都用同一套协议。
六、实际跑一遍 ACP
概念讲完了,看一段真实代码。这里演示 ACP 的分层实现——应用层(JSON-RPC)和传输层(stdio / WebSocket / TCP)各写一份,跑同一段 demo。
完整可运行的 Node 项目见文末链接,下面只抽最关键的部分。
6.1 应用层:一个 Transport 接口搞定
先写应用层——这是跟传输无关的部分:
// jsonrpc.ts —— 应用层,对所有传输一视同仁
type Transport = {
send(msg: JsonRpcMessage): void
onMessage(cb: (msg: JsonRpcMessage) => void): void
close(): void
}
class RpcEndpoint {
constructor(private transport: Transport) {
transport.onMessage((msg) => this.onIncoming(msg))
}
// 注册本端方法(可被对端调用)
registerMethod(method: string, handler: (params: any) => Promise<any>) { ... }
// 主动调对端方法(双向的关键!)
async request(method: string, params?: any): Promise<any> { ... }
// 主动发通知(不期望回复)
notify(method: string, params?: any): void { ... }
}
关键设计:同一个 RpcEndpoint 既能"接收"也能"发起"调用。 Client 和 Agent 各有一个 RpcEndpoint,双向对称。
6.2 三种传输:实现 Transport 接口
// ── stdio:默认方式 ──
function stdioTransport(): Transport {
const rl = createInterface({ input: process.stdin })
const cbs: ((m: JsonRpcMessage) => void)[] = []
rl.on('line', (line) => cbs.forEach(cb => cb(JSON.parse(line))))
return {
send(msg) { process.stdout.write(JSON.stringify(msg) + '\n') },
onMessage(cb) { cbs.push(cb) },
close() { rl.close(); process.exit(0) },
}
}
// ── WebSocket:跨机器用 ──
function wsTransport(ws: WebSocket): Transport {
const cbs: ((m: JsonRpcMessage) => void)[] = []
ws.on('message', (data) => cbs.forEach(cb => cb(JSON.parse(data.toString()))))
return {
send(msg) { ws.send(JSON.stringify(msg)) },
onMessage(cb) { cbs.push(cb) },
close() { ws.close() },
}
}
// ── TCP:低延迟长连接 ──
function tcpTransport(socket: net.Socket): Transport {
const rl = createInterface({ input: socket })
const cbs: ((m: JsonRpcMessage) => void)[] = []
rl.on('line', (line) => cbs.forEach(cb => cb(JSON.parse(line))))
return {
send(msg) { socket.write(JSON.stringify(msg) + '\n') },
onMessage(cb) { cbs.push(cb) },
close() { rl.close(); socket.destroy() },
}
}
三种实现都长得很像——因为它们都在做同一件事:把"双向字节流"包装成"JSON-RPC 消息流"。应用层代码一行都不用改。
6.3 Agent 端:一个 Echo Agent
function makeEchoAgent(transport: Transport): RpcEndpoint {
const endpoint = new RpcEndpoint(transport)
endpoint.registerMethod('initialize', () => ({
protocolVersion: 1,
agentInfo: { name: 'echo-agent', version: '1.0.0' },
capabilities: {},
}))
endpoint.registerMethod('session/new', (params) => ({
sessionId: `sess-${Math.random().toString(36).slice(2)}`,
}))
endpoint.registerMethod('session/prompt', async (params) => {
// ★ 反向调 Client 的工具 ★ —— 这就是双向的精髓
const file = await endpoint.request('fs/read_text_file', {
path: `${params.cwd}/demo.txt`,
})
// 流式推一条通知
endpoint.notify('session/update', {
sessionId: params.sessionId,
update: { sessionUpdate: 'agent_message_chunk', content: file.content },
})
return { stopReason: 'end_turn' }
})
return endpoint
}
6.4 Client 端:挂上工具、接收反向调用
const client = new RpcEndpoint(transport)
// 注册 Client 侧工具:Agent 可以反向调 fs/read_text_file
client.registerMethod('fs/read_text_file', (params) => ({
content: readFileSync(params.path, 'utf-8'),
}))
// 监听 Agent 流式推送的 session/update
client.onNotification('session/update', (params) => {
process.stdout.write(`🤖 ${params.update.content}\n`)
})
// 走一遍生命周期
await client.request('initialize', { protocolVersion: 1 })
const { sessionId } = await client.request('session/new', { cwd: process.cwd() })
await client.request('session/prompt', { sessionId, prompt: 'Hi!' })
6.5 三种传输跑同一个 demo
下面是同一个 demo 在三种传输上跑出来的真实日志(摘录):
stdio 模式:
═══════════════════════════════════════════════════════════
ACP Demo · 传输层:stdio(子进程 spawn)
═══════════════════════════════════════════════════════════
▶️ Client: initialize
✅ Agent 回复: {"protocolVersion":1,"agentInfo":{"name":"echo-agent"...}}
▶️ Client: session/new
✅ 会话创建: sessionId=sess-tss0ww
▶️ Client: session/prompt("Hello from ACP client!")
📂 [Client 侧工具被调] fs/read_text_file(path=.../demo.txt)
🤖 [Agent] 附:我从 Client 上读到了文件 ...
🎉 Demo 完成
WebSocket 模式:
═══════════════════════════════════════════════════════════
ACP Demo · 传输层:WebSocket
═══════════════════════════════════════════════════════════
🟢 Agent Server listening on ws://localhost:8765
🔵 Client: 已连上 Agent Server
...(后续输出完全一致)
TCP 模式:
═══════════════════════════════════════════════════════════
ACP Demo · 传输层:TCP(裸 socket,newline-delimited JSON)
═══════════════════════════════════════════════════════════
🟢 Agent Server listening on tcp://localhost:8766
🔵 Client: 已连上 Agent Server
...(后续输出完全一致)
三份日志除了开头那行传输类型,后面一模一样——这就是分层协议的价值:应用层代码只写一次,传输层随便换。
完整可运行的 Node 项目(含 tsconfig、package.json、完整源码)见:
👉 acp-node-demo/ 目录(跟本文同仓库)
跑法:
cd acp-node-demo
pnpm install
pnpm run:stdio # stdio 模式(dsh/Codex 默认)
pnpm run:ws # WebSocket 模式
pnpm run:tcp # TCP 模式
七、ACP 为什么重要
理由一:让 Agent 可以"寄宿"在宿主里
过去 Agent 是个独立进程,跑自己的代码、用自己的工具。ACP 之后,Agent 可以像寄生虫一样寄宿在 IDE、网关、Web 客户端里,借用宿主的一切能力(文件系统、终端、鉴权、计费)。
这让"Agent 嵌入产品"变得非常轻量——产品只需要实现 ACP 客户端,就能接入任何 ACP 兼容的 Agent。
理由二:标准化 IDE-Agent 交互
Codex 的 app-server、Claude Code、Cursor 的 Agent 集成……这些以前都是各家自己写一套 JSON-RPC 方言。ACP 把这些方言统一了。
以后 Zed、Cursor、VS Code 这些 IDE 只要支持 ACP,就能接入任何 ACP 兼容的 Agent——跟当年 LSP 让所有编辑器都能接入所有语言服务器是一个逻辑。
理由三:Agent 网关成为可能
有了 ACP,Agent 网关这种架构才顺理成章:
- 网关独立部署,对接多个 Agent
- 网关提供工具、鉴权、计费
- Agent 反向调网关来用工具
- 网关代理会话、审批危险操作
这种架构非常适合企业级部署——Agent 是核心智能,网关是基础设施。两者解耦,各自独立演进。
理由四:跟 MCP/A2A 形成完整的协议栈
- MCP 解决"Agent 怎么接工具"
- ACP 解决"Agent 怎么被宿主接入"
- A2A 解决"Agent 怎么互相调用"
三个协议正交互补,拼起来才是完整的 Agent 互操作性方案。Codex Harness、dsh、Claude Code 这些 Agent Harness 要真正成为生态级的基础设施,三个协议都得支持。
写在最后
如果这一篇只记住一件事,我希望是这句:
ACP 让 Agent 能寄宿在宿主里——用一根双向管子把 Agent 跟 Client 连起来,让 Agent 既能被宿主调用、也能反过来调宿主的能力。
这跟 MCP(Agent 调工具)和 A2A(Agent 调 Agent)是正交的。三个协议拼起来,才是 Agent 生态的完整协议栈。
Codex Harness 的 app-server 已经是 ACP 类协议的实现了(方法名不同但模式一致)。Zed 推 ACP 标准化,就是想让所有 IDE 跟所有 Agent 之间,不用每家都重写一套集成。
Agent 网关是 ACP 最有想象力的工程场景——独立部署、双向连接、代理会话、代理鉴权计费。这才是企业级 Agent 部署该有的样子。
相关参考资料
ACP 官方
- ACP 协议官网:https://zed.dev/acp
- ACP 协议 GitHub:https://github.com/agentclientprotocol/agent-client-protocol
- ACP 参考实现(Elixir):https://hexdocs.pm/acpex/protocol_overview.html
- Pydantic AI 的 ACP harness:https://pydantic.dev/docs/ai/harness/acp/
dsh 的 ACP 实现(TypeScript,本文参考的源码)
- dsh 的 ACP 桥接:
packages/acp/acp/src/index.tsin https://github.com/deepseek-ai/deepseek-harness - dsh 用的官方 SDK:
@agentclientprotocol/sdk(npm)
Codex app-server(ACP 类协议实现)
- Codex app-server 文档:https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md
协议生态对照
- MCP 官方:https://modelcontextprotocol.io/
- A2A 官方:https://github.com/a2aproject/a2a
更多推荐

所有评论(0)