跳到主要内容

02 · Task 生命周期

本章讲协议的心脏:一个“工作单元”从提交到终态怎么走、agent 凭什么决定回 Message 还是回 Task、contextId 怎么把一连串任务组织成一次会话、以及“任务不可变”这条设计为什么重要。

1. Task / Message / Artifact:三个名词先分清

这三个概念最容易混。一句话各自定位:

概念是什么类比
Message一“轮”通信(带 role:user/agent),装在若干 Part聊天里的一句话
Task一个有状态、有 ID、有生命周期的工作单元一张工单
Artifact任务产出的成果物(文档/图片/数据)工单交付的产品

关键规矩(docs/specification.md:758):Message 用来沟通,Artifact 用来交付结果——结果不应塞在 Message 里。 这把“通信”和“数据输出”干净分开。

Part(specification/a2a.proto:224)是最底层的内容容器,用 proto 的 oneof 实现“四选一”:text(文本)/ raw(内联字节,JSON 里 base64)/ url(指向文件)/ data(结构化 JSON 值)。这让 A2A 模态无关——文本、图片、表单、结构化数据走同一套结构。

2. agent 二选一:回 Message 还是回 Task

收到一条消息,agent 有两种根本回应(docs/topics/life-of-a-task.md:1-14):

  • 回一条 Message(无状态): 适合即答即走的简单交互,不需要状态管理。
  • 起一个 Task(有状态): 当这活需要长跑、可追踪、要管状态时,返回一个 Task,之后走状态机直到终态或中断态。

体现在 proto 上:SendMessageResponse(specification/a2a.proto:779)就是一个 oneof { Task task; Message message; }——结构层面强制“二选一”。

按这个选择把 agent 分成三类(docs/topics/life-of-a-task.md:50-70):

agent 类型行为
Message-only永远回 Message,不管复杂状态,用 contextId 串起对话
Task-generating永远回 Task(连简单回答都建成“已完成任务”),省去判断成本
Hybrid(混合)先用 Message 协商范围,确认后再起 Task 跟踪执行

所有类型有个共同硬规矩:一旦为某交互建了 Task,后续对该消息的回应就只能是 Task,且任务完成后不能再往里发消息。

3. Task 的 8 态状态机

Task 的当前状态放在 TaskStatus.state,取值是 enum TaskState(specification/a2a.proto:187)。八个状态分三类:

┌──────────────────────────── 中断态(可恢复)──────────────┐
│ INPUT_REQUIRED(要更多输入) AUTH_REQUIRED(要鉴权) │
│ ▲ │ │
提交 │ │ │(客户端补发消息) │
──▶ SUBMITTED ──▶ WORKING ┘ └──▶ WORKING ... │
│ │ │
│ ├──▶ COMPLETED (成功,终态) │
│ ├──▶ FAILED (出错,终态) │
│ ├──▶ CANCELED (被取消,终态) │
│ └──▶ REJECTED (agent 拒做,终态) │
└──────────────────────────────────────────────────────────┘
(另有 TASK_STATE_UNSPECIFIED = 0,表示未知/未定)

怎么读: SUBMITTEDWORKING 是正常推进;四个终态(COMPLETED/FAILED/CANCELED/REJECTED)一旦到达就冻结;两个中断态(INPUT_REQUIRED/AUTH_REQUIRED)是“暂停等你”,客户端补发消息后可回到 WORKING

各状态的精确语义直接来自 proto 注释(specification/a2a.proto:187-208)。值得注意:REJECTED 表示 agent 主动决定不做——可以在建任务时拒,也可以跑到一半发现做不了/不愿做。

取消的幂等性。 CancelTask 是幂等的——多次取消效果相同;若任务已取消并被清除,重复取消可以返回 TaskNotFoundError(docs/specification.md:496)。而取消一个已到终态的任务会得到 TaskNotCancelableError(docs/specification.md:556)。

4. 任务不可变:到了终态就别想重启

这是 A2A 的一条核心设计决定(docs/topics/life-of-a-task.md:82-96):任务一旦到终态就不可变,任何“改一改/再来一次”都必须开新任务(挂在同一个 contextId 下),而不是重启老任务。

