A2A Task 运行时指南:运行、流式传输、恢复与排错

A2A Task 运行时指南:运行、流式传输、恢复与排错

一个任务有两端:运行它的服务器,和跟踪它的客户端。这篇指南两端都管。对象页(Task 、Message 、Part 、Artifact )讲字段。这一页补上对象页没有的部分:运行任务时要做的决策、每个状态接受哪些操作、事件流的校验程序、从故障反推原因的对照表,以及从 v0.3 迁移时会踩的变化。

所有内容都对照 A2A v1.0.1 核对过:该标签下的 a2a.proto、规范本身,以及规范自带的示例交互。规范与 proto 不一致时,以 proto 为准(规范第 1.4 节)。规范自己前后矛盾时,本指南会明说,而不是悄悄选一边。

路线

  1. 选择响应形态与交付方式。
  2. 弄清每个状态允许什么。
  3. 正确地发出和读取事件,两端都一样。
  4. 校验轨迹:反复运行校验程序,直到输出 OK。
  5. 流、webhook 或后续消息出问题时恢复。
  6. 用固定的场景清单测试两端。

如果您来自 v0.3,请先读第 7 节 。

1. 选择响应形态与交付方式

SendMessage 返回 Task 或 Message,由智能体决定(第 3.1.1 节)。

情况返回规则与出处
简单交互,没有需要跟踪的东西Message对简单交互,智能体可以直接返回 Message(第 3.1.1 节)
回复之后工作仍在继续,或需要状态、产出物、更多输入Task返回 Task 时,处理在后台继续(第 3.3.3 节)
交付结果任务上的 Artifact消息不应承载任务产出;结果应当以产出物返回(第 3.7 节)
在承诺开展工作之前先提问Message澄清是消息的列明用途之一(第 3.7 节)

然后选择客户端如何跟踪任务:

您需要使用Agent Card 必须声明没有声明时,智能体必须返回
一个答案,工作很短SendMessage 并等待(默认)无不适用
先启动工作,稍后再回来SendMessage 加 returnImmediately: true,再用 GetTask无不适用
实时进度和部分产出物SendStreamingMessage;用 SubscribeToTask 重连capabilities.streaming: trueUnsupportedOperationError
无法保持连接的客户端由 taskPushNotificationConfig 或 CreateTaskPushNotificationConfig 配置的 webhookcapabilities.pushNotifications: truePushNotificationNotSupportedError

三个细节决定了这套做法在实际中能否奏效(第 3.2.2 和 3.2.4 节):

  • 默认就是等待。 returnImmediately 未设置或为 false 时,SendMessage 必须等到任务进入终态或中断状态才返回。一个很长的任务碰上很短的 HTTP 超时,会在这里失败,而不是在您的智能体里。
  • returnImmediately 不起作用的情形:智能体用 Message 回答、使用流式传输、推送配置(它们独立于执行模式工作)。
  • historyLength:不设置表示客户端不设上限、由服务器决定返回多少历史,0 表示不要历史,正数表示最多返回这么多条最近的消息。

2. 各状态允许什么

a2a.proto 把四个状态标为终态,两个标为中断状态,另外两个属于进行中。

状态类别类别的出处
TASK_STATE_SUBMITTED、TASK_STATE_WORKING进行中proto 中没有标为终态或中断状态
TASK_STATE_INPUT_REQUIRED、TASK_STATE_AUTH_REQUIRED中断proto 注释写明 “This is an interrupted state”
TASK_STATE_COMPLETED、TASK_STATE_FAILED、TASK_STATE_CANCELED、TASK_STATE_REJECTED终态proto 注释写明 “This is a terminal state”

规范对每个类别定死了什么(空格表示规范没有设定规则):

