A2A 任务幂等与恢复:重复消息、断流与停滞任务

A2A 任务幂等与恢复:重复消息、断流与停滞任务

网络会断、客户端会重试、进程会重启。协议不会替您吸收这些故障:它给了 messageId、contextId 和两条恢复通道,把"恰好一次"的效果留给实现。这里给的是把这三样东西用起来的模式,和能重复运行的恢复探针。

状态与操作的含义见Task 对象页 与Task 运行时指南 ;重启与重试的发布检查清单见生产就绪检查 第 5 节。

恢复决策树

连接中断或响应超时后,客户端先判断任务去到了哪里,再决定动作:

流中断 / 响应超时
  ├─ 任务已到终态(COMPLETED / FAILED / CANCELED / REJECTED)?
  │    └─ 是:GetTask 取终态与产出物,结束
  ├─ 订阅通道可用(capabilities.streaming)?
  │    └─ 是:SubscribeToTask 重连,从事件流继续
  │         └─ 服务端不支持从任意点续传时,以收到的事件为准,
  │            重复事件按序号去重(见下)
  ├─ 只剩轮询?
  │    └─ GetTask 按退避轮询,直到终态或停滞判定
  └─ 停滞判定:超过业务上限仍停在 SUBMITTED / WORKING
       ├─ 客户端:CancelTask,或在新任务里重发请求(同 contextId)
       └─ 服务端:把任务转入 FAILED 并写明原因,不要永远 WORKING

不要把"流结束"当成"任务结束":连接断开只说明您不再看得到它,任务本身可能仍在服务端推进。

1. 重复消息:messageId 是幂等键

messageId 由发送方生成,这是协议给幂等留的钩子:客户端超时重发时用同一个 messageId,服务端看到已处理过的 ID 就返回当时的结果,而不是重跑一遍工作。

场景客户端行为服务端期望
超时后重发同一条消息原样重发,messageId 不变识别重复,返回首次处理结果
对话中继续(同一任务)新 messageId + taskId(或 contextId)作为新输入推进任务
任务结束后补充需求同 contextId、不带 taskId 的新消息创建新任务(终态任务不可重启)
重复向终态任务发消息同 messageId 重发幂等返回结果,或按实现定义处理

实现要点:

  1. 服务端把 (taskId 或 contextId, messageId) 作为幂等键,存储首次处理的结果指针(任务 ID、响应或错误);
  2. 幂等窗口覆盖"客户端放弃之前"的整个周期,不只覆盖几秒;
  3. 业务副作用(外部调用、写库)发生在幂等键登记之后,避免"回执未落库、副作用已发生"的缝隙;
  4. 需要跨消息的幂等时用 contextId:同一上下文的新任务可以携带上游业务键,由您的实现定义去重范围。

协议不保证业务层面的恰好一次——referenceTaskIds、contextId 只提供关联信息,去重策略由实现定义。这一点生产就绪检查 第 5 节同样列为发布边界。

2. 重复投递:接收侧的三个去重点

恢复路径上有三个位置会遇到"同一事件第二次到达":

位置去重依据
流事件(重连后)事件序号 / TaskArtifactUpdateEvent 的 lastChunk 分界
推送通知任务 ID + 状态;流式通知按事件序号(见推送接收方加固 第 4 节)
产出物分块artifactId + append / lastChunk,重复块覆盖或跳过

lastChunk 为 true 之前,同一 artifactId 的分块都可能是重传;客户端缓存按 artifactId 聚合,不假设块数。

3. 重启恢复:给已接收任务一个结局

服务端重启后,已接收的任务只有两种可接受的结局:按产品契约继续,或以明确结果失败。实现模式:

启动 → 加载持久化的任务记录
  ├─ 幂等键登记过的任务 → 重放登记结果,不重跑副作用
  ├─ 已在执行的任务 → 恢复执行,或转 FAILED(原因写"服务重启")
  └─ 无法恢复的 → FAILED + 可读原因;绝不停回 WORKING 等一个不会来的执行器

把"重启后任务去哪了"写进产品契约,客户端才能决定重试还是放弃;这一点在生产就绪检查 第 5 节的检查表里对应第 3 项。

4. 恢复探针

两条探针分别验证"重复消息不产生重复工作"和"断流后终态不丢"。它们以Task 运行时指南 第 4 节的轨迹校验器为基座,只列出恢复相关的断言:

#!/usr/bin/env bash
set -euo pipefail

: "${A2A_BASE_URL:?Set A2A_BASE_URL}"
: "${A2A_VALID_TOKEN:?Set A2A_VALID_TOKEN}"

send() { # body -> response
  curl --silent -H "Authorization: Bearer $A2A_VALID_TOKEN" \
    -H 'Content-Type: application/json' -d "$1" "$A2A_BASE_URL"
}

# Probe 1 — duplicate message is idempotent:
#   send the same message twice with one messageId; the observable task
#   artifacts must not double. Pick a task whose artifact count is stable.
MSG_ID="probe-$$-$(date +%s)"
BODY="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"message/send\",\"params\":{\"message\":{\"role\":\"role_user\",\"parts\":[{\"kind\":\"text\",\"text\":\"count words in: one two three\"}],\"messageId\":\"$MSG_ID\",\"contextId\":\"probe-ctx-$MSG_ID\"}}}"

first="$(send "$BODY")"
second="$(send "$BODY")"

task_id="$(printf '%s' "$first" | jq -r '.result.taskId // empty')"
[[ -n "$task_id" ]] || { printf 'FAIL first send did not create a task\n' >&2; exit 1; }
first_count="$(printf '%s' "$first" | jq '[.result.artifacts[]?] | length')"
second_count="$(printf '%s' "$second" | jq '[.result.artifacts[]?] | length')"
[[ "$first_count" == "$second_count" ]] \
  || { printf 'FAIL duplicate message produced extra artifacts (%s vs %s)\n' "$first_count" "$second_count" >&2; exit 1; }
printf 'OK   duplicate message is idempotent (taskId=%s, artifacts=%s)\n' "$task_id" "$first_count"

# Probe 2 — terminal state survives a dropped stream:
#   1. start a streaming task, read the first event, then kill the connection
#   2. SubscribeToTask (or GetTask when streaming is unavailable)
#   3. assert the task reaches a terminal state with the same taskId

探针 2 需要中断一条流式连接(curl --max-time 1 读首个事件后退出),再按恢复决策树重连;完整的事件顺序断言交给轨迹校验器,这里只断言终态可达且任务 ID 不变。两个探针都可以重复运行:它们自己就是幂等的。

5. 观测:让停滞可见

  • 停滞告警:WORKING 超过业务 P99 时长仍无事件的任务数;命中时给出任务年龄分布与最近事件;
  • 终态分布:COMPLETED / FAILED / CANCELED / REJECTED 比例,FAILED 按原因聚类;
  • 重复率:命中幂等键的消息占比——它同时反映网络质量与客户端重试策略;
  • 恢复指标:重连后成功取回终态的比例、重启后任务被终结(而非悬挂)的数量。

字段命名与生产就绪检查 第 4 节的日志约定保持一致,任务 ID 只做关联、不做授权。

相关页面

版本与验证说明

对齐 A2A v1.0.1。message/send、role_user、taskId、contextId、lastChunk 等名称以数据结构 为准;探针 1 在示例端点上验证过幂等行为分类,探针 2 的重连断言依赖站点的流式实现,属于部署期检查。停滞阈值、幂等窗口时长是部署参数,本文只约束"必须存在且可观测"。

来源:A2A 数据结构 、任务的生命周期 。