A2A Task 运行时指南:运行、流式传输、恢复与排错
一个任务有两端:运行它的服务器,和跟踪它的客户端。这篇指南两端都管。对象页(Task 、Message 、Part 、Artifact )讲字段。这一页补上对象页没有的部分:运行任务时要做的决策、每个状态接受哪些操作、事件流的校验程序、从故障反推原因的对照表,以及从 v0.3 迁移时会踩的变化。
所有内容都对照 A2A v1.0.1 核对过:该标签下的 a2a.proto、规范本身,以及规范自带的示例交互。规范与 proto 不一致时,以 proto 为准(规范第 1.4 节)。规范自己前后矛盾时,本指南会明说,而不是悄悄选一边。
路线
- 选择响应形态与交付方式。
- 弄清每个状态允许什么。
- 正确地发出和读取事件,两端都一样。
- 校验轨迹:反复运行校验程序,直到输出
OK。 - 流、webhook 或后续消息出问题时恢复。
- 用固定的场景清单测试两端。
如果您来自 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: true | UnsupportedOperationError |
| 无法保持连接的客户端 | 由 taskPushNotificationConfig 或 CreateTaskPushNotificationConfig 配置的 webhook | capabilities.pushNotifications: true | PushNotificationNotSupportedError |
三个细节决定了这套做法在实际中能否奏效(第 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 节)。
| 事件成员 | 必填字段 |
|---|---|
task | id、status(以及 status.state) |
message | messageId、role、parts |
statusUpdate | taskId、contextId、status(以及 status.state) |
artifactUpdate | taskId、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.jsonlERROR 行表示违反了规范或 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 emptyERROR event[1]/artifactUpdate/artifact/artifactId: required string is missing or emptyERROR 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 = true | WARN 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 = true | ERROR 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,就完成了 |
后续消息得到 TaskNotFoundError | taskId 从未签发过、任务已被清除,或客户端无权访问(第 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 得到 UnsupportedOperationError | capabilities.streaming 为 false 或缺失(第 3.3.4 节) | 改用 SendMessage 并轮询,或修正卡片 |
SubscribeToTask 得到 UnsupportedOperationError | 任务已是终态,或没有声明流式传输(第 3.1.6 和 3.3.4 节) | 调用 GetTask 获取最终状态 |
PushNotificationNotSupportedError | capabilities.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)。标有“轨迹检查”的行可以由校验程序覆盖。
| 编号 | 端 | 场景 | 预期 | 出处 |
|---|---|---|---|---|
| T1 | S | 发送一条不需要任务的简单消息 | 一个 message 事件,然后流关闭。轨迹检查 | 第 3.1.2 节 |
| T2 | S | 以流的方式把任务运行到完成 | 先是 task,然后按顺序的更新,在终态关闭。轨迹检查 | 第 3.1.2 和 3.5.2 节 |
| T3 | S | 向终态任务发送消息 | UnsupportedOperationError | 第 3.1.1 节 |
| T4 | S | 取消终态任务 | TaskNotCancelableError | 第 3.1.5 节 |
| T5 | S | 订阅终态任务 | UnsupportedOperationError | 第 3.1.6 节 |
| T6 | S | 订阅运行中的任务 | 第一个事件是当前的 Task。轨迹检查 | 第 3.1.6 节 |
| T7 | S | 先发一条 taskId 未知的消息,再发一条 contextId 不匹配的 | 先是 TaskNotFoundError,然后是拒绝 | 第 3.4.2 和 3.4.3 节 |
| T8 | S | 在卡片两者都未声明时使用流式传输或推送 | UnsupportedOperationError、PushNotificationNotSupportedError | 第 3.3.4 节 |
| T9 | S | 先用默认值发送,再用 returnImmediately: true 发送 | 前者在终态或中断状态返回;后者立即返回任务当前的样子 | 第 3.2.2 节 |
| T10 | S | 在同一任务上打开两个流,关闭其中一个 | 两个流收到相同的事件、相同的顺序;另一个保持打开 | 第 3.5.2 节 |
| T11 | C | 收到一个以中断状态结束的流 | 客户端用相同的 taskId 回答;不报告失败。轨迹检查 | 第 3.4.3 和 7.6.2 节 |
| T12 | C | 任务进行中丢失了流 | 客户端重新订阅,并从第一个事件(当前的 Task)继续。轨迹检查 | 第 3.1.6 节 |
| T13 | C | 两次收到同一个 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.0 | v1.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、agent | ROLE_USER、ROLE_AGENT | 发送或匹配旧的角色字符串 |
| 类型标记 | Task、Message、每个 part 以及两种更新事件都必须带 kind | 没有 kind;由成员名表明类型(task、message、statusUpdate、artifactUpdate) | 依赖 kind 的解析器与序列化器 |
| Part | TextPart {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 为 0 | gRPC proto 写明 “if 0, the history will be unlimited”;JSON schema 只说 “the number of most recent messages” | 0 表示不要任何消息;不设置表示不限制 | 曾用 0 表示“全部”的客户端现在拿不到任何消息 |
| 发送时的推送配置 | configuration.pushNotificationConfig | configuration.taskPushNotificationConfig | Webhook 始终没有被注册 |
| JSON-RPC 方法 | message/send、message/stream、tasks/get、tasks/cancel、tasks/resubscribe、tasks/pushNotificationConfig/set | SendMessage、SendStreamingMessage、GetTask、CancelTask、SubscribeToTask、CreateTaskPushNotificationConfig | 按方法名路由 |
“等待”这一行是悄悄失败的那一个。按 v0.3.0 的 gRPC proto,从不设置 blocking 的客户端请求的是立即返回的答案。同样的代码对着 v1.0.1 智能体,会一直等到任务进入终态或中断状态,而短超时会把它变成一个看起来像网络问题的错误。
相关页面
- Task 、Message 、Part 、Artifact :任务所携带的对象。
- 任务的一生 和流式传输与异步操作 :概念与 webhook 安全。
- A2A 错误码 :上文使用的错误名称。
- A2A 互操作速查 :各绑定下的路由、版本与参数变化,包括任务之外的操作。
- Agent Card 实现指南 :声明本指南所依赖的能力。
出处: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
的示例交互