操作进行中中断终态
带该 taskId 的 SendMessage 或 SendStreamingMessage智能体可以接受(第 3.3.3 节)对 TASK_STATE_INPUT_REQUIRED,预期的下一步是带相同 taskId 和 contextId 的新消息(第 3.4.3 节)。对 TASK_STATE_AUTH_REQUIRED,智能体应当接受消息(第 7.6.1 节)UnsupportedOperationError(第 3.1.1 节)
CancelTask尝试取消,不保证成功(第 3.1.5 节)TaskNotCancelableError(第 3.1.5 节)
SubscribeToTask打开一个流;第一个事件是当前的 Task(第 3.1.6 节)同进行中:任何非终态任务都允许(第 3.1.6 节)UnsupportedOperationError(第 3.1.6 节)
GetTask返回当前状态(第 3.1.3 节)返回当前状态(第 3.1.3 节)返回最终状态;客户端在流结束或收到推送之后就是这样获取它的(第 3.1.3 节)
阻塞的 SendMessage继续等待返回(第 3.2.2 节)返回(第 3.2.2 节)
已打开的流保持打开见下文必须关闭(第 3.1.2 和 3.1.6 节)

规范留白的地方

这些地方,两个都正确的实现仍然可能不一致。请把您的选择写进智能体的文档。

  • 状态迁移。 规范没有迁移表。上面的规则就是规范定死的全部。它自己的示例首先展示的任务状态分别是 TASK_STATE_COMPLETED(第 6.1 节)、TASK_STATE_WORKING(第 6.2 节)、TASK_STATE_INPUT_REQUIRED(第 6.3 节)和 TASK_STATE_SUBMITTED(第 6.6 节),所以客户端不能要求 TASK_STATE_SUBMITTED 一定出现。
  • 流会在中断状态关闭吗? 三处文字的答案不同。第 3.1.2 和 3.1.6 节只要求在终态关闭。第 11.7 节描述更新会持续 “until the task reaches a terminal or interrupted state, at which point the stream closes”。第 7.6.1 节说,凭据经带外方式获得时,智能体设置 TASK_STATE_AUTH_REQUIRED 之后应当让活动的响应流保持打开。main(提交 98279d6)里这三处依然都在。**客户端规则:**流结束不等于任务结束,要读最后一个状态。**服务器规则:**选定一种行为并写进文档。
  • 取消处于中断状态的任务。 TaskNotCancelableError 只把终态当作示例,也没有任何规则排除中断状态的任务。请尝试取消并处理错误。
  • 历史。 不是每条消息都会存入 history,除非带外约定过,客户端不能依赖它(第 3.7 节)。
  • 重复。 SendMessage 可以是幂等的,智能体可以用 messageId 识别重复(第 3.3.1 节)。

3. 发出与读取事件

必须出现的字段

a2a.proto 把下列字段标为 REQUIRED。标为必填的数组必须至少有一个元素(第 5.7 节)。

事件成员必填字段
taskid、status(以及 status.state)
messagemessageId、role、parts
statusUpdatetaskId、contextId、status(以及 status.state)
artifactUpdatetaskId、contextId、artifact(以及 artifact.artifactId、artifact.parts)

每个事件恰好带有 task、message、statusUpdate、artifactUpdate 之一。一个 Part 在 text、raw、url、data 中恰好设置一个。

服务器清单

  • 在发出第一个字节之前就决定用 Message 还是 Task。只含消息的流恰好包含一个 Message,然后关闭(第 3.1.2 节)。
  • 任务流先发 Task,再按事件发生的顺序发 statusUpdate 和 artifactUpdate。事件不得重排(第 3.5.2 节)。
  • 中断状态的原因写进 status.message。对 TASK_STATE_AUTH_REQUIRED,除非细节已经通过带外方式或扩展约定,否则必须带这条消息(第 7.6.1 节)。
  • 用产出物交付结果。大的产出物,第一块不带 append,之后的块带 append: true,最后一块标 lastChunk: true(TaskArtifactUpdateEvent 的字段)。
  • 在终态关闭流。关闭之前可以再发一次最终的 Task 快照(第 11.7 节)。
  • 同一任务有多个流同时打开时,向每个流以相同顺序发送每个事件;关闭其中一个流不得影响其他流(第 3.5.2 节)。

