A2A Task Runtime Guide: Run, Stream, Resume, Recover

A2A Task Runtime Guide: Run, Stream, Resume, Recover

A task has two sides: the server that runs it and the client that follows it. This guide is for both. The object pages (Task , Message , Part , Artifact ) explain the fields. This page adds what they do not have: the decisions you make when you run a task, which operation each state accepts, a validator for event streams, a map from failures back to causes, and the changes that bite when you move from v0.3.

Everything is checked against A2A v1.0.1: the a2a.proto at that tag, the specification, and the specification’s own sample exchanges. Where the specification and the proto disagree, the proto wins (specification section 1.4). Where the specification disagrees with itself, this guide says so instead of picking a side silently.

The path

  1. Choose the response shape and delivery mode.
  2. Know what each state allows.
  3. Emit and read events correctly, on both sides.
  4. Validate a trace: run the checker until it prints OK.
  5. Recover when a stream, a webhook, or a follow-up goes wrong.
  6. Test both sides with a fixed list of scenarios.

If you are coming from v0.3, read section 7 first.

1. Choose the response shape and delivery mode

SendMessage returns either a Task or a Message, and the agent decides which (section 3.1.1).

SituationReturnRule and source
A simple exchange with nothing to trackA MessageThe agent MAY return a direct Message for simple interactions (section 3.1.1)
Work that continues after the reply, or needs states, artifacts, or more inputA TaskWhen a Task is returned, processing continues in the background (section 3.3.3)
Delivering resultsArtifacts on the taskMessages SHOULD NOT carry task outputs; results SHOULD be returned as artifacts (section 3.7)
Asking a question before committing to workA MessageClarification is one of the listed roles of a message (section 3.7)

Then choose how the client follows the task:

You needUseThe Agent Card must declareIf it does not, the agent MUST return
One answer, short workSendMessage and wait (the default)nothingnot applicable
Start the work and come back laterSendMessage with returnImmediately: true, then GetTasknothingnot applicable
Live progress and partial artifactsSendStreamingMessage; reconnect with SubscribeToTaskcapabilities.streaming: trueUnsupportedOperationError
A client that cannot hold a connectionA webhook from taskPushNotificationConfig or CreateTaskPushNotificationConfigcapabilities.pushNotifications: truePushNotificationNotSupportedError

Three details decide whether this works in practice (sections 3.2.2 and 3.2.4):

  • Waiting is the default. With returnImmediately unset or false, SendMessage MUST wait until the task is terminal or interrupted before it returns. A long task behind a short HTTP timeout fails here, not in your agent.
  • returnImmediately has no effect when the agent answers with a Message, when you stream, or on push configurations, which work independently of execution mode.
  • historyLength: unset means the client sets no limit and the server decides how much history to return, 0 means no history, and a positive number means at most that many recent messages.

2. What each state allows

a2a.proto marks four states terminal and two interrupted. The other two are in progress.

StateClassWhere the class comes from
TASK_STATE_SUBMITTED, TASK_STATE_WORKINGIn progressNot marked terminal or interrupted in the proto
TASK_STATE_INPUT_REQUIRED, TASK_STATE_AUTH_REQUIREDInterruptedThe proto comments say “This is an interrupted state”
TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, TASK_STATE_REJECTEDTerminalThe proto comments say “This is a terminal state”

What the specification fixes for each class (a blank cell means the specification sets no rule):

OperationIn progressInterruptedTerminal
SendMessage or SendStreamingMessage with that taskIdThe agent MAY accept it (section 3.3.3)For TASK_STATE_INPUT_REQUIRED, the expected next step is a new message with the same taskId and contextId (section 3.4.3). For TASK_STATE_AUTH_REQUIRED, agents SHOULD accept messages (section 7.6.1)UnsupportedOperationError (section 3.1.1)
CancelTaskAttempts to cancel; success is not guaranteed (section 3.1.5)TaskNotCancelableError (section 3.1.5)
SubscribeToTaskOpens a stream; the first event is the current Task (section 3.1.6)Same as in progress: allowed for any task that is not terminal (section 3.1.6)UnsupportedOperationError (section 3.1.6)
GetTaskReturns the current state (section 3.1.3)Returns the current state (section 3.1.3)Returns the final state; this is how clients fetch it after a stream ended or a push arrived (section 3.1.3)
Blocking SendMessageKeeps waitingReturns (section 3.2.2)Returns (section 3.2.2)
An open streamStays openSee belowMUST close (sections 3.1.2 and 3.1.6)

