跳到主要内容

acp-agent-client-protocol — 本课题摘录

读了哪几篇: 01-lifecycle(会话生命周期与回合)、03-content-and-tools(内容块、工具调用状态机与权限)。 其余三篇(角色与路由、能力协商、巧妙之处)本轮没读。

这一家在本课题里的位置:它给了"停止原因"和"工具调用状态"两套现成的枚举——我们不用自己发明。

它对本课题回答了什么

决定四:一轮一定带一个"为什么停了" —— 五种取值

一轮对话不会无限跑;结束时一定带一个停止原因。

停止原因含义
正常结束模型说完了
触到 token 上限被截断
触到"一轮里自发请求次数"的上限防失控自循环
拒绝继续agent 主动拒绝;这条 prompt 及其之后的内容不会进下一轮,界面应反映这点
被取消调用方主动取消

(依据:协议库 · ACP (Agent Client Protocol) · 会话生命周期与 prompt 回合 —— PromptResponse 一定带一个 stop_reason,五种取值是 end_turn / max_tokens / max_turn_requests / refusal / cancelled;refusal 注释强调这条 prompt 及其之后的内容不会进下一轮)

对比 fara 的"步数耗尽也标成完成":这里把"上限"和"正常结束"明确分开了,而且分得更细—— token 上限和轮数上限是两回事,拒绝又是第三回事。 这五种是我们可以直接抄的枚举。

"拒绝"那一条的注释很值得记: 拒绝不只是"这次不做",而是这条输入及其之后的内容不进下一轮——历史要被截断,而不是把拒绝当成一条普通回复留在那里。

决定四补充:取消被设计成一个对称的收尾握手

取消不是"直接掐断连接",而是一条通知,而且双方各有一条必须做的事:

必须做什么
agent收到取消后,即使底层操作抛了异常,也必须捕获并返回"被取消"这个停止原因,以此确认取消成功
调用方对所有还悬着的权限请求,必须用"被取消"这个结果回掉

(依据:协议库 · ACP (Agent Client Protocol) · 会话生命周期与 prompt 回合 —— 取消是一条无响应通知,agent 必须捕获底层异常并返回 StopReason::Cancelled 确认取消成功,client 必须用 Cancelled outcome 回掉所有悬着的 request_permission)

这样双方对"这一轮到底结束没"有确定的、对称的答案,不会留下半挂起的请求。

这条对我们直接有用。 "用户按了 Ctrl-C" 在很多实现里就是直接抛异常往上冒, 结果是:工具跑到一半、审批请求还挂着、历史里留下一个没有结果的调用。 ACP 的做法是把取消也当成一种"正常结束",走同一条收尾路径。

它还区分了两种取消:一种取消整轮对话,一种取消单个请求。别混淆。

决定三:工具调用是一个带状态机的实体,不是一行日志

它要解决的问题说得很好:模型说"我要读文件 / 跑命令",界面得把这件事作为一个有进度、有结果的实体画出来,而不是一行日志。

排队/等审批 ──► 执行中 ──► 完成
└──► 失败

(依据:协议库 · ACP (Agent Client Protocol) · 内容块、工具调用状态机与权限 —— ToolCall 是有 id/标题/kind/status/content 的实体,状态机是 pending → in_progress → completed/failed,agent 先发一个 tool_call 之后发若干只带变化字段的 tool_call_update 就地更新)

agent 先发一个"有这么个调用",之后发若干"只带变化字段"的更新就地改它。

工具还带一个类别,给界面选图标用: 读 / 改 / 删 / 移动 / 搜索 / 执行 / 思考 / 抓取 / 切换模式 / 其它。

还有一个"受影响的文件路径"字段,用途注释写得很直白:让界面能"跟随"——随 agent 自动跳到正在改的文件。

决定三补充:工具结果分三种,差异单列一种

结果类型是什么
普通内容文本/图片/资源
差异文件修改,带路径 + 旧文本 + 新文本
终端嵌入一个终端

差异单列一种、而不是塞进文本,是因为界面要画成红绿对照。

对我们的意义:工具结果不该只有"一段字符串"这一种形状。 griptape 的"信封"是从数据类型角度说的,这里是从渲染需求角度说的——两边指向同一个结论。

决定一:内容块统一,而且刻意兼容另一份协议

prompt、流式输出、工具结果全都复用同一个内容块枚举。而且它刻意兼容 MCP 的 JSON 表示——这样 agent 能把 MCP 工具的输出原样转发,不用再转换一层。 (依据:协议库 · ACP (Agent Client Protocol) · 内容块、工具调用状态机与权限 —— ContentBlock 被 prompt、流式输出、工具结果全部复用,且刻意兼容 MCP 的 JSON 表示以便把 MCP 工具输出原样转发)

协议规定了最低保证:必须支持文本和资源引用两种,其它按能力可选。

"思考"和"回复"是分开的两种变体——这样界面能把内心独白折叠或灰显,和正式回复区分开。

计划:更新必须发全量,不是打补丁

agent 可以发一份"我打算分几步走"的计划,每步有内容、优先级、状态。

关键语义:更新计划时必须发完整的全量列表,由调用方整体替换旧计划——不是增量打补丁。 (依据:协议库 · ACP (Agent Client Protocol) · 内容块、工具调用状态机与权限 —— 更新 Plan 时 agent 必须发完整全量列表由 client 整体替换,不是增量打补丁,把计划同步做成无状态的幂等替换)

这把"计划同步"做成了无状态的幂等替换,简单可靠。

权限:四种选项,而且区分"这次"和"记住"

选项含义
这次允许只这一次
允许并记住长期
这次拒绝只这一次
拒绝并记住长期

每个选项带一个类别,告诉界面用什么图标和措辞。

跟 openai-agents-js 的"批准默认只对当前调用生效,传参数可长期"是同一件事,但这里把四种都列成了协议里的一等选项

它没回答什么

  • 循环内部怎么写——它规定的是编辑器和 agent 之间的接口,不是 agent 内部。
  • 历史怎么压——不在它的范围。
  • 工具调用怎么从模型输出里解析出来——不在它的范围。

坑与代价

  • 它的设计目标是"让编辑器能清晰渲染 agent 的意图",不是"让 agent 跑得对"。 很多字段(类别、受影响路径、差异)是为渲染服务的——我们的最小原型没有界面,这些字段暂时用不上。

    判断(无锚): 但停止原因那五种、工具调用状态机那四态,现在就该用——它们是纯语义的,跟有没有界面无关。 如果错,会错在: 如果我们的循环只有"完成/上限"两种结局,那五种枚举里有三种永远用不上,反而增加概念。那就先用三种(完成/上限/取消),留扩展。

  • "终端"那种结果有一条时序约束:必须在终端被释放之前嵌入。 这类"资源生命周期"约束在协议里靠注释表达,编译器管不住。