客户端清单

  • 以 status.state 作为任务状态的依据。v1.0 没有 final 标志。
  • 流结束时读最后一个状态。终态:结束。中断状态:按下面的方式处理。其他状态:连接被切断,需要重连。
  • 用 SubscribeToTask 重连。它的第一个事件是当前的 Task,弥合了一次 GetTask 与新流之间的空档(第 3.1.6 节)。断开期间发出的状态消息不会重放,规范也说了不要依赖它们(第 3.7 节)。
  • 按 artifactId 拼接分块:带 append: true 的块扩展同 ID 的产出物,lastChunk: true 表示它结束。
  • 回答处于中断状态的任务,要发带相同 taskId 的消息。如果同时带 contextId,它必须是该任务自己的:contextId 与 taskId 不匹配的消息必须被拒绝(第 3.4.3 节)。要在同一会话中开始新工作,只发 contextId。任务 ID 始终由服务器分配(第 3.4.2 节)。
  • 任务处于 TASK_STATE_AUTH_REQUIRED、凭据经带外方式到达时,请保持一个流打开、注册 webhook,或用 GetTask 轮询;否则可能错过智能体恢复运行的时刻(第 7.6.2 节)。
  • Webhook 接收方必须回应 2xx;因为投递可能重复,应当幂等地处理通知;并且必须检查任务 ID 是自己预期的。发送方必须至少尝试投递一次,连续失败后可以停止(第 4.3.3 节)。通知可能只携带一个状态更新或产出物更新;需要完整状态时调用 GetTask。

4. 校验轨迹

轨迹是一个流或一次响应的全部事件,每行一个 JSON 对象,按到达顺序排列。把下面的程序保存为 task-trace-lint.jq。它只读轨迹,不发起任何网络调用。

# A2A task trace lint for v1.0.1.
# Usage: jq -s -r -f task-trace-lint.jq trace.jsonl
# A trace is one StreamResponse (or SendMessageResponse) object per line, in arrival order.
# Checks come from the REQUIRED annotations in a2a.proto and specification sections 3.1.2, 3.1.6, 3.2.3, 3.4.3 and 11.7.
# ERROR lines break the specification; WARN lines are advisory.

def blank: . == null or . == "" or . == [];
def states: ["TASK_STATE_SUBMITTED", "TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED",
  "TASK_STATE_CANCELED", "TASK_STATE_INPUT_REQUIRED", "TASK_STATE_REJECTED", "TASK_STATE_AUTH_REQUIRED"];
def terminal: ["TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"];
def interrupted: ["TASK_STATE_INPUT_REQUIRED", "TASK_STATE_AUTH_REQUIRED"];
def v03_states: {"submitted": "TASK_STATE_SUBMITTED", "working": "TASK_STATE_WORKING",
  "input-required": "TASK_STATE_INPUT_REQUIRED", "completed": "TASK_STATE_COMPLETED",
  "canceled": "TASK_STATE_CANCELED", "failed": "TASK_STATE_FAILED", "rejected": "TASK_STATE_REJECTED",
  "auth-required": "TASK_STATE_AUTH_REQUIRED"};
def v03_roles: {"user": "ROLE_USER", "agent": "ROLE_AGENT"};
def members: ["task", "message", "statusUpdate", "artifactUpdate"];

def part_checks($p):
  . as $part
  | if type != "object" then "ERROR \($p): expected a Part object"
    elif has("kind") or has("file") then "ERROR \($p): v0.3 Part shape; v1.0 sets exactly one of text, raw, url, data"
    else ([("text", "raw", "url", "data") as $k | select($part | has($k))] | length) as $n
      | if $n != 1 then "ERROR \($p): exactly one of text, raw, url, data must be set, found \($n)" else empty end
    end;

def message_checks($p):
  if type != "object" then "ERROR \($p): expected a Message object"
  else
    (if .messageId | blank then "ERROR \($p)/messageId: required string is missing or empty" else empty end),
    (.role as $r
     | if $r == null then "ERROR \($p)/role: required enum is missing"
       elif v03_roles[$r | tostring] then "ERROR \($p)/role: v0.3 value \($r | tojson), use \(v03_roles[$r | tostring])"
       elif (["ROLE_USER", "ROLE_AGENT"] | index($r)) == null then "ERROR \($p)/role: not ROLE_USER or ROLE_AGENT: \($r | tojson)"
       else empty end),
    (if (.parts | type) != "array" or (.parts | length) == 0 then "ERROR \($p)/parts: required array is missing or empty"
     else (.parts | to_entries[] | .key as $k | .value | part_checks("\($p)/parts/\($k)")) end)
  end;