What the specification leaves open

These are the places where two correct implementations can still disagree. Write your choice down in your agent’s documentation.

  • Transitions. There is no transition table. The rules above are everything the specification fixes. Its own samples first show a task in TASK_STATE_COMPLETED (section 6.1), TASK_STATE_WORKING (section 6.2), TASK_STATE_INPUT_REQUIRED (section 6.3), or TASK_STATE_SUBMITTED (section 6.6), so a client must not require TASK_STATE_SUBMITTED to appear.
  • Does a stream close at an interrupted state? Three passages answer differently. Sections 3.1.2 and 3.1.6 require a close only at a terminal state. Section 11.7 describes updates that continue “until the task reaches a terminal or interrupted state, at which point the stream closes”. Section 7.6.1 says that when the credential is received out of band, the agent SHOULD keep active response streams open after it sets TASK_STATE_AUTH_REQUIRED. The specification at main (commit 98279d6) still has all three. Client rule: the end of a stream is not the end of a task; read the last state. Server rule: pick one behavior and document it.
  • Cancelling an interrupted task. TaskNotCancelableError names only terminal states as examples, and nothing excludes an interrupted task. Attempt the cancel and handle the error.
  • History. Not every message is stored in history, and a client must not rely on it unless that was agreed out of band (section 3.7).
  • Duplicates. SendMessage MAY be idempotent, and an agent may use messageId to detect repeats (section 3.3.1).

3. Emit and read events

What must be present

a2a.proto marks these fields REQUIRED. Arrays marked required MUST hold at least one element (section 5.7).

Event memberRequired fields
taskid, status (and status.state)
messagemessageId, role, parts
statusUpdatetaskId, contextId, status (and status.state)
artifactUpdatetaskId, contextId, artifact (and artifact.artifactId, artifact.parts)

Each event carries exactly one of task, message, statusUpdate, or artifactUpdate. A Part sets exactly one of text, raw, url, or data.

Server checklist

  • Decide Message or Task before the first byte. A message-only stream holds exactly one Message, then closes (section 3.1.2).
  • For a task stream, send the Task first, then statusUpdate and artifactUpdate events in the order they happened. Events MUST NOT be reordered (section 3.5.2).
  • Put the explanation for an interrupted state in status.message. For TASK_STATE_AUTH_REQUIRED the message is required unless the details were negotiated out of band or by an extension (section 7.6.1).
  • Deliver results as artifacts. For a large artifact, send the first chunk without append, later chunks with append: true, and mark the last one lastChunk: true (the fields of TaskArtifactUpdateEvent).
  • Close the stream at the terminal state. You may send one final Task snapshot first (section 11.7).
  • With several open streams on one task, send every event to every stream in the same order; closing one stream must not affect the others (section 3.5.2).

Client checklist

  • Treat status.state as the truth about the task. There is no final flag in v1.0.
  • When a stream ends, read the last state. Terminal: done. Interrupted: act on it (below). Anything else: the connection was cut, so reconnect.
  • Reconnect with SubscribeToTask. Its first event is the current Task, which closes the gap between a GetTask and the new stream (section 3.1.6). Status messages sent while you were disconnected are not replayed, and the specification says not to depend on them (section 3.7).
  • Join chunks by artifactId: a chunk with append: true extends the artifact with that ID, and lastChunk: true ends it.
  • To answer an interrupted task, send a message with the same taskId. If you also send a contextId, it must be the task’s own: a message whose contextId and taskId do not match MUST be rejected (section 3.4.3). To start new work in the same conversation, send only the contextId. Task IDs are always assigned by the server (section 3.4.2).
  • For a task in TASK_STATE_AUTH_REQUIRED whose credential arrives out of band, keep a stream open, register a webhook, or poll with GetTask; otherwise you can miss the moment the agent resumes (section 7.6.2).
  • A webhook receiver MUST answer 2xx, SHOULD process notifications idempotently because deliveries can repeat, and MUST check that the task ID is one it expects. The sender must attempt delivery at least once and MAY stop after repeated failures (section 4.3.3). A notification can carry only a status or artifact update; call GetTask when you need the full state.

4. Validate a trace

A trace is the events of one stream or response, one JSON object per line, in the order they arrived. Save this as task-trace-lint.jq. It reads only the trace and makes no network calls.

# 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

Capture a trace from a stream by keeping the data: payloads. JSON-RPC responses are unwrapped automatically:

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

