Artifact 是 Server 处理任务产生的正式输出,和 Message 有明确分工:
Artifact
├── artifactId 唯一标识
├── name 名称(如 "dice-result"、"q3-report")
├── description 描述(可选)
└── parts[] 内容(和 Message 一样由 Part 组成,
所以产物同样可以是文本/文件/结构化数据)
一个任务可以有多个 Artifact(如一份 PDF 报告 + 一份数据 JSON);大的 Artifact 可以分块(chunk)流式传输,用 append / lastChunk标志拼接。
| 模式 | 方法 | 结果如何到达 | 适合场景 |
|---|---|---|---|
| 同步/轮询 | SendMessage + GetTask |
响应里直接带 Task;没完成就轮询 | 秒级任务、简单集成 |
| 流式 | SendStreamingMessage |
SSE 持续推事件 | 需要实时进度/打字机效果 |
| 推送通知 | SendMessage + PushNotificationConfig |
Server 主动 POST 你的 webhook | 小时级长任务、移动端 |
Client 调 SendStreamingMessage,Server 用 **SSE(Server-Sent Events,Content-Type: text/event-stream)**保持 HTTP 连接并持续推送事件:
Client Server
│ POST / {"method":"SendStreamingMessage"} │
│ ─────────────────────────────────────────>
│ HTTP 200, text/event-stream │
│ <═════ event: Task(submitted) ═══════════│ ① 先推 Task 对象
│ <═════ event: TaskStatusUpdateEvent ═════│ ② working
│ <═════ event: TaskArtifactUpdateEvent ═══│ ③ 产物(可分块多次)
│ <═════ event: TaskStatusUpdateEvent ═════│ ④ completed(final)
│ (连接关闭) │
任务进入终态后流必须关闭。若网络断开,Client 可用 SubscribeToTask(v0.3 叫 tasks/resubscribe)重新接上继续收事件。
示例输出:
[事件] Task 创建: id=b993f7cf... 状态=TASK_STATE_SUBMITTED
[事件] 状态更新: TASK_STATE_WORKING
[事件] 产物 'dice-result': 掷 3 个 6 面骰子: [2, 6, 5],总和 = 13
[事件] 状态更新: TASK_STATE_COMPLETED
如果任务要跑几小时,Client 不想挂着连接,可以注册一个 webhook:
Client Server Client 的 Webhook
│ CreateTaskPushNotification │ │
│ Config(url=https://my/hook) │ │
│ ────────────────────────────> │
│ (断开) │ ...数小时后任务完成... │
│ │ POST https://my/hook │
│ │ ────────────────────────────>│
│ │ (带任务 id + 新状态) │
│ GetTask(id) 拉取完整结果 │ │
│ ────────────────────────────> │
真实任务很少一句话说清。比如你对订票 Agent 说"订一张去上海的机票”,它至少要追问:哪天?哪个舱位?接受转机吗?
传统 API 遇到参数缺失只能报错返回;A2A 的 Task 是有状态的,可以"暂停任务 → 追问 → 等补充 → 原地恢复”:
Client Server (订票 Agent)
│ SendMessage("订一张去上海的机票") │
│ ────────────────────────────────────────────>
│ Task{id=T1, state=working} │
│ <────────────────────────────────────────────
│ TaskStatusUpdateEvent{ │
│ state=input-required, │ ← 任务暂停,
│ message="请问出发日期和舱位?"} │ 附带追问消息
│ <────────────────────────────────────────────
│ │
│ (Client 把问题转给用户,拿到答案) │
│ │
│ SendMessage("下周二,经济舱", │ ← 关键:带上
│ taskId=T1) │ 同一个 taskId
│ ────────────────────────────────────────────>
│ TaskStatusUpdateEvent{state=working} │ ← 任务原地恢复
│ <────────────────────────────────────────────
│ TaskArtifactUpdateEvent{订单确认} │
│ TaskStatusUpdateEvent{state=completed} │
│ <────────────────────────────────────────────
要点:
input-required,并在状态里附一条 Message说明需要什么(可以用 TextPart 自然语言问,也可以用 DataPart 给出结构化表单让对方精确填写);taskId,任务从暂停点恢复;input-required 不是终态——任务可以暂停任意久;auth-required:不是缺信息,而是缺授权(比如需要用户完成一次 OAuth 同意流程后任务才能继续)。AgentExecutor 发布 input-required 状态后直接返回、让出控制权;
当用户补充的消息到达时,框架会再次调用 execute(),此时
context.current_task 是同一个任务,历史消息都在:
async def execute(self, context, event_queue):
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
if context.current_task is None:
# —— 第一轮:发现信息不足,暂停追问 ——
await event_queue.enqueue_event(Task(id=context.task_id, ...))
if "日期" not in context.get_user_input():
await updater.requires_input( # state -> input-required
updater.new_agent_message(
[Part(text="请问出发日期和舱位?")])
)
return # 让出控制权,等补充
else:
# —— 第二轮:拿到补充信息,继续处理 ——
await updater.start_work()
answer = book_ticket(context.get_user_input())
await updater.add_artifact([Part(text=answer)], name="order")
await updater.complete()
一次合作往往包含多个任务。contextId 把它们串起来:
contextId = "c-123"(一次出差安排会话)
├── Task T1: 订机票 completed
├── Task T2: 订酒店 completed
└── Task T3: 报销申请 input-required(等发票)
contextId 下,Server 可以利用之前任务的上下文(如 T2 订酒店时
已知道 T1 的航班日期);ListTasks(按 contextId 过滤)可以列出整个会话的任务。