def state_checks($p):
  . as $s
  | if $s == null or $s == "TASK_STATE_UNSPECIFIED" then "ERROR \($p): required state is missing or TASK_STATE_UNSPECIFIED"
    elif (states | index($s)) != null then empty
    elif v03_states[$s | tostring] then "ERROR \($p): v0.3 state name \($s | tojson), use \(v03_states[$s | tostring])"
    else "ERROR \($p): not a TaskState value: \($s | tojson)" end;

def status_checks($p):
  if type != "object" then "ERROR \($p): required object is missing"
  else (.state | state_checks("\($p)/state")), (if .message != null then .message | message_checks("\($p)/message") else empty end) end;

def artifact_checks($p):
  if type != "object" then "ERROR \($p): required object is missing"
  else
    (if .artifactId | blank then "ERROR \($p)/artifactId: required string is missing or empty" else empty end),
    (if (.parts | type) != "array" or (.parts | length) == 0 then "ERROR \($p)/parts: required array is missing or empty"
     else (.parts | to_entries[] | .key as $k | .value | part_checks("\($p)/parts/\($k)")) end)
  end;

def unwrap: if type == "object" and has("jsonrpc") and (.result | type) == "object" then .result else . end;
def kind: if type == "object" then [members[] as $m | select(has($m)) | $m] | .[0] else null end;
def state_of: if type == "object" then ((.task | objects | .status | objects | .state) // (.statusUpdate | objects | .status | objects | .state) // null) else null end;
def shaped: type == "object" and (has("kind") | not) and ([members[] as $m | select(has($m) and (.[$m] | type) == "object")] | length) == 1;

def event_checks($i):
  "event[\($i)]" as $p
  | if type != "object" then "ERROR \($p): expected a JSON object"
    elif has("kind") or (.statusUpdate | type) == "object" and (.statusUpdate | has("final")) then
      "ERROR \($p): v0.3 event shape (kind or final); v1.0 wraps the payload in exactly one of task, message, statusUpdate, artifactUpdate"
    elif ([members[] as $m | select(has($m))] | length) != 1 then
      "ERROR \($p): expected exactly one of task, message, statusUpdate, artifactUpdate, found \([members[] as $m | select(has($m))] | length)"
    elif ([members[] as $m | select(has($m) and (.[$m] | type) != "object")] | length) > 0 then
      "ERROR \($p): the payload must be a JSON object"
    elif has("task") then
      (if .task.id | blank then "ERROR \($p)/task/id: required string is missing or empty" else empty end),
      (.task.status | status_checks("\($p)/task/status"))
    elif has("message") then (.message | message_checks("\($p)/message"))
    elif has("statusUpdate") then
      (.statusUpdate | (if .taskId | blank then "ERROR \($p)/statusUpdate/taskId: required string is missing or empty" else empty end),
        (if .contextId | blank then "ERROR \($p)/statusUpdate/contextId: required string is missing or empty" else empty end),
        (.status | status_checks("\($p)/statusUpdate/status")))
    else
      (.artifactUpdate | (if .taskId | blank then "ERROR \($p)/artifactUpdate/taskId: required string is missing or empty" else empty end),
        (if .contextId | blank then "ERROR \($p)/artifactUpdate/contextId: required string is missing or empty" else empty end),
        (.artifact | artifact_checks("\($p)/artifactUpdate/artifact")))
    end;

# Trace-level rules: order, identity, termination and artifact chunking. They run only when every event is well formed.
def trace_checks:
  . as $e
  | ($e | length) as $n
  | ($e[0].task // {}) as $first
  | ($e | map(state_of)) as $st
  | ($e | map(select(kind == "task" or kind == "statusUpdate") | state_of) | .[-1]) as $last
  | ([range(0; $n) | select($st[.] as $s | $s != null and (terminal | index($s)) != null)] | .[0]) as $end
  | (if $n > 1 and ($e[0] | kind) != "task" and ($e[0] | kind) != "message" then
       "ERROR event[0]: a stream starts with a task or a message, found \($e[0] | kind // "nothing")" else empty end),
    (if $n > 1 and ($e[0] | kind) == "message" then "ERROR event[1]: a message-only stream holds exactly one message" else empty end),
    (range(1; $n) as $i | $e[$i] as $x
      | (if ($x | kind) == "message" and ($e[0] | kind) == "task" then "ERROR event[\($i)]: message inside a task stream; only status and artifact updates follow the task" else empty end),
        (if ($x | kind) == "task" and ($e[0] | kind) == "task" and ($end == null or $i != $end + 1 or $x.task.status.state != $st[$end])
           then "ERROR event[\($i)]: task snapshot inside a stream; only one final snapshot after the terminal state is allowed" else empty end),
        (($x.statusUpdate // $x.artifactUpdate) as $u
          | if $u != null and ($first.id // null) != null and $u.taskId != null and $u.taskId != $first.id
            then "ERROR event[\($i)]/\($x | kind)/taskId: \($u.taskId | tojson) differs from the stream task \($first.id | tojson)" else empty end,
            if $u != null and ($first.contextId // null) != null and $u.contextId != null and $u.contextId != $first.contextId
            then "ERROR event[\($i)]/\($x | kind)/contextId: \($u.contextId | tojson) differs from the stream context \($first.contextId | tojson)" else empty end),
        (if $end != null and $i > $end and ($x | kind) != "task" then "ERROR event[\($i)]: event after terminal state \($st[$end]) at event[\($end)]" else empty end)),
    (reduce range(0; $n) as $i ({seen: {}, closed: {}, out: []};
        ($e[$i]) as $x
        | if ($x | kind) == "task" then .seen += ([$x.task.artifacts[]? | {(.artifactId | tostring): true}] | add // {})
          elif ($x | kind) == "artifactUpdate" then
            ($x.artifactUpdate.artifact.artifactId | tostring) as $id
            | (if $x.artifactUpdate.append == true and (.seen[$id] | not) then .out += ["WARN event[\($i)]/artifactUpdate: append for artifact \($id | tojson) before any earlier chunk"]
               elif $x.artifactUpdate.append == true and .closed[$id] then .out += ["WARN event[\($i)]/artifactUpdate: append for artifact \($id | tojson) after its lastChunk"] else . end)
            | .seen[$id] = true
            | (if $x.artifactUpdate.lastChunk == true then .closed[$id] = true else . end)
          else . end) | .out[]),
    (if $n > 1 and ($e[0] | kind) == "task" and (["TASK_STATE_SUBMITTED", "TASK_STATE_WORKING"] | index($last)) != null
     then "WARN event[\($n - 1)]: the trace ends in \($last) before a terminal or interrupted state; the stream was cut, resubscribe" else empty end);

map(unwrap) as $e
| if ($e | length) == 0 then "ERROR trace: no events"
  else
    ([range(0; $e | length) as $i | $e[$i] | event_checks($i)] + (if $e | all(shaped) then [$e | trace_checks] else [] end))
    | if length == 0 then "OK: \($e | length) event(s), last state \($e | map(state_of) | map(select(. != null)) | .[-1] // "none")" else .[] end
  end

从流中截取轨迹,只保留 data: 的内容。JSON-RPC 响应会被自动拆开:

sed -n 's/^data: //p' stream.txt > trace.jsonl
jq -s -r -f task-trace-lint.jq trace.jsonl

ERROR 行表示违反了规范或 proto。WARN 行只是提示。只有当每个事件的形状都正确时,才会运行顺序和标识的规则,所以请先修复只涉及单个事件的 ERROR。只含一个事件的文件被当作一次响应或一个 webhook 载荷,所以涉及流顺序的规则只对含两个及以上事件的文件生效。在 CI 中:

if jq -s -r -f task-trace-lint.jq trace.jsonl | grep -q '^ERROR'; then exit 1; fi

它对官方示例流的判断

规范第 6 节的示例交互是作为说明性例子写的,其中好几个省略了 a2a.proto 要求的字段。下表是校验程序对每个示例在 v1.0.1 标签和 main(提交 98279d6,2026-10-20)上的判断。想自己复现,请从规范中取出 JSON,每个事件放一行。取第 6.2 节的流:

git show v1.0.1:docs/specification.md \
  | awk '/^### 6\.2\./{f=1} /^### 6\.3\./{f=0} f' | sed -n 's/^data: //p' > sample-6.2.jsonl

另一列把 v1.0.1 换成 main 即可。

示例在 v1.0.1在 main
第 6.1 节,已完成的任务OK: 1 event(s), last state TASK_STATE_COMPLETED相同
第 6.2 节,流式传输ERROR event[1]/artifactUpdate/contextId: required string is missing or empty
ERROR event[1]/artifactUpdate/artifact/artifactId: required string is missing or empty
ERROR event[2]/statusUpdate/contextId: required string is missing or empty
ERROR event[1]/artifactUpdate/artifact/artifactId: required string is missing or empty
第 6.3 节,需要输入ERROR event[0]/task/status/message/messageId: required string is missing or empty相同
第 6.6 节,任务响应OK: 1 event(s), last state TASK_STATE_SUBMITTED相同
第 6.6 节,webhook 载荷OK: 1 event(s), last state TASK_STATE_COMPLETED相同

不要把这些示例直接抄进服务器。请按第 3 节 的必填字段来构建,再运行校验程序。下面这个流是 main 中第 6.2 节的示例补上缺失的那一个字段,它能通过校验:

{"task":{"id":"task-uuid","contextId":"context-uuid","status":{"state":"TASK_STATE_WORKING"}}}
{"artifactUpdate":{"taskId":"task-uuid","contextId":"context-uuid","artifact":{"artifactId":"artifact-uuid","parts":[{"text":"# Climate Change Report\n\n"}]}}}
{"statusUpdate":{"taskId":"task-uuid","contextId":"context-uuid","status":{"state":"TASK_STATE_COMPLETED"}}}

故意改坏它

校验程序只有在该失败时失败才有用。把上面的流保存为 stream-ok.jsonl。每一行只改动一处,并给出由此得到的那一条结果:

jq -s -c '<edit> | .[]' stream-ok.jsonl | jq -s -r -f task-trace-lint.jq
改动结果
del(.[0])ERROR event[0]: a stream starts with a task or a message, found artifactUpdate
del(.[2])WARN event[1]: the trace ends in TASK_STATE_WORKING before a terminal or interrupted state; the stream was cut, resubscribe
. + [.[1]]ERROR event[3]: event after terminal state TASK_STATE_COMPLETED at event[2]
.[1].artifactUpdate.taskId = "other"ERROR event[1]/artifactUpdate/taskId: "other" differs from the stream task "task-uuid"
.[1].artifactUpdate.contextId = "other"ERROR event[1]/artifactUpdate/contextId: "other" differs from the stream context "context-uuid"
.[1].artifactUpdate.append = trueWARN event[1]/artifactUpdate: append for artifact "artifact-uuid" before any earlier chunk
.[1].artifactUpdate.artifact.artifactId = ""ERROR event[1]/artifactUpdate/artifact/artifactId: required string is missing or empty
.[1].artifactUpdate.artifact.parts = []ERROR event[1]/artifactUpdate/artifact/parts: required array is missing or empty
.[0].task.status.state = "TASK_STATE_UNSPECIFIED"ERROR event[0]/task/status/state: required state is missing or TASK_STATE_UNSPECIFIED
.[2].statusUpdate.status.state = "completed"ERROR event[2]/statusUpdate/status/state: v0.3 state name "completed", use TASK_STATE_COMPLETED
.[2].statusUpdate.status.message = {"messageId":"m1","role":"agent","parts":[{"text":"done"}]}ERROR event[2]/statusUpdate/status/message/role: v0.3 value "agent", use ROLE_AGENT
.[1].artifactUpdate.artifact.parts[0] = {"kind":"text","text":"x"}ERROR event[1]/artifactUpdate/artifact/parts/0: v0.3 Part shape; v1.0 sets exactly one of text, raw, url, data
.[2].statusUpdate.final = trueERROR event[2]: v0.3 event shape (kind or final); v1.0 wraps the payload in exactly one of task, message, statusUpdate, artifactUpdate
.[1] = {"message":{"messageId":"m1","role":"ROLE_AGENT","parts":[{"text":"hi"}]}}ERROR event[1]: message inside a task stream; only status and artifact updates follow the task

它不检查什么

  • 您的服务器是否执行了第 2 节 中的操作与状态规则。它读的是事件,不是请求;第 6 节 列出了要试的请求。
  • 状态之间的迁移。规范没有定义,所以校验程序接受任意顺序的非终态。
  • 跨多个任务的 contextId 是否一致、产出物是否完整、raw 中的字节是否是合法的 base64。
  • 从多个请求中收集到的一组 webhook 投递。请一次只检查一个载荷。

5. 出问题时

您看到的现象可能的原因修复
发出后续消息后得到 UnsupportedOperationError任务已是终态(第 3.1.1 节)在同一个 contextId 下开始新任务;用 referenceTaskIds 指向之前的任务
TaskNotCancelableError任务已是终态(第 3.1.5 节)调用 GetTask。如果状态是 TASK_STATE_CANCELED,就完成了
后续消息得到 TaskNotFoundErrortaskId 从未签发过、任务已被清除,或客户端无权访问(第 3.4.2 和 3.3.2 节)。规范说服务器不应区分这几种情况只使用服务器返回过的 taskId;旧任务已不存在时开始新任务
后续消息被拒绝消息带的 contextId 与任务自己的不同(第 3.4.3 节)只发 taskId,或把两者都从任务中复制
SendMessage 卡住或超时v1.0 默认等待任务完成(第 3.2.2 节)设置 returnImmediately: true 并轮询或订阅,或改用 SendStreamingMessage
流结束了,任务却没结束中断状态,或连接被切断(见第 2 节 )读最后一个状态。中断状态:回答它。否则调用 SubscribeToTask 或 GetTask
SendStreamingMessage 得到 UnsupportedOperationErrorcapabilities.streaming 为 false 或缺失(第 3.3.4 节)改用 SendMessage 并轮询,或修正卡片
SubscribeToTask 得到 UnsupportedOperationError任务已是终态,或没有声明流式传输(第 3.1.6 和 3.3.4 节)调用 GetTask 获取最终状态
PushNotificationNotSupportedErrorcapabilities.pushNotifications 为 false 或缺失(第 3.3.4 节)改用轮询或流式传输
Webhook 始终不触发,或触发两次发送方连续失败后会停止,至少投递一次,也可能重复(第 4.3.3 节)快速回应 2xx,按任务 ID 和状态去重,并回退到 GetTask
经 HTTP+JSON 调用 SubscribeToTask 被以路由或方法错误拒绝规范的表格(第 5.3 和 11.3.2 节)写的是 POST /tasks/{id}:subscribe;a2a.proto 的 HTTP 注解写的是 GET。两者在 main 上都没改,而 proto 优先(第 1.4 节)服务端提供并调用 GET;对端以方法错误应答时,再试另一种动词
客户端拒绝您的事件缺少必填字段,或枚举值仍是 v0.3 的写法对抓取的轨迹运行校验程序
升级后解析器失败仍在使用 kind、小写状态、file part 或 final见第 7 节
产出物被截断或重复append 和 lastChunk 用错,或按错误的 ID 拼接分块按 artifactId 拼接;校验程序会对没有前序分块的 append、lastChunk 之后的分块给出警告

错误名称及其在 JSON-RPC、gRPC、HTTP 中的映射见 A2A 错误码 。

6. 测试场景

用这些场景测试服务器(S)或客户端(C)。标有“轨迹检查”的行可以由校验程序覆盖。

编号端场景预期出处
T1S发送一条不需要任务的简单消息一个 message 事件,然后流关闭。轨迹检查第 3.1.2 节
T2S以流的方式把任务运行到完成先是 task,然后按顺序的更新,在终态关闭。轨迹检查第 3.1.2 和 3.5.2 节
T3S向终态任务发送消息UnsupportedOperationError第 3.1.1 节
T4S取消终态任务TaskNotCancelableError第 3.1.5 节
T5S订阅终态任务UnsupportedOperationError第 3.1.6 节
T6S订阅运行中的任务第一个事件是当前的 Task。轨迹检查第 3.1.6 节
T7S先发一条 taskId 未知的消息,再发一条 contextId 不匹配的先是 TaskNotFoundError,然后是拒绝第 3.4.2 和 3.4.3 节
T8S在卡片两者都未声明时使用流式传输或推送UnsupportedOperationError、PushNotificationNotSupportedError第 3.3.4 节
T9S先用默认值发送,再用 returnImmediately: true 发送前者在终态或中断状态返回;后者立即返回任务当前的样子第 3.2.2 节
T10S在同一任务上打开两个流,关闭其中一个两个流收到相同的事件、相同的顺序;另一个保持打开第 3.5.2 节
T11C收到一个以中断状态结束的流客户端用相同的 taskId 回答;不报告失败。轨迹检查第 3.4.3 和 7.6.2 节
T12C任务进行中丢失了流客户端重新订阅,并从第一个事件(当前的 Task)继续。轨迹检查第 3.1.6 节
T13C两次收到同一个 webhook只产生一次效果;两次都回应 2xx第 4.3.3 节

7. 迁移 v0.3 的运行时代码

对比对象是 v0.3.0 的 JSON schema(specification/json/a2a.json,schema 没写的地方参照 v0.3.0 的 gRPC proto)和 v1.0.1 的 a2a.proto,逐字段比较。协议其余部分的迁移见完整迁移指南 。

方面v0.3.0v1.0.1会坏的地方
状态submitted、working、input-required、completed、canceled、failed、rejected、auth-required、unknown(v0.3.0 的 gRPC proto 把已取消写作 TASK_STATE_CANCELLED)TASK_STATE_SUBMITTED、TASK_STATE_WORKING、TASK_STATE_INPUT_REQUIRED、TASK_STATE_COMPLETED、TASK_STATE_CANCELED、TASK_STATE_FAILED、TASK_STATE_REJECTED、TASK_STATE_AUTH_REQUIRED;TASK_STATE_UNSPECIFIED(“未知或不确定”)取代了 unknown对旧字符串的比较与 switch
角色user、agentROLE_USER、ROLE_AGENT发送或匹配旧的角色字符串
类型标记Task、Message、每个 part 以及两种更新事件都必须带 kind没有 kind;由成员名表明类型(task、message、statusUpdate、artifactUpdate)依赖 kind 的解析器与序列化器
PartTextPart {kind, text}、FilePart {kind, file: {name, mimeType, bytes 或 uri}}、DataPart {kind, data}一个 Part,在 text、raw、url、data 中恰好设置一个,另有 filename 和 mediaType文件与数据的处理代码
流的结束状态更新必须带 final没有 final;读取状态等待 final 的代码永远不会结束
等待blocking: true 请求服务器等待;false 或不设置则立即返回任务(gRPC proto)returnImmediately 默认 false,调用会等到终态或中断状态才返回超时,见下文
historyLength 为 0gRPC proto 写明 “if 0, the history will be unlimited”;JSON schema 只说 “the number of most recent messages”0 表示不要任何消息;不设置表示不限制曾用 0 表示“全部”的客户端现在拿不到任何消息
发送时的推送配置configuration.pushNotificationConfigconfiguration.taskPushNotificationConfigWebhook 始终没有被注册
JSON-RPC 方法message/send、message/stream、tasks/get、tasks/cancel、tasks/resubscribe、tasks/pushNotificationConfig/setSendMessage、SendStreamingMessage、GetTask、CancelTask、SubscribeToTask、CreateTaskPushNotificationConfig按方法名路由

“等待”这一行是悄悄失败的那一个。按 v0.3.0 的 gRPC proto,从不设置 blocking 的客户端请求的是立即返回的答案。同样的代码对着 v1.0.1 智能体,会一直等到任务进入终态或中断状态,而短超时会把它变成一个看起来像网络问题的错误。

相关页面

出处:A2A 官方规范:核心操作(3.1)、操作语义(3.3)、多轮交互(3.4)、任务更新交付机制(3.5)、消息与产出物(3.7)、流式事件与推送通知对象(4.2 与 4.3)、常见工作流(6)、任务内授权(7.6)、HTTP+JSON 流式传输(11.7) ;v1.0.1 标签下的 a2a.proto;v0.3.0 JSON schema ;规范在 v1.0.1 标签和 main 提交 98279d6 的示例交互