ERROR lines break the specification or the proto. WARN lines are advisory. Order and identity rules run only when every event has the right shape, so fix the ERROR lines about single events first. A file with one event is treated as a single response or a single webhook payload, so the stream-order rules apply only to files with two or more events. In CI:

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

What it reports on the official sample streams

The specification’s section 6 sample exchanges are written as illustrations, and several leave out fields that a2a.proto requires. The table shows what the validator says about each sample at the v1.0.1 tag and on main (commit 98279d6, 2026-10-20). To follow along, extract the JSON from the specification and put one event per line. For the section 6.2 stream:

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

Use main instead of v1.0.1 for the other column.

SampleAt v1.0.1On main
Section 6.1, completed taskOK: 1 event(s), last state TASK_STATE_COMPLETEDsame
Section 6.2, streamingERROR 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
Section 6.3, input requiredERROR event[0]/task/status/message/messageId: required string is missing or emptysame
Section 6.6, task responseOK: 1 event(s), last state TASK_STATE_SUBMITTEDsame
Section 6.6, webhook payloadOK: 1 event(s), last state TASK_STATE_COMPLETEDsame

Do not copy the samples into a server. Build from the required fields in section 3 , then run the validator. The stream below is the section 6.2 sample from main with the one missing field added, and it passes:

{"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"}}}

Break it on purpose

A checker is only useful if it fails when it should. Save the stream above as stream-ok.jsonl. Each row edits it in one place and shows the single finding that results:

jq -s -c '<edit> | .[]' stream-ok.jsonl | jq -s -r -f task-trace-lint.jq
EditFinding
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

What it does not check

  • That your server enforces the operation and state rules in section 2 . It reads events, not requests; section 6 lists the requests to try.
  • Transitions between states. The specification defines none, so the validator accepts any order of non-terminal states.
  • That contextId stays the same across several tasks, that artifacts are complete, or that the bytes in raw are valid base64.
  • A set of webhook deliveries collected from several requests. Run it on one payload at a time.

5. When it breaks

What you seeLikely causeFix
UnsupportedOperationError after a follow-up messageThe task is already terminal (section 3.1.1)Start a new task in the same contextId; use referenceTaskIds to point at the earlier one
TaskNotCancelableErrorThe task is already terminal (section 3.1.5)Call GetTask. If the state is TASK_STATE_CANCELED, you are done
TaskNotFoundError on a follow-upThe taskId was never issued, the task was purged, or the client may not access it (sections 3.4.2 and 3.3.2). The specification says servers SHOULD NOT tell these apartUse only taskId values the server returned; start a new task if the old one is gone
A follow-up is rejectedThe message carries a contextId that differs from the task’s (section 3.4.3)Send only taskId, or copy both from the task
SendMessage hangs or times outWaiting for completion is the default in v1.0 (section 3.2.2)Set returnImmediately: true and poll or subscribe, or use SendStreamingMessage
The stream ended but the task is not doneAn interrupted state, or a cut connection (see section 2 )Read the last state. Interrupted: answer it. Otherwise call SubscribeToTask or GetTask
UnsupportedOperationError from SendStreamingMessagecapabilities.streaming is false or absent (section 3.3.4)Use SendMessage and poll, or fix the card
UnsupportedOperationError from SubscribeToTaskThe task is terminal, or streaming is not declared (sections 3.1.6 and 3.3.4)Call GetTask for the final state
PushNotificationNotSupportedErrorcapabilities.pushNotifications is false or absent (section 3.3.4)Poll or stream instead
The webhook never fires, or fires twiceThe sender stops after repeated failures, delivers at least once, and may repeat (section 4.3.3)Answer 2xx quickly, deduplicate on task ID and state, and fall back to GetTask
SubscribeToTask over HTTP+JSON is refused with a routing or method errorThe specification’s tables (sections 5.3 and 11.3.2) say POST /tasks/{id}:subscribe; the a2a.proto HTTP annotation says GET. Both are unchanged on main, and the proto wins (section 1.4)Serve and call GET; when a peer answers with a method error, try the other verb
A client rejects your eventsRequired fields are missing, or enum values are in the v0.3 spellingRun the validator on a captured trace
Parsers fail after an upgradekind, lowercase states, file parts, or final are still in useSee section 7
An artifact is cut short or doubledappend and lastChunk are misused, or chunks are joined by the wrong IDJoin by artifactId; the validator warns on an append with no earlier chunk and on a chunk after lastChunk