换来三个好处:

  • 可溯源(Task Immutability): 每个任务的输入→状态→产物是干净的快照,利于编排和审计。
  • 清晰的工作单元: 每次 follow-up 都是一个独立任务,便于细粒度跟踪。
  • 实现更简单: agent 开发者不用纠结“该新建还是重启”。

5. contextId:把多个任务串成一次“会话”

单个任务是一张工单;但真实协作是“一连串相关任务”。A2A 用 contextId 把它们逻辑分组(docs/specification.md:584-602)。

生成与归属规则(精确):

  • 客户端首发消息不带 contextId 时,agent 可以生成一个新的并必须在响应里带回;
  • agent 可以接受并保留客户端给的 contextId;若不能接受,必须报错,且不得另造一个新的(docs/specification.md:593);
  • 服务端生成的 contextId 客户端应当当成不透明标识,别自己造来塞给服务端,除非你懂它怎么处理。

taskId 是服务端生成的(docs/specification.md:604-615)。 客户端不能自带 taskId 来创建新任务;客户端在消息里带 taskId,必须指向一个已存在的任务,否则 agent 返回 TaskNotFoundError。若只给 taskId 不给 contextId,agent 必须从任务推断出 contextId;两者都给但不匹配必须拒绝(docs/specification.md:627-628)。

并行 follow-up。 同一 contextId 下可以并行开多个任务,形成依赖图(docs/topics/life-of-a-task.md:98-110):

ctx-trip-abc
├─ Task1: 订去赫尔辛基的机票
├─ Task2: (基于 Task1) 订酒店
├─ Task3: (基于 Task1) 订雪地摩托
└─ Task4: (基于 Task2) 给酒店加 SPA

客户端用 Message.reference_task_ids(specification/a2a.proto:276)显式指出“这条消息参考了哪些任务”,帮 agent 理解 follow-up 的上下文。

6. 多轮的两条典型路径

路径 A:input-required(要更多输入)。 agent 把任务切到 INPUT_REQUIRED,在 TaskStatus.message 里问问题;客户端用同一个 taskId+contextId 补发消息;任务回到 WORKING(docs/specification.md:630-633)。spec §6.3 有完整的“订机票→问出发到达地→补充”示例(docs/specification.md:1383-1440)。

路径 B:refinement(基于结果再加工)。 客户端用同一 contextId、并在 reference_task_ids 指向原任务,发新请求;agent 开新任务产出新 artifact。规范建议:精修版 artifact 沿用同一个 name、换新 artifactId,让客户端能自己维护版本历史(docs/topics/life-of-a-task.md:113-133)。spec §6 的“生成帆船图→把船改成红色”就是这个模式(docs/topics/life-of-a-task.md:134-249)。

谁来追踪 artifact 的版本链? 规范明确:客户端,不是 agent(docs/topics/life-of-a-task.md:125)。因为只有客户端知道哪个结果可接受;artifact 之间的版本链不属于 A2A 协议本身。

7. 边界与坑

  • Message 不是可靠投递通道。 Task history 不保证存下每条 message;流式断线重连可能漏掉中间的 status message。关键信息别只靠 message 传(docs/specification.md:760-764)。
  • 客户端别自造 contextId/taskId 这是常见误用——两者都由服务端主导。
  • 终态任务发消息是错的。 任务完成后不能再往里发;要继续就开新任务。

8. 代码地图

主题文件符号 / 锚点
Task 结构specification/a2a.protomessage Task(:167)
8 态枚举specification/a2a.protoenum TaskState(:187)
状态容器specification/a2a.protomessage TaskStatus(:210)
二选一响应specification/a2a.protomessage SendMessageResponse(:779)
内容容器(四选一)specification/a2a.protomessage Part(:224)
消息(含 reference_task_ids)specification/a2a.protomessage Message(:260)
多轮语义docs/specification.md§3.4(:580)
不可变 / 并行docs/topics/life-of-a-task.md“Task Immutability”、“Parallel Follow-ups”