任务处理

任务处理——Artifact、流式(SSE)与推送通知

Artifact:任务的"交付物”

Artifact 是 Server 处理任务产生的正式输出,和 Message 有明确分工:

  • Message:过程中的沟通(提问、确认、进度说明、思考性回复);
  • Artifact:最终交付物(报告、图片、JSON 数据、生成的文件)。
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 小时级长任务、移动端

流式(Streaming over SSE)

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

推送通知(Push Notifications)

如果任务要跑几小时,Client 不想挂着连接,可以注册一个 webhook:

Client                         Server                    Client 的 Webhook
  │ CreateTaskPushNotification   │                              │
  │ Config(url=https://my/hook) │                               │
  │ ────────────────────────────>                               │
  │ (断开)                      │   ...数小时后任务完成...        │
  │                              │  POST https://my/hook        │
  │                              │ ────────────────────────────>│
  │                              │   (带任务 id + 新状态)        │
  │  GetTask(id) 拉取完整结果      │                              │
  │ ────────────────────────────>                               │

多轮交互:input-required 与人机协同

为什么需要 input-required

真实任务很少一句话说清。比如你对订票 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}      │
  │ <────────────────────────────────────────────

要点:

  1. Server 把任务置为 input-required,并在状态里附一条 Message说明需要什么(可以用 TextPart 自然语言问,也可以用 DataPart 给出结构化表单让对方精确填写);
  2. Client 回复时在 Message 里带上同一个 taskId,任务从暂停点恢复;
  3. input-required 不是终态——任务可以暂停任意久;
  4. 类似的还有 auth-required:不是缺信息,而是缺授权(比如需要用户完成一次 OAuth 同意流程后任务才能继续)。

Server 端实现(SDK 视角)

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 把它们串起来:

contextId = "c-123"(一次出差安排会话)
 ├── Task T1: 订机票     completed
 ├── Task T2: 订酒店     completed
 └── Task T3: 报销申请   input-required(等发票)
  • 同一 contextId 下,Server 可以利用之前任务的上下文(如 T2 订酒店时 已知道 T1 的航班日期);
  • 终态任务不能复活,但同 context 开新任务可以自然延续对话;
  • 用 ListTasks(按 contextId 过滤)可以列出整个会话的任务。