Error names and their JSON-RPC, gRPC, and HTTP mappings are in A2A Error Codes .

6. Test scenarios

Run these against a server (S) or a client (C). The validator covers the rows marked with a trace.

IDSideScenarioExpectedSource
T1SSend a simple message that needs no taskOne message event, then the stream closes. Trace checkSection 3.1.2
T2SStream a task to completiontask first, then updates in order, closed at a terminal state. Trace checkSections 3.1.2 and 3.5.2
T3SSend a message to a terminal taskUnsupportedOperationErrorSection 3.1.1
T4SCancel a terminal taskTaskNotCancelableErrorSection 3.1.5
T5SSubscribe to a terminal taskUnsupportedOperationErrorSection 3.1.6
T6SSubscribe to a running taskThe first event is the current Task. Trace checkSection 3.1.6
T7SSend a message with an unknown taskId, then one with a mismatched contextIdTaskNotFoundError, then a rejectionSections 3.4.2 and 3.4.3
T8SUse streaming or push when the card declares neitherUnsupportedOperationError, PushNotificationNotSupportedErrorSection 3.3.4
T9SSend with the default and then with returnImmediately: trueThe first returns at a terminal or interrupted state; the second returns at once with the task as it standsSection 3.2.2
T10SOpen two streams on one task, close oneBoth receive the same events in the same order; the other stays openSection 3.5.2
T11CReceive a stream that ends in an interrupted stateThe client answers with the same taskId; it does not report failure. Trace checkSections 3.4.3 and 7.6.2
T12CLose a stream mid-taskThe client resubscribes and continues from the first event, which is the current Task. Trace checkSection 3.1.6
T13CReceive the same webhook twiceOne effect; both answered 2xxSection 4.3.3

7. Migrating runtime code from v0.3

Compared field by field between the v0.3.0 JSON schema (specification/json/a2a.json, with the v0.3.0 gRPC proto where the schema is silent) and a2a.proto at v1.0.1. The full migration guide covers the rest of the protocol.

Areav0.3.0v1.0.1What breaks
Statessubmitted, working, input-required, completed, canceled, failed, rejected, auth-required, unknown (the v0.3.0 gRPC proto spells cancelled 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 or indeterminate”) takes the place of unknownComparisons and switches on the old strings
Rolesuser, agentROLE_USER, ROLE_AGENTSending or matching the old role strings
Type tagkind is required on Task, Message, every part, and both update eventsNo kind; the member name tells the type (task, message, statusUpdate, artifactUpdate)Parsers and serializers keyed on kind
PartsTextPart {kind, text}, FilePart {kind, file: {name, mimeType, bytes or uri}}, DataPart {kind, data}One Part with exactly one of text, raw, url, data, plus filename and mediaTypeFile and data handling
End of streamfinal is required on the status updateNo final; read the stateCode that waits for final never finishes
Waitingblocking: true asks the server to wait; false or unset returns the task at once (gRPC proto)returnImmediately defaults to false, and the call waits for a terminal or interrupted stateTimeouts, see below
historyLength of 0The gRPC proto says “if 0, the history will be unlimited”; the JSON schema says only “the number of most recent messages”0 is a request for no messages; unset means no limitA client that sent 0 to mean “all” now gets none
Push config in a sendconfiguration.pushNotificationConfigconfiguration.taskPushNotificationConfigA webhook that is never registered
JSON-RPC methodsmessage/send, message/stream, tasks/get, tasks/cancel, tasks/resubscribe, tasks/pushNotificationConfig/setSendMessage, SendStreamingMessage, GetTask, CancelTask, SubscribeToTask, CreateTaskPushNotificationConfigRouting by method name

The “Waiting” row is the one that fails quietly. By the v0.3.0 gRPC proto, a client that never set blocking asked for an immediate answer. The same code against a v1.0.1 agent waits until the task is terminal or interrupted, and a short timeout turns that into an error that looks like a network problem.

Related pages

Source: A2A official specification: Core Operations (3.1), Operation Semantics (3.3), Multi-Turn Interactions (3.4), Task Update Delivery Mechanisms (3.5), Messages and Artifacts (3.7), Streaming Events and Push Notification Objects (4.2 and 4.3), Common Workflows (6), In-Task Authorization (7.6), and HTTP+JSON Streaming (11.7) ; a2a.proto at v1.0.1 ; the v0.3.0 JSON schema ; sample exchanges from the specification at the v1.0.1 tag and at main commit 98279d6