跳到主要内容

a2a-protocol — 本课题摘录

读了哪几篇: 02-task-lifecycle(Task 生命周期)。 其余三篇(发现与名片、传输与绑定、流与异步安全)本轮没读——属 agent 互操作课题。

这一家在本课题里的位置:它给了"一次运行有哪些状态、什么算终态"的一份权威定义。虽然它讲的是 agent 之间的互操作,但状态机本身是通用的。

它对本课题回答了什么

决定四:八态状态机 —— 终态、中断态、进行态分得很清

┌────────── 中断态(可恢复)──────────┐
│ 要更多输入 要鉴权 │
│ ▲ │ │
提交 │ │ │(补发消息) │
──► 已提交 ──► 进行中 ┘ └──► 进行中 … │
│ │ │
│ ├──► 已完成 (终态) │
│ ├──► 失败 (终态) │
│ ├──► 已取消 (终态) │
│ └──► 被拒绝 (终态) │
└────────────────────────────────────┘

(依据:协议库 · A2A (Agent2Agent) Protocol · Task 生命周期 —— TaskState 八个状态分三类——SUBMITTED/WORKING 是进行态,COMPLETED/FAILED/CANCELED/REJECTED 四个终态一到就冻结,INPUT_REQUIRED/AUTH_REQUIRED 两个中断态是暂停等你、补发消息后可回到 WORKING)

三类分法值得直接抄:

状态特点
进行态已提交、进行中正常推进
终态已完成、失败、已取消、被拒绝一到就冻结
中断态要更多输入、要鉴权暂停等你,补上就能回到进行中

"被拒绝"单独是一个终态,不跟"失败"合并——跟 ACP 把"拒绝"单列一种停止原因一致。 两家独立做了同一个区分:"我做不到"和"我不做"不是一回事。

"中断态"这个类别对我们特别有用。 我们讨论"什么时候停"时,一直默认停就是结束。这里说的是第三种:暂停,而且暂停有类型。 缺输入和缺权限是两种不同的暂停,界面和调用方要做的事不一样。

决定四最重要的一条:终态即不可变,再来一次必须开新的

任务一旦到终态就不可变,任何"改一改 / 再来一次"都必须开新任务(挂在同一个会话下),而不是重启老任务。 (依据:协议库 · A2A (Agent2Agent) Protocol · Task 生命周期 —— 任务一旦到终态就不可变,任何「改一改/再来一次」必须开新任务挂在同一 contextId 下而不是重启老任务;换来可溯源、清晰工作单元、实现更简单三个好处)

换来三个好处,它列得很清楚:

好处说的是什么
可溯源每个任务的输入 → 状态 → 产物是一份干净快照,利于编排和审计
清晰的工作单元每次追加要求都是一个独立任务,便于细粒度跟踪
实现更简单开发者不用纠结"该新建还是重启"

第三条最实在。 "重启一个跑过的运行"这件事的语义特别难定义:历史留不留?计数器清不清?已经产生的副作用算不算? 它的答案是:这个问题根本不该存在。

决定三:沟通和交付要分开

一条明确的规矩:消息用来沟通,产物用来交付结果——结果不应塞在消息里。 (依据:协议库 · A2A (Agent2Agent) Protocol · Task 生命周期 —— Message 用来沟通、Artifact 用来交付结果,规范明确「结果不应塞在 Message 里」,把通信和数据输出干净分开)

这条对我们有直接启发。 我们的循环里,工具结果是当成一条消息写回历史的。 但"这次运行到底产出了什么"和"过程中说了哪些话"其实是两件事—— 前者应该能被单独取出来,而不是从消息流里翻。 这跟 griptape 的"大结果只回引用"、fara 的"最终答案单独存"指向同一个方向。

最底层的内容容器是"四选一":文本 / 内联字节 / 指向文件的地址 / 结构化数据。 于是文本、图片、表单、结构化数据走同一套结构。

决定一:回一条消息还是起一个任务,是一个显式的二选一

收到一条输入,有两种根本回应:

  • 回一条消息(无状态): 适合即答即走,不需要状态管理;
  • 起一个任务(有状态): 当这活要长跑、可追踪、要管状态时。

而且这个二选一是在结构层面强制的——响应类型本身就是"要么是任务、要么是消息"。

按这个选择把 agent 分成三类:

类型行为
只回消息永远无状态,用一个上下文 id 串起对话
只建任务连简单回答都建成"已完成任务",省去判断成本
混合先用消息协商范围,确认后再起任务跟踪执行

(依据:协议库 · A2A (Agent2Agent) Protocol · Task 生命周期 —— agent 分三类 Message-only / Task-generating / Hybrid,共同硬规矩是「一旦为某交互建了 Task,后续回应就只能是 Task,且任务完成后不能再往里发消息」)

共同的硬规矩:一旦为某次交互建了任务,后续回应就只能是任务;任务完成后不能再往里发消息。

会话由一个上下文 id 串起,而且归属规则很严

任务 id 由服务端生成,客户端不能自带 id 来创建新任务。 客户端带的 id 必须指向已存在的任务,否则报"找不到"。

同一个会话下可以并行开多个任务,形成依赖图。

多轮的两条典型路径:

路径怎么走
要更多输入切到中断态、在状态里问问题;客户端用同一个任务 id 补发;回到进行中
基于结果再加工客户端用同一会话、指向原任务,发新请求;agent 开新任务产出新产物

产物的版本链由谁维护?规范明确:客户端,不是 agent。 因为只有客户端知道哪个结果可接受;版本链不属于协议本身。

它没回答什么

  • 循环内部怎么写——它是 agent 之间的互操作协议。
  • 工具怎么定义、怎么调用——不在这一篇。
  • 历史怎么压——不在它的范围。

坑与代价

  • 消息不是可靠投递通道。 任务历史不保证存下每条消息;流式断线重连可能漏掉中间的状态消息。关键信息别只靠消息传。 (依据:协议库 · A2A (Agent2Agent) Protocol · Task 生命周期 —— Task history 不保证存下每条 message、流式断线重连可能漏掉中间的 status message,规范提醒关键信息别只靠 message 传)

    这条要记: 我们如果把"这次到底做了什么"全存在消息流里,一旦有丢失就无法复盘。关键状态要有独立落点。

  • 八态对最小原型是过重的。

    判断(无锚): 我们第一版只需要三态:进行中 / 完成 / 失败。但**"终态即冻结"和"再来一次开新的"这两条现在就该定下来**——它们不增加代码,却能省掉后面一大堆"重启语义"的纠结。 如果错,会错在: 如果第一版就要支持"改一下再跑",而每次都开新运行会让历史变得难追,那可能需要一个"这次运行是基于哪次"的字段,而不是纯粹的独立任务。