AI Engineering from Scratch · Manual© 2026 Rohit Ghumare · MIT license

A2A 101

The Agent2Agent protocol, from its purpose to each request and response

A2A protocol 1.0.1 · 3303592 · 2026-05-28 · Edition 2026.10
  • The Protocol on One Page
  • AgentCard
  • Data Model
  • Operations
  • Bindings
  • Security and Extensions
  • Implementing A2A
Plate IOne planner, three remote agents
One planner, three remote agents The planner from capture/planner.py on the left and three lanes, one per delegation, in the order it ran them. In each lane the planner calls a remote agent, shown with its port and skills, and the agent runs a task shown with its states. test-runner completes with 13 passed. code-reviewer asks which base branch to use, the planner answers main, and the task completes with one finding. deployer waits in TASK_STATE_AUTH_REQUIRED until an operator approves, while the planner follows the task with SubscribeToTask, and then completes. CLIENT AGENT REMOTE AGENT THE TASK IT RUNS planner client agent reads 3 cards, then delegates test-runner port 41241 skill run-tests SendMessage task 954267b7 TASK_STATE_COMPLETED summary.json: 13 passed code-reviewer port 41242 skills review-diff, answer-question SendMessage task 80bc7784 TASK_STATE_INPUT_REQUIRED asks for the base branch the planner answers: main TASK_STATE_COMPLETED review.json: 1 finding answer: main deployer port 41243 skill deploy bearer token required SendMessage task a31361b3 TASK_STATE_AUTH_REQUIRED waits for an operator the planner subscribes TASK_STATE_COMPLETED build live on staging SubscribeToTask operator outside A2A approves
The planner reads three cards and delegates three tasks: one completes at once, one after a question, and one after an operator approves. Lanes run top to bottom in the order of capture/out/19-planner.log, and ids are shortened to 8 characters.
00

How to read this manual

Every claim in this manual traces to a ranked source, and every request, response, and stream frame comes from a capture kit you can run yourself.

This manual explains the Agent2Agent protocol, A2A, at release 1.0.1, through one system: a planner that gives work to three agents it cannot see inside.

Who this manual is for

You have built an agent that calls tools or a model API, and you know HTTP and JSON. Now you want to give work to an agent that another team owns.

After the last part, you can:

  • Read an agent card and say what the agent offers, how to reach it, and what it requires.
  • Predict which state a task is in at any point of a run, and what a client can do next.
  • Choose between a blocking call, polling, streaming, and push notifications for a given job.
  • Read any A2A request, response, or stream frame line by line, in either JSON binding.
  • Name the errors a client must handle and the rules a server can break without noticing.

The A2A lesson predates version 1.0, so where the two differ, follow this manual.

How it was made

The manual pins tag v1.0.1 of the A2A repository, commit 3303592. In requests and cards, that release is protocol version 1.0, because "Patch version numbers SHOULD NOT be used in requests, responses and Agent Cards" spec §3.6. The facts were also checked against the main branch at commit 679ab3a of 2026-10-05, where v1.0.1 was still the latest release.

Facts come from five sources, ranked by authority. When two of them disagree, the higher one wins and the text says so.

RankSourceWhat the manual takes from itCited as
1specification/a2a.proto at v1.0.1every object, field, enum value, and methodproto and a message or enum name
2docs/specification.md at v1.0.1every behavior rule, quoted word for wordspec and a section number
3the project's documentation pages at v1.0.1the project's own framing, flagged where a page is out of datedocs and a page name
4the reference SDK, a2a-python 1.2.2what a production implementation does where the specification is silentsdk and a source file
5the capture kit in capture/every request, response, and stream frame the manual showsthe capture file name

The proto, the specification, and the documentation pages are vendored unchanged under research/sources/.

The specification disagrees with itself or the proto on the HTTP verb for SubscribeToTask (conflict D1) and on whether a send waits (conflict D2). It also disagrees on when a stream closes (conflict D3) and on the name of the card's security field (conflict D6). The sources reference lists all forty conflicts with the section that discusses each.

The capture kit

The capture kit runs three remote agents and a recording client on your machine, with the Python standard library only and no network or keys.

AgentPortWhat it doesWhat its runs show
test-runner41241runs a test suite and streams the logstreaming, artifacts in chunks, polling, cancel, push notifications, both JSON bindings
code-reviewer41242reviews a diff against a base branchdirect message replies, TASK_STATE_INPUT_REQUIRED, rejected content types, no push support
deployer41243deploys a build to staging after an operator approvesbearer authentication, TASK_STATE_AUTH_REQUIRED with approval outside A2A, rejection, extended and signed cards

A client walks through the real planner, capture/planner.py.

Seeded ids and a fixed clock make two runs byte-identical. The kit README lists where the kit differs from the reference SDK.

Conventions

Citations name a place: spec §3.7 a section of the specification, proto TaskState a message or enum in the proto, docs life-of-a-task a documentation page, and sdk src/a2a/server/tasks/task_manager.py a file in the SDK.

A listing copies lines from a capture file unchanged, and a … marks a cut that the note explains.

Field names and enum values are set in code type as they travel, such as messageId. Figures shorten task and context ids to 8 characters, so 728ba084-97eb-422b-b94b-b0fe9153ce2c appears as 728ba084.

Figures animate on the web, where a Replay button runs the steps again, and print and reduced motion show the final frame.

A first run

The kit needs Python 3 and four free ports on 127.0.0.1: 41241 to 41243 for the agents and 41250 for the webhook receiver. From the repository root, run it and check it:

cd manuals/a2a-101
python3 capture/run.py
python3 capture/run.py --check

Open capture/out/02-message-reply.http first: a question to code-reviewer and a direct answer, with no task. Then open capture/out/05-streaming.http, where a request to test-runner opens a stream of nine data: lines:

the start of a streamcapture/out/05-streaming.httphttp
POST /a2a/jsonrpc HTTP/1.1
Host: localhost:41241
Content-Type: application/json
A2A-Version: 1.0
Accept: text/event-stream
…
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"jsonrpc": "2.0", "id": 1, "result": {"task": {"id": "728ba084-97eb-422b-b94b-b0fe9153ce2c", … "state": "TASK_STATE_SUBMITTED", …
The request body and the rest of the first frame are cut. Frames 2 to 9 follow it in the file.

The first frame is the task itself, and one task, request by request reads all nine.

How the parts are ordered

Part 1 gives you the whole protocol. Parts 2 to 6 each take one layer, from the AgentCard to security and extensions, and Part 7 builds a client and a server:

PartWhat it covers
1 · The Protocol on One PageA2A standardizes how one agent finds another agent, gives it work, and follows that work, and it leaves the agent itself out of scope.
2 · AgentCardBefore a client sends a message, the AgentCard tells it what the agent offers, where to reach it, and what it requires.
3 · Data ModelEvery A2A exchange is built from five objects: Message, Part, Artifact, Task, and the contextId that groups tasks.
4 · OperationsEleven operations create, read, cancel, and follow tasks, and each one has rules that a client must know before it calls.
5 · BindingsThe same operations travel over JSON-RPC, HTTP+JSON, and gRPC, with two headers and one error model shared by all three.
6 · Security and ExtensionsThe card declares how to authenticate and which extensions apply, and the specification lists what each side must check.
7 · Implementing A2AA client and a server follow the rules of the earlier parts, and the official test kit checks the server against them.
R · ReferenceOperations by binding, objects and fields, task states, errors, a glossary, the sources, and the index of figures.

Colour in figures

Each hue keeps one meaning in every figure:

violetthe client agent and what it sends
blueremote agents, their cards, and the messages they send back
greenparts and artifacts: the content a task produces
ambertasks, task states, and transitions
tealthe server's stored task records
indigostreams, subscriptions, and push notifications
plumthe model or logic inside an agent, which A2A never shows
olivetools and systems an agent calls on its own: CI, Git, MCP servers
roseerrors, failure, rejection, and cancellation
greythe operator, the host, and anything outside the protocol

Figure 0.1 teaches seven of the eight arrow styles. It leaves out solid plum, because A2A never carries a model call.

Fig. 0.1reading a sequence figure in this manualsequence
Reading a sequence figure in this manual Five lifelines: planner, test-runner, the task store of test-runner, a webhook receiver at /a2a-events, and an operator. Eight numbered rows show each arrow style once: SendMessage as a solid ink call, task saved as a solid teal write, TASK_STATE_WORKING as a dashed amber state change, a statusUpdate stream frame and a push POST as dotted indigo events, the planner asking an operator for approval as a dashed olive effect outside A2A, TaskNotFoundError as a dashed rose failure, and the SendMessage result as a dashed ink reply. planner client agent test-runner remote agent task store of test-runner webhook /a2a-events operator a person 1 SendMessage a request · solid ink 2 task saved a write · solid teal 3 TASK_STATE_WORKING state change · dashed amber 4 statusUpdate stream frame · dotted indigo 5 push POST push · dotted indigo 6 asks for approval outside A2A · dashed olive 7 TaskNotFoundError an error · dashed rose 8 SendMessage result a reply · dashed ink
Each of the seven arrow styles marks one kind of exchange, and the label on each arrow is a real name from a run of the kit. Read each numbered row from the tail of the arrow to its head. The note under a label gives the style and its meaning. The rows come from 03-blocking-task.http, 05-streaming.http, 15-push.http, 19-planner.log, and 11-errors.http, and the task store is the kit's own store inside test-runner.

Sources:spec §3.6, §3.7 (research/sources/specification.md); proto TaskState (research/sources/a2a.proto); docs life-of-a-task (research/sources/docs.md); sdk src/a2a/server/tasks/task_manager.py; manual.json; research/sources/README.md; research/brief-spec.md §12.2; research/brief-docs.md; research/sources/specification-main.md (main at 679ab3a); capture/README.md, capture/run.py, capture/agents.py, capture/a2a_ref.py, capture/planner.py; capture/out/02-message-reply.http, 03-blocking-task.http, 05-streaming.http, 11-errors.http, 15-push.http, 19-planner.log

Part 1

The Protocol on One Page

A2A standardizes how one agent finds another agent, gives it work, and follows that work, and it leaves the agent itself out of scope.

  1. 1.1What A2A standardizes
  2. 1.2One task, request by request
1.1

What A2A standardizes

A2A defines a data model, a set of operations, and three bindings for agents that cannot see inside each other, and leaves everything inside an agent to the implementation.

Your planner needs the unit tests of payments-api run at commit 9f3c2e1. The test-runner agent does that job on port 41241, in a container the platform team owns. You cannot import it as a function. You can only send it a request and read what comes back.

The specification calls such agents "independent, potentially opaque AI agent systems" spec §1. When you finish this section, you can name the three layers, state the core rules, and say where A2A stops.

Three layers in the specification

The specification is "organized into three distinct layers" spec §1.3.

Data model: what the records are. Layer 1 defines them "expressed as Protocol Buffer messages" spec §1.3, and the proto file is normative spec §1.4.

Operations: what a client can ask for. Layer 2 describes the eleven RPCs of A2AService proto A2AService "independent of how they are exposed over specific protocols" spec §1.3.

Bindings: how the bytes travel. Layer 3 maps the operations onto JSON-RPC, gRPC, and HTTP+JSON spec §1.3.

A client first reads the AgentCard, usually at /.well-known/agent-card.json spec §8.2, with a plain GET that is none of the eleven operations. The card of test-runner lists /a2a/jsonrpc and /a2a/rest, and figure 1.1 stacks the layers behind them.

Below the dashed line, test-runner builds what the protocol leaves open: an HTTP router, a request handler, a task store, and an event broadcast. The §1.3 diagram lists a Get Agent Card operation that §3.1 never defines (conflict D25), and §1.4 misnames the proto path (conflict D15).

Fig. 1.1the A2A stack for test-runnerlayers
The A2A stack for test-runner The planner, a client agent with a card cache and an interface choice, sits on top. Below it are the three protocol layers A2A defines for test-runner at localhost:41241: bindings with the paths /.well-known/agent-card.json, /a2a/jsonrpc and /a2a/rest, the operations, and the data model. Under a dashed line sits what test-runner builds itself: an HTTP router, a request handler, a task store and an event broadcast, and below them the opaque agent logic, where a real agent would also keep a model and tools. CLIENT AGENT you build it A2A PROTOCOL card and bindings operations data model A2A defines it TEST-RUNNER · LOCALHOST:41241 server agent logic its owner builds it planner card cache: agents by skill id interface: JSONRPC, version 1.0 /.well-known/agent-card.json the card, a plain GET GET the card /a2a/jsonrpc JSONRPC 1.0 /a2a/rest HTTP+JSON 1.0 POST SendMessage SendMessage · SendStreamingMessage · GetTask ListTasks · CancelTask · SubscribeToTask 4 push config operations · GetExtendedAgentCard AgentCard · Task · TaskStatus · Message · Part Artifact · TaskStatusUpdateEvent · TaskArtifactUpdateEvent HTTP router Handler routes 3 paths request handler Agent.send creates the task task store Agent.tasks in memory broadcast Agent.broadcast SSE and webhooks run_tests() capture/agents.py model not in the kit Git, container not in the kit
A2A defines the card, the bindings, the operations, and the objects, while test-runner's server parts and agent logic below the dashed line belong to its owner. Read from the planner down. Above the dashed line sit the three layers of spec §1.3 with test-runner's real paths, from capture/out/01-agent-cards.http. Below it are the kit's own parts in capture/a2a_ref.py, and dashed boxes mark what a production agent adds.

The core rules

An agent is opaque: agents collaborate "without needing to share their internal thoughts, plans, or tool implementations" spec §1.2, so a client sees only what comes back.

The server chooses the reply: a message gets either a new Task or "a direct Message response for simple interactions" spec §3.1.1, and SendMessage shows both.

The server owns the task: "Client-provided taskId values for creating new tasks is NOT supported" spec §3.4.2, and the client mints only the messageId proto Message.

A task is in one of nine states: status.state holds one TaskState value proto TaskState, four of them terminal and two interrupted. Task, TaskStatus, and TaskState lists all nine.

A terminal task does not change: no message moves it again, as CancelTask and terminal states shows.

Artifacts carry results: "Messages SHOULD NOT be used to deliver task outputs" spec §3.7, so the planner reads the test count from summary.json.

Events arrive in order: "All implementations MUST deliver events in the order they were generated" spec §3.5.2, and a client that reconnects confirms the state with GetTask spec §3.7.

Credentials travel in HTTP headers: the client "includes these credentials in protocol-appropriate headers or metadata for every A2A request" spec §7.3, so the planner sends Authorization: Bearer to deployer with every request.

The boundary with MCP

MCP connects an agent to its tools, and A2A connects agents to each other: "One connects agents to tools and resources. The other enables agent-to-agent collaboration" docs a2a-and-mcp. The 1.0 announcement gives the short form, "MCP inside agents, A2A between agents" docs announcing-1.0. No card names a tool: the run-tests skill on the test-runner card describes the work and stops there. MCP fundamentals covers the other protocol.

What A2A leaves to the host

A2A defines no registry: the specification "does not prescribe a standard API for curated registries" docs agent-discovery. It issues no credentials, since identity is handled "at the protocol layer, not within A2A semantics" spec §7. It sets no retention period, so a task id can be "invalid, expired, or already completed and purged" spec §3.3.2. It promises no exactly-once effect, because "Send Message operations MAY be idempotent" spec §3.3.1 and a webhook gets at least one attempt spec §4.3.3. Figure 1.2 pairs each guarantee with the host's job.

Fig. 1.2what A2A guarantees and what the host buildscomparison
What A2A guarantees and what the host builds Two columns of six rows. Left, what the A2A specification guarantees: a card at a known path, declared security schemes with credentials in HTTP headers, task ids and states where a finished task never changes, ordered events per stream with at least one attempt per webhook, tasks readable with GetTask while the server keeps them, and four kinds of part with media types. Right, in grey, what the host builds: which agents to trust, issuing credentials, what an approval allows, exactly-once effects, its own task records and retention, and what content to believe. A · THE PROTOCOL GUARANTEES B · THE HOST BUILDS a card at a known path /.well-known/agent-card.json which agents to trust registries, allowlists, signing keys declared security schemes credentials travel in HTTP headers issuing the credentials tokens, OAuth, an operator channel task ids and states a finished task never changes what an approval allows authorization policy and audit ordered events per stream at least one attempt per webhook exactly-once effects dedupe and idempotent handlers GetTask while it is kept no retention period is set your own task records retention, history, purge four kinds of part each with a media type what to believe validation, injection checks
A2A fixes the messages, states, and events between agents, while trust, discovery, retention, and exactly-once effects stay with the host. Read across each row, one row per step. The left box is what the specification guarantees, and the grey box beside it is what you build. No capture: the rows come from spec §1, §3.3.1, §3.4.1, §3.7, §4.3.3, §7, §8.2, and §13.1.

The specification also contradicts itself in ten places that change your code, listed below and settled in the sources reference.

ConflictWhat disagrees
D1the HTTP verb for SubscribeToTask
D2whether a plain SendMessage waits
D3whether a stream closes at an interrupted state
D4 and D5the names of the push configuration objects
D6the name of the card's security field
D11deprecated OAuth flows
D12the page field names of ListTasks
D13the HTTP+JSON error body
D36the error for another client's task

Sources:spec §1, §1.2, §1.3, §1.4, §3.1.1, §3.3.1, §3.3.2, §3.4.1, §3.4.2, §3.5.2, §3.7, §4.3.3, §7, §7.3, §8.2, §13.1 (research/sources/specification.md); proto A2AService, Message, TaskState (research/sources/a2a.proto); docs a2a-and-mcp, announcing-1.0, agent-discovery (research/sources/docs.md); capture/a2a_ref.py, capture/planner.py; capture/out/01-agent-cards.http, 03-blocking-task.http, 10-cancel.http, 19-planner.http

1.2

One task, request by request

One streamed run of test-runner shows every piece of the protocol in order, from the card read to the moment the server closes the stream.

Your planner wants the unit tests of payments-api run at commit 9f3c2e1, and it wants to watch the log grow while they run. It already holds the card of test-runner, whose capabilities include "streaming": true, so it sends the request with SendStreamingMessage.

This section follows that run frame by frame, from capture/out/05-streaming.http, and figure 1.3 shows it. When you finish this section, you can read any task stream and say what each frame changed in the stored task.

Fig. 1.3one streamed task from the card read to the closesequence
One streamed task from the card read to the close Two lifelines, planner and test-runner at localhost:41241. The planner reads the agent card, sends SendStreamingMessage with a new messageId and no taskId, and gets HTTP 200 with text/event-stream. Nine numbered stream frames follow: the task 728ba084 in TASK_STATE_SUBMITTED, a statusUpdate to TASK_STATE_WORKING, five artifactUpdate frames that build test-log.txt, the last with lastChunk, one artifactUpdate for summary.json, and a statusUpdate to TASK_STATE_COMPLETED. Only frames 1, 2 and 9 carry a timestamp, at 09:00:01.750Z, 09:00:02.000Z and 09:00:02.250Z. Then the server closes the stream, with no final flag. planner client agent test-runner localhost:41241 KIT CLOCK GET /.well-known/agent-card.json AgentCard, capabilities.streaming: true SendStreamingMessage messageId 5bc8fbbc, no taskId HTTP 200, text/event-stream 1 task · TASK_STATE_SUBMITTED task 728ba084, history holds the request 09:00:01.750Z 2 statusUpdate · TASK_STATE_WORKING Checking out 9f3c2e1 and starting the suite 09:00:02.000Z 3 artifactUpdate · test-log.txt first chunk: collected 12 items 4 artifactUpdate · append 5 artifactUpdate · append 6 artifactUpdate · append 7 artifactUpdate · append, lastChunk 8 artifactUpdate · summary.json, lastChunk 9 statusUpdate · TASK_STATE_COMPLETED 1 failed, 11 passed 09:00:02.250Z frames 3 to 8 carry no timestamp the server closes the stream after frame 9, with no final flag
A streamed task opens with the task itself, reports its work as status and artifact frames, and ends when the server closes the stream after the terminal status. Read top to bottom. Unnumbered rows come before the stream, and each numbered row is one data frame, numbered as in capture/out/05-streaming.http. Timestamps on the right come from the kit clock, and the task id is shortened to 8 characters.

Before the first frame

Unless the card sets capabilities.streaming to true, the agent must refuse SendStreamingMessage with UnsupportedOperationError spec §3.3.4, as discovery explains. The request itself is short:

the request that opens the streamcapture/out/05-streaming.httphttp
POST /a2a/jsonrpc HTTP/1.1
Host: localhost:41241
Content-Type: application/json
A2A-Version: 1.0
Accept: text/event-stream
…
  "method": "SendStreamingMessage",
  "params": {
    "message": {
      "messageId": "5bc8fbbc-bde5-4099-8164-d8399f767c45",
      "role": "ROLE_USER",
      "parts": [
        {
          "text": "Run the unit tests for payments-api at commit 9f3c2e1"
        }
      ]
…
The first lines of the JSON-RPC envelope and its closing brackets are cut.

The client minted the messageId itself, and it sent no taskId and no contextId, so the agent creates both. The response head is HTTP/1.1 200 OK with Content-Type: text/event-stream. Each event after it is one data: line that holds a complete JSON-RPC response, with the request's id of 1 spec §9.4.2.

Frame 1: the task appears

A task stream has a fixed opening: "the stream MUST begin with the Task object" spec §3.1.2. This one is already in TASK_STATE_SUBMITTED:

frame 1, the new taskcapture/out/05-streaming.events.jsonjson
"task": {
  "id": "728ba084-97eb-422b-b94b-b0fe9153ce2c",
  "contextId": "c026046c-6890-4830-9618-8fffa35cb080",
  "status": {
    "state": "TASK_STATE_SUBMITTED",
    "timestamp": "2026-10-06T09:00:01.750Z"
  },
  "history": [
    {
      "messageId": "5bc8fbbc-bde5-4099-8164-d8399f767c45",
      "contextId": "c026046c-6890-4830-9618-8fffa35cb080",
      "taskId": "728ba084-97eb-422b-b94b-b0fe9153ce2c",
      "role": "ROLE_USER",
      …
The JSON-RPC envelope, the message parts, and the closing brackets are cut.

The server minted task 728ba084 and context c026046c, and it wrote both ids into its copy of the planner's message. From here on, the planner matches every frame to those two ids. The kit fills the optional timestamp of this first status proto TaskStatus, and the SDK leaves it out, as the kit README lists.

Frame 2: the work starts

Frame 2 is a statusUpdate that moves the task to TASK_STATE_WORKING, with the status message "Checking out 9f3c2e1 and starting the suite". Every status event names its task and its context, because taskId, contextId, and status are all REQUIRED proto TaskStatusUpdateEvent.

Frames 3 to 8: the results arrive in pieces

The next six frames are artifactUpdate events, a type with no timestamp field proto TaskArtifactUpdateEvent. Frames 3 to 7 build one artifact, test-log, one line of the log at a time:

  • Frame 3 carries the artifactId test-log, the name test-log.txt, and the first part. It has no append, so it starts the artifact.
  • Frames 4, 5, and 6 repeat the artifactId, leave out the name, and set append: true.
  • Frame 7 sets both append: true and lastChunk: true, so the log is complete.
  • Frame 8 starts a second artifact, summary, in one chunk: a data part with the media type application/json, and lastChunk: true.

A lastChunk closes one artifact and nothing more: frame 8 carries its own while the task keeps running. Artifact and chunks shows how a client joins the chunks, and what the specification leaves open.

Frame 9 and the close

Frame 9 is the last statusUpdate. It moves the task to TASK_STATE_COMPLETED at 09:00:02.250Z, with the status message "1 failed, 11 passed". The task completed although a test failed, because the failure is part of the result.

Then the server closes the connection, and no frame announces it, because version 1.0 removed the final flag docs whats-new-v1. The rule is "The stream MUST close when the task reaches a terminal state" spec §3.1.2. At an interrupted state, the specification contradicts itself about whether a stream closes (conflict D3). Interrupted states sets out when a stream closes, and its figure 4.4 follows a stream that stays open.

The task after the stream

After the close, the task remains in the store, and a client reads it back with GetTask, the operation meant "for fetching the final state of a task after being notified via a push notification or after a stream has ended" spec §3.1.3. GetTask and ListTasks covers what it returns.

Sources:spec §3.1.2, §3.1.3, §3.3.4, §9.4.2 (research/sources/specification.md); proto TaskStatus, TaskStatusUpdateEvent, TaskArtifactUpdateEvent (research/sources/a2a.proto); docs whats-new-v1 (research/sources/docs.md); capture/README.md, capture/a2a_ref.py; capture/out/01-agent-cards.http, 05-streaming.http, 05-streaming.events.json

Part 2

AgentCard

Before a client sends a message, the AgentCard tells it what the agent offers, where to reach it, and what it requires.

  1. 2.1AgentCard fields
  2. 2.2Discovery and supportedInterfaces
  3. 2.3Extended cards and signatures
2.1

AgentCard fields

An AgentCard has 14 fields, eight of them REQUIRED, and together they say what an agent offers, where to reach it, and what it demands.

The planner is about to delegate its first test run to test-runner, an agent it knows only as localhost:41241. Its first request is a GET for one JSON document.

When you finish this section, you can read any agent card field by field and say which fields a valid card must carry.

The card and its REQUIRED fields

Agent card: "A JSON metadata document published by an A2A Server, describing its identity, capabilities, skills, service endpoint, and authentication requirements." spec §2.2 Every server publishes one: "A2A Servers MUST make an Agent Card available." spec §8.1

The proto message AgentCard has 14 fields, and eight of them are REQUIRED: name, description, supportedInterfaces, version, capabilities, defaultInputModes, defaultOutputModes, and skills proto AgentCard. A REQUIRED field "MUST be present and set in valid messages" spec §5.7.

Four of those fields are lists, and "Arrays marked as required MUST contain at least one element" spec §5.7. The signing example in §8.4.1 keeps an empty skills list all the same spec §8.4.1 (conflict D19, open as issue #2122), and real cards follow §5.7.

Figure 2.1 sorts the whole test-runner card into four groups: who it is, where to reach it, what it demands, and what it offers. Of the identity fields, version is the agent's own release, and extended cards and signatures covers signatures.

Fig. 2.1the test-runner agent card, field by fieldstructure
The test-runner agent card, field by field The agent card that test-runner serves at /.well-known/agent-card.json, drawn as rows under four headings that appear one after another. Who it is: name, description, provider and version, with documentationUrl, iconUrl and signatures not set. Where to reach it: two supportedInterfaces, JSON-RPC first and HTTP+JSON second. What it demands: capabilities with streaming and pushNotifications true, and no security fields. What it offers: text/plain and application/json modes and the run-tests skill. A dot marks each REQUIRED field. test-runner agent card GET http://localhost:41241/.well-known/agent-card.json WHO IT IS identity, plus proof of who published it name "test-runner" description "Checks out a commit, runs its test suite in a clean container, and streams the log." provider organization "Platform Team", url "https://ci.example.com" version "2.3.0" the agent release, not the protocol version documentationUrl not set iconUrl not set signatures not set deployer signs its card (section 2.3) WHERE TO REACH IT ordered, and the first entry is preferred supportedInterfaces [0] JSONRPC, "1.0", http://localhost:41241/a2a/jsonrpc [1] HTTP+JSON, "1.0", http://localhost:41241/a2a/rest WHAT IT DEMANDS an absent flag counts the same as false capabilities streaming true, pushNotifications true securitySchemes not set securityRequirements not set so calls need no credentials WHAT IT OFFERS media types, and skills that describe work defaultInputModes "text/plain", "application/json" defaultOutputModes "text/plain", "application/json" skills [0] id "run-tests", name "Run tests", tags "ci", "tests" plus a description and one example REQUIRED in proto AgentCard optional, and not set on this card
One GET returns what a client needs before its first message: who the agent is, where to reach it, what it demands, and what it offers. Each row is one field of the card, grouped by the question it answers. A dot marks a field the proto makes REQUIRED, and dashed rows are optional fields this card leaves out. From capture/out/01-agent-cards.http.

What it offers: skills and modes

Skill: one entry in skills, a unit of work the agent says it can do. Each skill needs an id, a name, a description, and at least one entry in tags spec §5.7. The optional examples hold sample prompts, and a skill can carry its own modes and securityRequirements proto AgentSkill.

the one skill on the test-runner cardcapture/out/01-agent-cards.httpjson
  "skills": [
    {
      "id": "run-tests",
      "name": "Run tests",
      "description": "Run the test suite of a repository at one commit and report each failure with its log.",
      "tags": [
        "ci",
        "tests"
      ],
      "examples": [
        "Run the unit tests for payments-api at commit 9f3c2e1"
      ]
    }
  ]
The skills array of the first card in the file, complete.

No field of SendMessageRequest or Message names a skill proto SendMessageRequest, and the proto calls the list "largely a descriptive concept" proto AgentCard.

Mode: a media type that names content an agent accepts or produces, such as text/plain proto AgentCard. The REQUIRED lists defaultInputModes and defaultOutputModes apply across all skills, and a skill overrides them with its own inputModes and outputModes. The code-reviewer card narrows the input of its review-diff skill to text/x-diff and text/plain.

Where to reach it: supportedInterfaces

Interface: one entry in supportedInterfaces, a URL with the binding and the protocol version spoken there. Its fields url, protocolBinding, and protocolVersion are REQUIRED, and tenant is optional proto AgentInterface. The order matters: "Ordered list of supported interfaces. The first entry is preferred." proto AgentCard

the two interfaces of test-runner, JSON-RPC firstcapture/out/01-agent-cards.httpjson
  "supportedInterfaces": [
    {
      "url": "http://localhost:41241/a2a/jsonrpc",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    {
      "url": "http://localhost:41241/a2a/rest",
      "protocolBinding": "HTTP+JSON",
      "protocolVersion": "1.0"
    }
  ],
The supportedInterfaces array of the first card in the file, complete.

The proto names three standard bindings: JSONRPC, GRPC, and HTTP+JSON proto AgentInterface. The field protocolVersion is the A2A version spoken at that URL, as Major.Minor spec §3.6, and the version and extension headers cover the matching A2A-Version header.

Each URL "Must be a valid absolute HTTPS URL in production" proto AgentInterface, and the kit uses http://localhost on one machine. The spec does not say which form a GRPC entry takes, and the reference SDK's sample writes a plain host and port sdk samples/hello_world_agent.py (conflict D34).

What it demands: capabilities and security

Capability: a flag in the REQUIRED capabilities object that switches on an optional part of the protocol: streaming, pushNotifications, or extendedAgentCard proto AgentCapabilities. An absent flag counts the same as false spec §3.3.4, and discovery lists the operations that each flag allows. The same object lists extensions by uri, and extensions covers that list.

The optional securitySchemes and securityRequirements say which credentials a call needs proto AgentCard. The test-runner card sets neither, and security schemes reads the bearer scheme on the deployer card.

In 1.0, five top-level fields of the 0.3 card moved into supportedInterfaces and capabilities docs whats-new-v1, and from 0.3 to 1.0 maps each one. The migration steps in §A.2.1 still use a card field protocolVersions spec §A.2.1, which the 1.0 proto lacks (conflict D8). PR #2165 fixed that on main after v1.0.1, not yet released. The sample card in §8.5 still uses security where the proto has securityRequirements spec §8.5 proto AgentCard (conflict D6). PR #2046 fixed that sample on main, not yet released.

Sources:spec §2.2, §3.3.4, §3.6, §5.7, §8.1, §8.4.1, §8.5, §A.2.1 (research/sources/specification.md); proto AgentCard, AgentProvider, AgentSkill, AgentInterface, AgentCapabilities, SendMessageRequest (research/sources/a2a.proto); docs/whats-new-v1.md at v1.0.1; sdk samples/hello_world_agent.py; capture/out/01-agent-cards.http

2.2

Discovery and supportedInterfaces

The spec standardizes only the well-known path to a card, and a client takes the first supportedInterfaces entry it speaks before it checks a capability flag.

The planner starts with three ports in its configuration: 41241, 41242, and 41243. Before it delegates any work, it fetches three cards, picks one interface from each, and records which agents stream and which one wants a token.

When you finish this section, you can locate an agent's card, pick the interface to call, and avoid the calls its card rules out.

Three routes to a card

The spec names three ways to reach a card, and it standardizes the details of only one spec §8.2:

  • Well-known URI. The client fetches https://{server_domain}/.well-known/agent-card.json from a host it already knows spec §8.2. The project recommends this route for public agents docs agent-discovery.
  • Registry or catalog. The client queries "curated catalogs of agents" spec §8.2, and "The current A2A specification does not prescribe a standard API for curated registries." docs agent-discovery
  • Direct configuration. The client starts from "Pre-configured Agent Card URLs or content" spec §8.2, kept in code, a configuration file, or an environment variable docs agent-discovery.

The planner's ports come from direct configuration in capture/run.py, and each card from the well-known path.

discovery is one GET on the well-known pathcapture/out/01-agent-cards.httphttp
### 1 · discover test-runner
GET /.well-known/agent-card.json HTTP/1.1
Host: localhost:41241
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "name": "test-runner",
…
}
The response body is cut after its first field. Figure 2.1 draws the whole card.

Use the path exactly as the spec writes it. One diagram in the project's guides still shows /.well-known/agent-card with no .json, and POST /sendMessage where 1.0 has POST /message:send docs what-is-a2a spec §5.3 (conflict D32). PR #2261 fixed the diagram on main after v1.0.1, not yet released.

Figure 2.2 follows the planner from its configuration to a checked call on code-reviewer.

Fig. 2.2from a host to a checked callflow
From a host to a checked call Three stages, top to bottom, drawn step by step. First, the planner finds the code-reviewer card: direct configuration gives it the host, and a GET of /.well-known/agent-card.json returns the card, while a registry is a third route the kit does not use. Second, the planner takes interface 0, JSON-RPC 1.0 at /a2a/jsonrpc, and skips interface 1, HTTP+JSON. Third, it reads the capability flags: streaming is true and pushNotifications is absent, and a CreateTaskPushNotificationConfig call that ignores the absent flag fails with error -32003. 1 · FIND THE CARD 2 · TAKE THE FIRST INTERFACE YOU SPEAK 3 · CHECK THE FLAG BEFORE AN OPTIONAL CALL direct configuration ports 41241, 41242, 41243 well-known URI GET /.well-known/ agent-card.json GET code-reviewer agent card 200 OK registry or catalog no standard query API or a registry query [0] JSONRPC, "1.0", http://localhost:41242/a2a/jsonrpc chosen [1] HTTP+JSON, "1.0", http://localhost:41242/a2a/rest skipped: not JSON-RPC streaming: true SendStreamingMessage, SubscribeToTask pushNotifications: absent the four push config operations fail exchange 6 of 11-errors.http calls one anyway: planner client code-reviewer /a2a/jsonrpc CreateTaskPushNotificationConfig -32003 PUSH_NOTIFICATION_NOT_SUPPORTED
Three routes lead to one card, and a client takes the first interface it speaks and checks a capability flag before each optional operation. Read top to bottom. The planner finds the code-reviewer card through configuration and the well-known path, takes interface 0, and reads two flags. The failed call is exchange 6 of capture/out/11-errors.http.

Choosing from supportedInterfaces

The spec gives the client four rules for supportedInterfaces, all of them MUST spec §8.3.2. The first one decides the endpoint:

Supported means a binding and a version that the client implements. A JSON-RPC 1.0 client skips a GRPC entry and a JSONRPC entry at 0.3, since one card can list a binding at several versions spec §3.6.2.

The other three rules ask the client to prefer earlier entries, to use the URL of the chosen entry, and to "Set the tenant field in every request message to exactly the value declared in the selected AgentInterface entry" spec §8.3.2. No kit card sets a tenant.

Checking capabilities before a call

On the server side the rule is a MUST: a call that the card rules out fails with a fixed error spec §3.3.4. The table gives each error with its JSON-RPC code spec §5.4.

Flag in capabilitiesOperations it allowsError when the flag is absent or false
streamingSendStreamingMessage, SubscribeToTaskUnsupportedOperationError, -32004
pushNotificationsCreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfigPushNotificationNotSupportedError, -32003
extendedAgentCardGetExtendedAgentCardUnsupportedOperationError, -32004

Section 3 says its operations "define the fundamental capabilities that all A2A implementations must support" spec §3, yet an agent declines streaming, push, or the extended card by leaving its flag out. The flags decide (conflict D26).

The code-reviewer card has no pushNotifications flag, and the kit's error scenario registers a webhook with it anyway:

a push configuration on an agent that declares no push supportcapture/out/11-errors.httphttp
### 6 · CreateTaskPushNotificationConfig: not supported
POST /a2a/jsonrpc HTTP/1.1
Host: localhost:41242
…
  "method": "CreateTaskPushNotificationConfig",
  "params": {
    "taskId": "00000000-0000-4000-8000-000000000000",
…
HTTP/1.1 200 OK
…
    "code": -32003,
    "message": "Push notifications are not supported",
…
        "reason": "PUSH_NOTIFICATION_NOT_SUPPORTED",
Exchange 6, cut to the method, the task id, and the error. The JSON-RPC error arrives with HTTP 200, as Errors explains.

The task id does not exist, yet the error names the missing capability, so the call cost a round trip the card had answered.

Skills, modes, and security fields say whether the agent fits the job and what it needs. The planner maps run-tests to test-runner and attaches a bearer token to every deployer call (capture/planner.py). Part and security schemes read those fields.

Caching the card

A card changes rarely, so the spec asks both sides to use ordinary HTTP caching spec §8.6. A server "SHOULD include a Cache-Control response header with a max-age directive" spec §8.6.1, plus an ETag derived from the card's version or a hash of its content. A client honors those headers and revalidates an expired card with If-None-Match or If-Modified-Since spec §8.6.2.

The kit sends no Cache-Control or ETag header, as the kit README lists. An extended card replaces the cached public card for an authenticated session, as extended cards and signatures explains.

Sources:spec §3, §3.3.4, §3.6.2, §5.3, §5.4, §8.2, §8.3.2, §8.6, §8.6.1, §8.6.2 (research/sources/specification.md); docs/topics/agent-discovery.md, docs/topics/what-is-a2a.md at v1.0.1; capture/README.md, capture/run.py, capture/planner.py; capture/out/01-agent-cards.http, 11-errors.http

2.3

Extended cards and signatures

GetExtendedAgentCard returns a fuller card to an authenticated caller, and signatures lets any caller check that a card is the one its publisher signed.

The planner holds a bearer token and reads the public card of deployer. That card carries a signature, and its extendedAgentCard flag says that an authenticated caller sees more.

When you finish this section, you can fetch an extended card, and sign and verify a card as the spec prescribes.

GetExtendedAgentCard

Extended card: the fuller card an agent returns to an authenticated caller through GetExtendedAgentCard, with "additional details or skills not present in the public card" spec §3.1.11.

The call needs credentials: "The client MUST authenticate the request using one of the schemes declared in the public AgentCard.securitySchemes and AgentCard.security fields." spec §3.1.11 That rule still names security, which 1.0 replaced with securityRequirements proto AgentCard (conflict D6). PR #2046 fixed the §8.5 sample on main, and the §3.1.11 prose still says security.

Appendix A.2.2 calls the 0.3 flag supportsExtendedAgentCard, and the migration guide says supportsAuthenticatedExtendedCard spec §A.2.2 docs whats-new-v1 (conflict D40). It gives the new flag field 5, and the proto uses 4 spec §1.4 proto AgentCapabilities (conflict D7).

Figure 2.3 shows deployer refusing the call without a token, then returning the public card plus a second skill, without signatures.

Fig. 2.3earning the extended cardsequence
Earning the extended card Three lifelines: the platform team outside A2A, the planner, and the deployer. Seven steps appear in order. The planner reads the public card, which declares extendedAgentCard true and a bearer scheme. It calls GetExtendedAgentCard without an Authorization header and gets HTTP 401 with WWW-Authenticate: Bearer realm="deployer". The platform team issues a bearer token outside A2A. The planner calls again with the token and receives the extended card, whose skills are deploy and rollback and which carries no signature. platform team outside A2A planner client deployer port 41243 1 GET /.well-known/agent-card.json 2 public card extendedAgentCard: true, scheme bearer 3 GetExtendedAgentCard no Authorization header 4 HTTP 401 Unauthorized WWW-Authenticate: Bearer realm="deployer" 5 bearer token issued outside A2A 6 GetExtendedAgentCard Authorization: Bearer dpl_test_7c1e4b 7 extended card skills deploy and rollback, unsigned
The same GetExtendedAgentCard call fails with HTTP 401 before any A2A method runs, then returns a card with a second skill once a token is attached. Time runs down. Steps 1 and 2 come from capture/out/01-agent-cards.http. Steps 3 and 4 are exchange 1 of capture/out/18-extended-card.http, and steps 6 and 7 are exchange 2. The token comes from the platform team, outside A2A.
GetExtendedAgentCard with a token, and the skill it addscapture/out/18-extended-card.httphttp
### 2 · GetExtendedAgentCard: with a token
…
Authorization: Bearer dpl_test_7c1e4b
…
HTTP/1.1 200 OK
…
      {
        "id": "rollback",
        "name": "Roll back a deploy",
        "description": "Return staging to the previous build. Shown only to authenticated callers.",
        "tags": [
          "deploy",
          "rollback"
        ]
      }
Exchange 2, cut to the token and the second skill. The rest of the card equals the public card, without its signatures.

A client SHOULD replace its cached public card with the extended one "for the duration of their authenticated session or until the card's version changes" spec §3.1.11. Such cards "SHOULD NOT include sensitive information that could be exploited if leaked" spec §13.3. A declared flag with no card behind it answers ExtendedAgentCardNotConfiguredError, -32007 spec §3.3.4 spec §5.4.

The official SDKs leave the credential check to the host, and the table says who receives the card when the host adds nothing.

SDKWho receives the extended card without a host check
Python 1.2.2Any caller. The extended_card_modifier hook receives the call context and can vary the card sdk src/a2a/server/request_handlers/default_request_handler_v2.py.
Go v2.6.0Any caller. A CallInterceptor or the card producer can reject the call sdk-go a2asrv/handler.go at v2.6.0.
Java v1.4.0.FinalAny caller in the core handlers, by design. The Quarkus reference servers mark the route @Authenticated sdk-java SECURITY.md at v1.4.0.Final sdk-java reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java at v1.4.0.Final.
JS v1.3.0An authenticated user when the card is a static object, and anyone else gets the public card with no error. A provider function decides itself sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0.
.NET v1.0.0-preview2Nobody by default, because the method throws ExtendedAgentCardNotConfiguredError. An application overrides it and adds its own check sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2.
Rust a2a-server-lf-v0.5.1Any caller. A resolver sees the request headers and can vary the card sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1.

Signing a card

Card signature: an optional entry in signatures that holds a JSON Web Signature, as RFC 7515 defines it, over the card spec §8.4.

Each entry holds a base64url protected header and signature proto AgentCardSignature, and a verifier rebuilds the signed bytes from the received card under three rules spec §8.4.1:

  1. Field presence. "Fields marked with REQUIRED MUST always be present, even if the field value matches the default." spec §8.4.1 A field with the optional keyword stays whenever it was set, and any other field at its default value is dropped.
  2. RFC 8785. The JSON Canonicalization Scheme sorts object keys, fixes one form for each value, and removes whitespace spec §8.4.1.
  3. No signatures. "The signatures field itself MUST be excluded from the content being signed to avoid circular dependencies." spec §8.4.1

The protected header holds alg, typ, and kid spec §8.4.2, and the line for typ says SHOULD under a MUST heading (conflict D20). An optional jku points to a JSON Web Key Set with the public key spec §8.4.2.

To sign, join the base64url header and canonical payload with a period, and sign that input with the key that kid names spec §8.4.2.

the deployer card, canonicalized, signed, and checkedcapture/out/17-signed-card.txttext
# 1. canonical payload (RFC 8785): the card without signatures, keys sorted, no spaces
{"capabilities":{"extendedAgentCard":true,"streaming":true},…"version":"1.4.1"}

# 2. protected header, decoded
{"alg": "HS256", "typ": "JOSE", "kid": "deployer-key-1"}

# 3. JWS signing input: BASE64URL(header) '.' BASE64URL(payload)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJkZXBsb3llci1rZXktMSJ9.eyJjYXBhYmlsaXRpZXMiOnsi…

# 4. the signature entry published in the card
{
  "protected": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJkZXBsb3llci1rZXktMSJ9",
  "signature": "yPk834ji_dOVmxElhzV3yTYBT9NVp8Bx1j5K0LQmOww"
}

# 5. verify the published card: True
# 6. verify after changing the description to name production: False
The payload and the signing input are cut inside their lines. Step 2 prints the header with spaces, and the encoded bytes have none.

The kit signs with HS256, as the kit README lists. Real cards use ES256 or RS256 spec §8.4.2, because an HMAC verifier holds the signing key.

Verifying a card

Verification has six MUST steps spec §8.4.3. They take a signature, fetch the public key by kid and jku or from a trusted store, and check it against the rebuilt payload.

Apply rule 1 when you rebuild, although the signing and verifying steps both say "Remove properties with default values" spec §8.4.2 spec §8.4.3 (conflict D21, open as issue #2249).

The kit's check passes on the published card and fails on a copy whose description names production (capture/run.py), because the canonical bytes change. Figure 2.4 shows both checks.

Fig. 2.4signing and verifying the deployer cardflow
Signing and verifying the deployer card Top half, signing, in three steps: the deployer card minus its signatures field is canonicalized with RFC 8785 into a 959-byte payload. The protected header {"alg":"HS256","typ":"JOSE","kid":"deployer-key-1"} and the payload are each base64url-encoded and joined with a period into a 1,348-character signing input, which HMAC-SHA256 signs to give signatures[0] with protected eyJhbGci and signature yPk834ji. Bottom half, verifying, in four steps: the published card rebuilds the same payload and its signature matches, so verification returns True. An edited copy whose description reads "Deploys a tagged build to production." rebuilds a different payload, and verification returns False. SIGN VERIFY deployer card 11 members, plus signatures canonical payload, RFC 8785 sorted keys, no spaces, no signatures 959 bytes, from capabilities to version canonicalize rules 1 to 3 protected header {"alg":"HS256","typ":"JOSE", "kid":"deployer-key-1"} JWS signing input BASE64URL(header) '.' BASE64URL(payload) 1,348 characters on one line base64url base64url signatures[0] protected "eyJhbGci" signature "yPk834ji" HMAC-SHA256 HS256, key deployer-key-1 the kit: HS256, one shared key real cards: ES256 or RS256, with a public key in a JWKS at jku published card description as signed recompute "yPk834ji" rebuild True step 5 match edited copy "Deploys a tagged build to production." recompute new payload, another value rebuild False step 6 differs
The signature covers the RFC 8785 bytes of the card without signatures, so an edit to the description changes those bytes and verification fails. Read the top half left to right and down, then the bottom half left to right. Steps 5 and 6 are the last two lines of capture/out/17-signed-card.txt. Values are shortened to 8 characters.

"Clients SHOULD verify at least one signature before trusting an Agent Card" spec §8.4.3, and never with an expired or revoked key. Give your verifier an explicit list of allowed algorithms, as the reference SDK does, to prevent algorithm confusion attacks sdk src/a2a/utils/signing.py. Security requirements covers card spoofing.

Sources:spec §1.4, §3.1.11, §3.3.4, §5.4, §8.4, §8.4.1, §8.4.2, §8.4.3, §13.3, §A.2.2 (research/sources/specification.md); proto AgentCard, AgentCapabilities, AgentCardSignature (research/sources/a2a.proto); docs/whats-new-v1.md at v1.0.1; sdk src/a2a/server/request_handlers/default_request_handler_v2.py, src/a2a/utils/signing.py; sdk-go a2asrv/handler.go at v2.6.0; sdk-java SECURITY.md, reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java at v1.4.0.Final; sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0; sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; capture/README.md, capture/run.py; capture/out/01-agent-cards.http, 17-signed-card.txt, 18-extended-card.http

Part 3

Data Model

Every A2A exchange is built from five objects: Message, Part, Artifact, Task, and the contextId that groups tasks.

  1. 3.1Message
  2. 3.2Part
  3. 3.3Artifact and chunks
  4. 3.4Task, TaskStatus, and TaskState
  5. 3.5contextId
3.1

Message

A Message is one turn in a conversation, and its role and three ids say which way it travels and which task it continues.

The planner sends code-reviewer a diff to review. The reviewer asks which base branch to compare against, the planner answers main, and the reviewer finishes the task with two findings.

When you finish this section, you can build a valid message and set its ids to continue a task.

messageId, role, and parts

Message: "one unit of communication between client and server" proto Message. It has eight fields, and only messageId, role, and parts are REQUIRED proto Message. The parts list "MUST contain at least one element" spec §5.7, and Part explains what a part holds.

Figure 3.1 sets three messages of that review side by side and names who sets each field.

Fig. 3.1who sets each field of a messagestructure
Who sets each field of a message A table of the eight Message fields across three messages of the review in 06-input-required.http, which appear one column at a time in the order they were sent. The planner first message 14a03569 has no contextId and no taskId. The code-reviewer question 46494a30 carries contextId 49c5a238 and taskId e5c95b09. The planner answer 096d3737 echoes both ids. Roles are ROLE_USER, ROLE_AGENT and ROLE_USER. metadata, extensions and referenceTaskIds are not set in any of them. A right column says who sets each field, and a last note says the server writes both ids into its stored copy of the first message. FIELD WHO SETS IT messageId minted by the sender for every message contextId minted by the server, echoed by the client taskId minted by the server, echoed to continue a task role the direction: client to server is USER parts the sender, at least one part metadata not set in any kit capture the sender: a JSON object extensions not set in any kit capture the sender: extension URIs referenceTaskIds not set in any kit capture the sender: related tasks REQUIRED in proto Message planner first message 14a03569 omitted omitted ROLE_USER text, raw code-reviewer its question 46494a30 49c5a238 e5c95b09 ROLE_AGENT text planner the answer 096d3737 49c5a238 e5c95b09 ROLE_USER text "main" The server writes contextId 49c5a238 and taskId e5c95b09 into its stored copy of the first message, which the planner sent without them.
A message carries an id its sender mints, while its contextId and taskId come from the server and the client echoes them to continue. Columns are three messages from capture/out/06-input-required.http, in the order they were sent. The right column names who sets each field. Ids shortened to 8 characters.

The sender creates messageId: "This is created by the message creator" proto Message. The kit refuses a message without one, as Errors shows. Spec samples in §11.2 and §6.3 leave messageId out spec §11.2 spec §6.3, so never copy a sample as it stands (conflict D23).

Role: the direction a message travels. The value ROLE_USER marks a message "from the client to the server" proto Role, and ROLE_AGENT marks one from the server to the client proto Role. The planner is an agent, yet its messages to code-reviewer carry ROLE_USER, because it is the client there.

The glossary in §2.2 still writes the roles as "user" and "agent" spec §2.2 spec §5.5 (conflict D35). The spec names no error for a client that sends ROLE_AGENT, and the kit answers -32602, as the kit README lists.

contextId and taskId

A client message with neither id starts something new. The server's messages then carry the contextId it minted, plus the taskId of any task it creates proto Message. The direct reply in 02-message-reply.http carries a contextId and no taskId, because the reviewer answered without creating a task:

a server message with a context and no taskcapture/out/02-message-reply.httpjson
  "result": {
    "message": {
      "messageId": "3b4b1206-4b6f-467f-b735-2d7e4f7f4bcd",
      "contextId": "dc423c62-d5f4-48e5-82c0-4963b9bc4334",
      "role": "ROLE_AGENT",
      "parts": [
        {
          "text": "I review unified diffs in Python, TypeScript and Go. Send the diff as a text/x-diff part and name the base branch."
        }
      ]
    }
  }
The result of the only exchange in the file, complete.

To continue a task, the client copies both ids from the task into its next message, as the planner's answer does:

the planner's answer, with both ids copied from the taskcapture/out/06-input-required.httpjson
    "message": {
      "messageId": "096d3737-42f9-4039-8320-a4737c2b3abe",
      "contextId": "49c5a238-4158-455d-8d63-b2cb39c0ecac",
      "taskId": "e5c95b09-6d4b-4b3b-8693-91e81472d12f",
      "role": "ROLE_USER",
      "parts": [
        {
          "text": "main"
        }
      ]
    }
The message of exchange 2, complete.

Three rules govern those two ids:

  • A client taskId must name a live task. "When a client includes a taskId in a Message, it MUST reference an existing task" spec §3.4.2. An unknown id gets TaskNotFoundError, and CancelTask and terminal states covers a finished task.
  • The two ids must agree. "Agents MUST reject messages containing mismatching contextId and taskId" spec §3.4.3. The spec names no error for this, and both the kit and the reference SDK answer -32602 sdk src/a2a/server/request_handlers/default_request_handler_v2.py.
  • The context follows the task. "Agents MUST infer contextId from the task if only taskId is provided" spec §3.4.3. The kit does, and the table below says what each SDK does.

A message with a contextId and no taskId starts a new task in that conversation, as contextId shows.

What the SDKs do with the ids

"Send Message operations MAY be idempotent. Agents may utilize the messageId to detect duplicate messages" spec §3.3.1. So a retry of a send that timed out can reuse the same messageId. No SDK rejects a repeated id, and without a taskId each one creates another task for it.

SDKMessage with only taskIdSame messageId sent twice to one task
Python 1.2.2Does not read the task, and stamps a fresh contextId on the message sdk src/a2a/server/agent_execution/context.pyAccepted. The history skips a second copy, and the agent runs again sdk src/a2a/server/agent_execution/active_task.py
Go v2.6.0The agent context gets the task's contextId, and the stored message stays as sent sdk-go a2asrv/agentexec.go at v2.6.0Accepted. The history skips a second copy, and the agent runs again sdk-go a2asrv/agentexec.go at v2.6.0
Java v1.4.0.FinalThe agent context and its events get the task's contextId, and the stored message stays as sent sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java at v1.4.0.FinalAccepted. The message is appended to history again, and the agent runs again sdk-java server-common/src/main/java/org/a2aproject/sdk/server/tasks/TaskManager.java at v1.4.0.Final
JS v1.3.0The agent context and its copy of the message get the task's contextId, and the stored message stays as sent sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0Accepted. The message is appended to history again, and the agent runs again sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0
.NET v1.0.0-preview2The agent context gets the task's contextId, and the stored message stays as sent sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2Accepted. The message is appended to history again, and the handler runs again sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2
Rust a2a-server-lf-v0.5.1The executor context gets the task's contextId, and the stored message stays as sent sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1Accepted. The message is appended to history again, and the executor runs again sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1

Five SDKs infer the context for the agent and store the message as sent, and the reference SDK generates a new one. Send both ids, and the difference never reaches you.

Optional fields and the status message

No message in the kit's captures sets these three:

  • referenceTaskIds lists "task IDs that this message references for additional context" proto Message.
  • extensions lists "The URIs of extensions that are present or contributed to this Message" proto Message, and extensions covers them.
  • metadata is a free-form JSON object proto Message.

Status message: the one message a TaskStatus can hold, the agent's words inside a task proto TaskStatus. The reviewer's question was a status message, and the findings themselves arrive in the review.json artifact, as what A2A standardizes explains.

Sources:proto Message, Role, TaskStatus (research/sources/a2a.proto); spec §2.2, §3.3.1, §3.4.2, §3.4.3, §5.5, §5.7, §6.3, §11.2 (research/sources/specification.md); sdk src/a2a/server/agent_execution/active_task.py, src/a2a/server/agent_execution/context.py, src/a2a/server/request_handlers/default_request_handler_v2.py; sdk-go a2asrv/agentexec.go at v2.6.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java, server-common/src/main/java/org/a2aproject/sdk/server/tasks/TaskManager.java at v1.4.0.Final; sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0; sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; capture/README.md; capture/out/02-message-reply.http, 06-input-required.http

3.2

Part

Every piece of content in A2A is a Part that holds exactly one of text, raw, url, or data, and its mediaType says how to read it.

Your planner asks code-reviewer to review a diff. The request carries an instruction and the diff as a file, and the answer carries findings that the planner's own code will parse. A2A carries all three in one container shape, the part, and labels each one with a media type.

When you finish this section, you can build all four kinds of part and predict whether an agent will accept them.

One container, four contents

Part: "The smallest unit of content within a Message or Artifact" spec §2.2. In the proto, Part declares its content as a oneof with four members proto Part:

  • text holds a string.
  • raw holds the bytes of a file. "In JSON serialization, this is encoded as a base64 string." proto Part
  • url holds "A url pointing to the file's content" proto Part.
  • data holds "Arbitrary structured data as a JSON value (object, array, string, number, boolean, or null)" proto Part.

Beside the content sit three optional fields, mediaType, filename, and metadata, and mediaType "is available for all part types" proto Part. The kit's review request sends two kinds in one message:

one message, two kinds of partcapture/out/06-input-required.httpjson
      "parts": [
        {
          "text": "Review this diff of payments-api."
        },
        {
          "raw": "LS0tIGEvcGF5bWVudHMvcmVmdW5kcy5weQor…",
          "filename": "refunds.diff",
          "mediaType": "text/x-diff"
        }
      ]
The parts array of the planner's first message to code-reviewer. The base64 value of raw is cut after 36 of its 236 characters.

Base64 costs size: the 175-byte diff of payments/refunds.py becomes 236 characters in the JSON body. A url part keeps the message small and leaves the download, with its risk, to the receiver.

No kind, no nesting

In version 0.3, a part named its type in a kind field and wrapped a file in a nested file object. Version 1.0 removed both, and "The kind field is no longer part of the protocol and should not be emitted" spec §A.2.1.

A file part is flat: raw or url sits next to filename and mediaType. The specification's own file-part sample has that shape but is not valid JSON spec §6.7 (conflict D24). Copy part shapes from the proto, never from the samples.

Read a part by testing which content member is present, and never send two of them. The reference SDK refuses a part with two members as invalid parameters, -32602, before any agent code runs sdk src/a2a/server/routes/jsonrpc_dispatcher.py. It ignores a stray kind, as the specification asks: "Implementations SHOULD ignore unrecognized fields in messages" spec §5.7.

Fig. 3.2four kinds of part from the kitstructure
Four kinds of part Top: a part holds exactly one of four content members, text, raw, url or data, and every kind can also carry mediaType, filename and metadata. Below, four parts from the captures in the order they were sent: the refunds.diff file the planner sent as raw base64 with media type text/x-diff, the code-reviewer question as text, the review findings as a data part with media type application/json, and a url part labelled image/png that code-reviewer rejects with error -32005 CONTENT_TYPE_NOT_SUPPORTED. ONE PART HOLDS EXACTLY ONE OF OPTIONAL ON EVERY KIND text raw url data mediaType filename metadata FOUR PARTS FROM THE CAPTURES, IN THE ORDER THEY WERE SENT raw LS0tIGEvcGF5bWVudHMvcmVmdW5kcy5weQor first 36 of 236 base64 characters filename "refunds.diff" mediaType "text/x-diff" sent by the planner, a five-line diff · 06-input-required.http text "Which base branch should I compare this diff against?" mediaType absent: the kit reads it as text/plain sent by code-reviewer in status.message · 06-input-required.http data "base": "main" plus "findings", two objects with file, line, severity, finding mediaType "application/json" part 2 of the review.json artifact, sent by code-reviewer · 06-input-required.http url "https://example.com/screenshot.png" mediaType "image/png" sent by the planner · 11-errors.http CONTENT_TYPE_NOT_SUPPORTED rejected with error -32005
The kit's captures use all four kinds of part, and only the image/png part fails, because code-reviewer does not accept that media type. The top row is the shape of every part. Each row below is one captured part, in the order the captures sent them. Its content member is on the left, its media type in green, and its sender underneath. The review.json artifact holds a text part for a person and a data part for code. From capture/out/06-input-required.http and capture/out/11-errors.http.

Who declares media types

Media types are declared in four places, and a skill's modes override the card's defaults proto AgentSkill:

FieldLives inSet bycode-reviewer in the kit
defaultInputModesthe agent cardthe agenttext/plain, text/x-diff
defaultOutputModesthe agent cardthe agenttext/plain, application/json
inputModes, outputModeseach skill on the cardthe agentreview-diff takes text/x-diff and text/plain
acceptedOutputModesconfiguration of a sendthe clientnot sent anywhere in the kit
mediaTypeeach partwhoever sends the parttext/x-diff on refunds.diff

The client's one field is acceptedOutputModes, and the rule on the agent is a SHOULD: "Agents SHOULD use this to tailor their output" proto SendMessageConfiguration. The SDK's request handler never reads the field sdk src/a2a/server/request_handlers/default_request_handler_v2.py, so check the mediaType of every part you receive.

When the media type does not fit

ContentTypeNotSupportedError: "A Media Type provided in the request's message parts or implied for an artifact is not supported by the agent or the specific skill being invoked" spec §3.3.2. Its JSON-RPC code is -32005 spec §5.4, and errors gives its other forms.

In the kit, the planner sends code-reviewer a screenshot as a url part labelled image/png, and the agent refuses before it creates a task:

a media type the agent does not acceptcapture/out/11-errors.httpjson
      "parts": [
        {
          "url": "https://example.com/screenshot.png",
          "mediaType": "image/png"
        }
      ]
…
  "error": {
    "code": -32005,
    "message": "Content type not supported",
    "data": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "CONTENT_TYPE_NOT_SUPPORTED",
        "domain": "a2a-protocol.org",
        "metadata": {
          "mediaType": "image/png"
        }
Exchange 5, cut to the part that was sent and the error that came back inside an HTTP 200 response.

Servers check differently, because the specification gives an unlabelled part no default type. The SDK checks only when the server turns the check on, and then it skips every part with no mediaType sdk src/a2a/utils/input_mode_validator.py. The kit gives such a part a default type and checks it against the card's defaultInputModes, as the kit README lists. The same unlabelled part can pass one server and fail the other.

The security rules ask agents to "reject unexpected media types" spec §13.4, and a url part must be checked against server-side request forgery spec §14.1.1, as security requirements explains.

Sources:spec §2.2, §3.3.2, §5.4, §5.7, §6.7, §13.4, §14.1.1, §A.2.1 (research/sources/specification.md); proto Part, AgentCard, AgentSkill, SendMessageConfiguration (research/sources/a2a.proto); sdk src/a2a/server/routes/jsonrpc_dispatcher.py, src/a2a/server/request_handlers/default_request_handler_v2.py, src/a2a/utils/input_mode_validator.py at a2a-python 1.2.2; capture/out/01-agent-cards.http, 06-input-required.http, 11-errors.http; capture/a2a_ref.py, capture/README.md

3.3

Artifact and chunks

A task returns its results as artifacts, and a long artifact arrives as chunks that you join by artifactId until lastChunk marks the end.

test-runner writes a log while the suite runs. Your planner wants to show that log as it grows, and to keep the verdict once the run ends. On a stream, the log arrives as five chunks of one artifact named test-log.txt. A second artifact, summary.json, arrives in one piece and holds the counts the planner acts on.

When you finish this section, you can rebuild a chunked artifact, name what the specification leaves open, and know which copy to trust.

Results are artifacts

Artifact: "An output (e.g., a document, image, structured data) generated by the agent as a result of a task, composed of Parts" spec §2.2. Two fields are REQUIRED: artifactId, which "must be unique within a task", and parts, which "Must contain at least one part" proto Artifact. The optional fields are name, description, metadata, and extensions.

Results go in artifacts and talk goes in messages, one of the core rules: "Messages SHOULD NOT be used to deliver task outputs" spec §3.7.

Chunks on the stream

On a stream, artifacts travel inside artifactUpdate events. Each TaskArtifactUpdateEvent carries taskId, contextId, and artifact, all REQUIRED, and two optional flags proto TaskArtifactUpdateEvent. When append is true, the content "should be appended to a previously" sent artifact with the same ID. When lastChunk is true, "this is the final chunk of the artifact" proto TaskArtifactUpdateEvent.

four of the six artifact framescapture/out/05-streaming.httpsse
data: {…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "name": "test-log.txt", "parts": [{"text": "collected 12 items\n"}]}}}}

data: {…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "tests/test_charges.py ........  [ 66%]\n"}]}, "append": true}}}

…

data: {…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "1 failed, 11 passed in 4.21s\n"}]}, "append": true, "lastChunk": true}}}

data: {…"artifactUpdate": {…"artifact": {"artifactId": "summary", "name": "summary.json", "parts": [{"data": {"passed": 11, "failed": 1, …}, "mediaType": "application/json"}]}, "lastChunk": true}}}
Frames 3, 4, 7, and 8 of the stream. Each frame loses its JSON-RPC envelope, taskId, and contextId. Frames 5 and 6 are cut whole, and so is the failure list in summary.json.

Figure 3.3 lines up all nine frames of the run with what each artifact frame did to the stored task.

Fig. 3.3five chunks become one stored artifactstructure
Five chunks become one stored artifact Top: the nine stream frames of the streamed run on the kit clock. Frames 1, 2 and 9 are status frames stamped 09:00:01.750Z, 09:00:02.000Z and 09:00:02.250Z, 250 ms apart. Frames 3 to 8 are artifact updates that carry no timestamp and fall between the second and third stamps. Middle: a table of the artifactId and flags each of frames 3 to 8 carries and what it does to the stored task: frame 3 creates artifact test-log with its first part, frames 4 to 7 append one part each, frame 7 sets lastChunk, and frame 8 creates the summary artifact. Bottom: the stored test-log.txt artifact with its five text parts, one per chunk. THE NINE FRAMES ON THE KIT CLOCK frames 3 to 8 carry no timestamp in the stream: artifactId and flags in the stored task 09:00:01.750Z 1 09:00:02.000Z 250 ms 2 3 3 test-log, name: test-log.txt creates artifacts[0] and parts[0] artifacts[0] · artifactId test-log · name test-log.txt parts[0] collected 12 items 4 4 test-log, append: true adds parts[1] parts[1] progress line for tests/test_charges.py, at 66% 5 5 test-log, append: true adds parts[2] parts[2] progress line for tests/test_refunds.py, at 100% 6 6 test-log, append: true adds parts[3] parts[3] FAILED tests/test_refunds.py::test_partial_refund - AssertionError: 450 != 500 7 7 test-log, append: true, lastChunk: true adds parts[4], the last chunk parts[4] 1 failed, 11 passed in 4.21s 8 8 summary, name: summary.json, lastChunk: true creates artifacts[1], one data part 09:00:02.250Z 250 ms 9
The five chunks of test-log.txt become five parts of one stored artifact in frame order, and lastChunk ends that artifact while the stream continues. Top, the nine frames of capture/out/05-streaming.events.json on the kit clock. Frames 1, 2, and 9 sit at their timestamps, and frames 3 to 8 are spaced evenly between. Middle, what frames 3 to 8 carry and what each does to the stored task. Bottom, the stored test-log.txt as GetTask returns it in 04-polling.http. Parts 1 and 2 are the progress lines of the two test files, given in full in the capture.

A lastChunk closes one artifact and nothing more: two frames follow it on this stream. An artifact event has no timestamp field, so the only order among chunks is the stream's order, which every binding must keep spec §3.5.2.

Joining the chunks

The kit and the reference SDK rebuild an artifact the same way. They find the stored artifact with the same artifactId and, if append is true, add the new parts to its parts. If append is absent, they replace the whole artifact, or add it when the id is new (capture/a2a_ref.py, sdk src/a2a/server/tasks/task_manager.py). The five log lines stay five text parts, so join their text in order to get the file.

The specification does not say what append means for an id the server has never seen, and the implementations disagree:

Implementationappend: true for an unknown artifactIdReads lastChunk
a2a-pythonraises InvalidAgentResponseError sdk src/a2a/server/tasks/task_manager.pyno
a2a-goan error, and the task ends in TASK_STATE_FAILED with no cause sdk-go a2aevent/event.go at v2.6.0no
a2a-javadrops the chunk, logs a warning, and keeps the task unchanged sdk-java sdk/spec/util/Utils.java at v1.4.0.Finalno
a2a-jsstores the chunk as a new artifact sdk-js src/server/result_manager.ts at v1.3.0no
a2a-dotnetstores the chunk as a new artifact sdk-dotnet src/A2A/Server/TaskProjection.cs at v1.0.0-preview2no
a2a-rsreturns InvalidAgentResponseError, and the task keeps its last state sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1no
the kitstarts a new artifact (capture/README.md)no

So the same chunk can fail the task, vanish, or become a new artifact. Send the first chunk of every artifact without append. The specification is also silent on whether later metadata merges or replaces.

Do not take field names from the migration guide (conflicts D9 and D10). It wraps the event as taskArtifactUpdate and gives it an index field docs whats-new-v1. The proto names the wrapper artifactUpdate proto StreamResponse, and its event has no index field proto TaskArtifactUpdateEvent. PR #2056 fixed the wrapper names on main and kept the index claim, and no release carries that fix yet.

Trust the stored copy

GetTask returns test-log.txt with all five parts, as the bottom of figure 3.3 shows, and a blocking SendMessage returns the same merged artifacts spec §3.2.2.

After a dropped connection, the missed chunks are never replayed. SubscribeToTask starts instead with a Task snapshot that already holds them spec §3.1.6. Replace your copy of each artifact with the snapshot's before you apply new chunks, as SendStreamingMessage and SubscribeToTask shows.

An artifact can also end without a last chunk: the canceled run in 10-cancel.http stores two parts of a log, and lastChunk never arrives. A stored Artifact has no field that says it is complete, so judge it by the task's state.

Sources:spec §2.2, §3.1.6, §3.2.2, §3.5.2, §3.7 (research/sources/specification.md); proto Artifact, TaskArtifactUpdateEvent, StreamResponse (research/sources/a2a.proto); docs docs/whats-new-v1.md at v1.0.1; sdk src/a2a/server/tasks/task_manager.py at a2a-python 1.2.2; sdk-go a2aevent/event.go and internal/taskupdate/manager.go at v2.6.0; sdk-java spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java at v1.4.0.Final; sdk-js src/server/result_manager.ts at v1.3.0; sdk-dotnet src/A2A/Server/TaskProjection.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; capture/out/04-polling.http, 05-streaming.http, 05-streaming.events.json, 10-cancel.http, 14-resubscribe.http, 14-resubscribe.events.json; capture/a2a_ref.py, capture/planner.py, capture/README.md

3.4

Task, TaskStatus, and TaskState

A Task carries one TaskStatus, whose state is one of nine TaskState values, and the specification names those values without a table of the transitions between them.

Your planner holds three tasks at once: the tests on test-runner, the review on code-reviewer, and the deploy on deployer. Each is a Task with a status, and the planner's next action turns on status.state: wait, act, or stop.

When you finish this section, you can read every field of a Task, classify every TaskState value, and say which transitions the specification defines.

Task

Task: "The fundamental unit of work managed by A2A, identified by a unique ID" spec §2.2. The proto gives Task six fields and marks id and status REQUIRED proto Task. The server creates the id proto Task, and a client cannot supply one spec §3.4.2.

The optional fields are contextId, artifacts, history, and metadata proto Task. contextId, Artifact and chunks, and GetTask and ListTasks cover the first three in turn. Figure 3.4 lays out task 3d8f7801 from the blocking run.

Fig. 3.4the object model of one taskstructure
The object model of one task A dashed frame marks context ac3830af. Inside it, task 3d8f7801 from the blocking run is drawn as a table that fills in the order the run filled it: id and contextId, then the two history messages, the user message 21636369 and the agent message 10cc3c92 with their roles, then the artifacts test-log and summary with their names and part counts, and last status.state TASK_STATE_COMPLETED, status.message 1 failed, 11 passed, and status.timestamp 2026-10-06T09:00:00.750Z. Below, four chips show that a part holds exactly one of text, raw, url or data, with an example of each. Task EVERY MESSAGE AND ARTIFACT HOLDS PARTS text collected 12 items raw refunds.diff, base64 url screenshot.png data "passed": 11 Each part holds exactly one of these four, plus optional mediaType, filename and metadata. context ac3830af groups related tasks and messages, and this run made one task id 3d8f7801 contextId ac3830af history 2 messages messageId: 21636369 role: ROLE_USER parts: 1 text part messageId: 10cc3c92 role: ROLE_AGENT parts: 1 text part artifacts 2 artifacts artifactId: test-log name: test-log.txt parts: 5 text parts artifactId: summary name: summary.json parts: 1 data part status.state TASK_STATE_COMPLETED status.message 1 failed, 11 passed status.timestamp 2026-10-06T09:00:00.750Z
A task carries its own id, its context, one current status, its artifacts, and its history, and every message and artifact inside it is made of parts. Read the task table from the top, in the order the blocking run filled it. The boxes on the right expand its last two rows, and the chips below show the four kinds of part. Values come from capture/out/03-blocking-task.http, ids shortened to 8 characters. The raw and url examples come from 06-input-required.http and 11-errors.http.

TaskStatus

TaskStatus: the current status of a task, with state REQUIRED and message and timestamp optional proto TaskStatus. The message is "A message associated with the status" proto TaskStatus, and the timestamp is an "ISO 8601 Timestamp when the status was recorded" proto TaskStatus.

A task has one status at a time, and the previous status message can move into history, as 06-input-required.http shows.

TaskState

The enum TaskState has nine values, numbered 0 to 8 proto TaskState. In JSON, a state travels as its name spec §5.5.

Terminal state: a state that ends the task for good. The proto marks four values with "This is a terminal state" proto TaskState.

Interrupted state: a state in which the task waits for something from outside the agent. The proto marks two values with "This is an interrupted state" proto TaskState.

Active state: this manual's name for TASK_STATE_SUBMITTED and TASK_STATE_WORKING, which the proto puts in neither group.

ValueNumberClassIn the kit
TASK_STATE_UNSPECIFIED0nonenever sent
TASK_STATE_SUBMITTED1activeevery new task, 04-polling.http
TASK_STATE_WORKING2activethe suite runs, 05-streaming.http
TASK_STATE_COMPLETED3terminalthe tests end, 03-blocking-task.http
TASK_STATE_FAILED4terminalcommit deadbee, 09-failed.http
TASK_STATE_CANCELED5terminalafter CancelTask, 10-cancel.http
TASK_STATE_INPUT_REQUIRED6interruptedthe base branch question, 06-input-required.http
TASK_STATE_REJECTED7terminala production deploy, 08-rejected.http
TASK_STATE_AUTH_REQUIRED8interruptedwaiting for an operator, 07-auth-required.http

TASK_STATE_UNSPECIFIED is the proto3 zero value, for a task "in an unknown or indeterminate state" proto TaskState. The kit never sends it, so treat it and any unknown name as an error.

A client needs both named sets, because a blocking SendMessage returns at a terminal or an interrupted state spec §3.2.2, as SendMessage shows.

The transitions the specification allows

The specification says tasks "progress through a defined lifecycle" spec §2.2, yet neither its prose nor the proto has a transition table. What it says about transitions fits in four rules:

  • Input: "Agents can request additional input mid-processing by transitioning a task to the input-required state" spec §3.4.3. That is the 0.3 name of TASK_STATE_INPUT_REQUIRED (conflict D37).
  • Authorization: an agent "MUST transition the TaskState to TASK_STATE_AUTH_REQUIRED" to ask for it, and can resume without a client message spec §7.6.1.
  • Rejection: an agent can reject "during initial task creation or later" proto TaskState.
  • Finality: a terminal task refuses messages spec §3.1.1, cancellation spec §3.1.5, and subscription spec §3.1.6.

The migration guide says 1.0 clarified "Task state transitions for cancellation scenarios" docs whats-new-v1, and the specification lists none (conflict D38). The extensions guide lists extensions for "Adding new states or transitions" docs extensions and says extensions "should use existing enum values" docs extensions (conflict D29). Expect only the nine values.

With no table in the protocol, an agent's transitions are whatever its code emits. Figure 3.5 collects every transition the kit's agents make.

Fig. 3.5the nine task states and the transitions the kit makesstate
The nine task states and the transitions the kit makes Nine TaskState values without their TASK_STATE_ prefix, and the transitions the kit makes in capture order. SUBMITTED goes to WORKING in 05, and WORKING ends in COMPLETED in 03 and 05. SUBMITTED goes to INPUT_REQUIRED when the reviewer asks a question in 06, and INPUT_REQUIRED returns to WORKING when the client answers. WORKING goes to AUTH_REQUIRED and back when an operator approves in 07. SUBMITTED goes to REJECTED when the deployer refuses production in 08. WORKING ends in FAILED for a missing commit in 09, and in CANCELED after CancelTask in 10. The four terminal states have double borders and no outgoing arrows. UNSPECIFIED, value 0, stands apart and is never sent by the kit. AUTH_REQUIRED value 8 SUBMITTED value 1 WORKING value 2 INPUT_REQUIRED value 6 TERMINAL COMPLETED value 3 in 03 05 06 07 FAILED value 4 commit missing, in 09 CANCELED value 5 CancelTask, in 10 REJECTED value 7 in 08 UNSPECIFIED value 0, unused Prefix TASK_STATE_ dropped from every name. Dashed border: interrupted. Double border: terminal. starts (05) asks for a branch (06) client answers (06) needs approval (07) operator approves (07) refuses production (08)
The kit's captures show nine transitions between task states, and none of them leaves a double-bordered terminal state. Boxes are TaskState values with the TASK_STATE_ prefix dropped. Dashed arrows are the transitions the kit's agents make, in capture order, labelled with the capture that shows each one. Read them as one implementation's behavior, never as the protocol's rules. Blocking captures return only the final state, so their earlier transitions come from the history they return and from capture/agents.py.

The deployer leaves TASK_STATE_AUTH_REQUIRED with no message from the client, as the specification allows spec §7.6.1, and rejects straight from TASK_STATE_SUBMITTED. The reference SDK enforces no table either sdk src/a2a/server/agent_execution/active_task.py, so accept any transition out of a state that is not terminal.

A terminal task refuses a message on its taskId and SubscribeToTask with UnsupportedOperationError, and CancelTask with TaskNotCancelableError spec §3.1.1 spec §3.1.5 spec §3.1.6. CancelTask and terminal states covers the four endings, and the state table lists the client's next step for each value.

Sources:spec §2.2, §3.1.1, §3.1.5, §3.1.6, §3.2.2, §3.4.2, §3.4.3, §5.5, §7.6.1 (research/sources/specification.md); proto Task, TaskStatus, TaskState (research/sources/a2a.proto); docs docs/topics/extensions.md and docs/whats-new-v1.md at v1.0.1; sdk src/a2a/server/agent_execution/active_task.py at a2a-python 1.2.2; capture/out/03-blocking-task.http, 04-polling.http, 05-streaming.http, 06-input-required.http, 07-auth-required.http, 08-rejected.http, 09-failed.http, 10-cancel.http, 11-errors.http; capture/agents.py

3.5

contextId

A contextId groups many tasks into one conversation, a taskId groups many messages into one job, and only the server creates a task id.

code-reviewer stops halfway through a review and asks your planner which base branch to use. The answer must reach that same review. Later the diff changes, and the planner wants a second review that remembers the first. Two ids carry this: the task id for the job and the context id for the conversation.

When you finish this section, you can continue a task, start a follow-up in the same context, and know who creates each id.

Two ids, two scopes

Context: "An optional identifier to logically group related tasks and messages" spec §2.2. "All tasks and messages with the same contextId SHOULD be treated as part of the same conversational session" spec §3.4.1.

Task id: the id of one stateful job. "Task IDs are server-generated when a new task is created in response to a Message" spec §3.4.2, and Message gives the rules for sending one back.

The server usually creates the context too: "Agents MAY generate a new contextId when processing a Message that does not include a contextId field" spec §3.4.1, and the response must carry it spec §3.4.1. In the kit, every first message carries no ids, and every answer, even the direct reply in 02-message-reply.http, brings back a new context.

"Server-generated contextId values SHOULD be treated as opaque identifiers by clients" spec §3.4.1. Behind the id, the agent can keep "internal state, conversational history, or LLM context across multiple interactions" spec §3.4.1.

Propose your own context id only when you understand how the server will process it spec §3.4.1. A server that cannot take it "MUST reject the request with an error and MUST NOT generate a new contextId for the response" spec §3.4.1. The concept guide calls the context id "A server-generated identifier" docs key-concepts, which leaves no room for a proposal (conflict D28). The specification wins, so expect either answer.

Continuing a task

When a task waits for you, answer on that same task and send both of its ids, as Message explains. Interrupted states follows the review's answer.

Only a task that is not terminal takes more messages spec §3.3.3. A message to a finished task fails with UnsupportedOperationError, as CancelTask and terminal states shows.

A follow-up is a new task

Once the review completes, a second round cannot reopen it. A refinement "must initiate a new task within the same contextId" docs life-of-a-task. The specification gives the client two fields for that. "Clients MAY use contextId without taskId to start a new task within an existing conversation context" spec §3.4.3, and "Clients SHOULD use the referenceTaskIds field in Message to explicitly reference related tasks" spec §3.4.3.

The captures stop before such a follow-up. The kit's server would accept one: given a contextId and no taskId, its send creates a new task in that context (capture/a2a_ref.py). Figure 3.6 draws the captured review beside the follow-up a client would send next.

Fig. 3.6one context, two taskstree
One context, two tasks A tree. The root is context 49c5a238. Its first child is the review task e5c95b09, completed, whose history holds four messages from two SendMessage calls: the planner request with the diff and the agent question, then the planner answer main carrying taskId and contextId and the agent reply, followed by the artifact review.json. Its second child, drawn dashed because the captures stop before it, is a follow-up task in the same context whose first message carries contextId 49c5a238 and referenceTaskIds naming e5c95b09 and no taskId. context 49c5a238 created by code-reviewer, opaque to you task e5c95b09 · review TASK_STATE_COMPLETED, status: 2 findings Violet: sent by the planner. Blue: sent by code-reviewer. Ids shortened to 8 characters. CALL 1 · REVIEW A DIFF ROLE_USER · 14a03569 text and raw refunds.diff, no ids sent ROLE_AGENT · 46494a30 asks which base branch to use CALL 2 · ANSWER THE QUESTION ROLE_USER · 096d3737 · main taskId e5c95b09, contextId 49c5a238 ROLE_AGENT · 8f9ee658 Comparing the diff against main RESULT artifact review.json a text part and a data part a follow-up task id created by the server ITS FIRST MESSAGE ROLE_USER · a new messageId contextId 49c5a238 referenceTaskIds [e5c95b09] no taskId A message with taskId e5c95b09 now fails: the task is terminal.
The review task holds both exchanges of the conversation, and a follow-up review joins the same context as a new task, because the review task is terminal. Read top down: the context, its tasks, and each task's history in order. The dashed task and its message show what a client sends next, because the captures stop before it. Ids shortened to 8 characters. From capture/out/06-input-required.http and capture/out/13-history.http.

Nothing limits a context to one task at a time: agents can "create distinct, parallel tasks for each follow-up message" docs life-of-a-task.

What the agent keeps

The review task keeps its four messages in history, and GetTask and ListTasks shows how to read them. Do not treat that history as a full transcript, because "The agent is responsible to determine which Messages are persisted in the Task History" spec §3.7. In particular, "Messages exchanged prior to task creation may not be stored in Task history" spec §3.7.

Contexts do not live forever either. "Agents MAY implement context expiration or cleanup policies and SHOULD document any such policies" spec §3.4.1. Check the agent's documentation before you rely on an old context id.

Sources:spec §2.2, §3.3.3, §3.4.1, §3.4.2, §3.4.3, §3.7 (research/sources/specification.md); proto Task (research/sources/a2a.proto); docs docs/topics/key-concepts.md and docs/topics/life-of-a-task.md at v1.0.1; capture/out/02-message-reply.http, 06-input-required.http, 10-cancel.http, 13-history.http; capture/a2a_ref.py

Part 4

Operations

Eleven operations create, read, cancel, and follow tasks, and each one has rules that a client must know before it calls.

  1. 4.1SendMessage
  2. 4.2Interrupted states: INPUT_REQUIRED and AUTH_REQUIRED
  3. 4.3GetTask and ListTasks
  4. 4.4CancelTask and terminal states
  5. 4.5SendStreamingMessage and SubscribeToTask
  6. 4.6Push notification configs
4.1

SendMessage

SendMessage carries one message and a SendMessageConfiguration, and the agent answers with a Message or a Task that, by default, is terminal or interrupted.

Your planner sends code-reviewer two messages through the same method. The first asks which languages it reviews, and the answer arrives at once as a message. The second carries a diff, and the answer is a task with an id, a state, and later an artifact.

When you finish this section, you can build a SendMessage request and act on whichever shape comes back.

The request and its configuration

SendMessageRequest: a message, as Message defines it, an optional configuration, and metadata proto SendMessageRequest. The configuration has four fields proto SendMessageConfiguration:

  • acceptedOutputModes: the media types the client accepts.
  • taskPushNotificationConfig: a webhook, as push notification configs explains.
  • historyLength: how much history the returned task carries.
  • returnImmediately: whether the server answers before the task settles.

A send blocks unless returnImmediately is true:

In 03-blocking-task.http, one SendMessage with no configuration gets task 3d8f7801 in TASK_STATE_COMPLETED, with every artifact spec §3.2.2. The client never sees TASK_STATE_SUBMITTED or TASK_STATE_WORKING. The specification sets no time limit, and a client that times out never learns the task id spec §3.4.2.

One sentence of the specification says the opposite (conflict D2): "The operation MUST return immediately with either task information or response message" spec §3.1.1. The proto makes false the default proto SendMessageConfiguration, and the proto is normative spec §1.4. The kit and the reference SDK block sdk src/a2a/server/request_handlers/default_request_handler_v2.py.

The SDKs disagree on whether historyLength in the configuration trims the returned task:

SDKconfiguration.historyLength on the returned task
a2a-python 1.2.2applied to the returned Task and to task events of a stream, and a negative value is InvalidParamsError sdk src/a2a/server/request_handlers/default_request_handler_v2.py
a2a-gonever applied, so the task returns with its full stored history sdk-go a2asrv/handler.go at v2.6.0
a2a-javaapplied to the returned Task and to each Task event of a stream, and a negative value is InvalidParamsError sdk-java requesthandlers/DefaultRequestHandler.java at v1.4.0.Final
a2a-jsapplied to the returned task and to task payloads of a stream, and 0 or less omits history sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0
a2a-dotnetnever read by the server, so the task returns with its full stored history sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2
a2a-rsapplied to the returned Task of a blocking send only, and a stream ignores it sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1

Message or Task

SendMessage returns a SendMessageResponse whose payload is a oneof of two members, task and message proto SendMessageResponse. The agent decides, and the specification gives it no rule: "The agent MAY create a new Task to process the provided message asynchronously or MAY return a direct Message response for simple interactions." spec §3.1.1

In the kit, test-runner and deployer create a task for every message. code-reviewer answers with a message when the request has no text/x-diff part and its text ends with a question mark (capture/agents.py). Once a task exists, the choice is over: "Once a task is created, the agent will only return Task objects in response to messages sent" docs life-of-a-task.

A message is the whole answer, with no id to poll, cancel, or subscribe to. It carries a contextId proto Message, so keep it for a follow-up. A task is a handle on work that can outlive the call. Figure 4.1 puts the two answers from code-reviewer side by side. SendStreamingMessage makes the same choice in its first event, as SendStreamingMessage and SubscribeToTask shows.

Fig. 4.1one method, two result shapesdecision
One method, two result shapes The planner sends SendMessage to code-reviewer, and the agent logic decides the shape. A question with no text/x-diff part gets result.message, message 3b4b1206 in context dc423c62, and the client can only read its parts and send a new message in that context. A diff gets result.task, task e5c95b09 in context 49c5a238 in state TASK_STATE_INPUT_REQUIRED, and the client can answer with that taskId or follow the task with GetTask, SubscribeToTask, or CancelTask. SendMessage planner to code-reviewer, one method code-reviewer decides a question with no text/x-diff part: a message anything else: a task result.message messageId 3b4b1206 contextId dc423c62, no task id a question (02) result.task id e5c95b09, contextId 49c5a238 TASK_STATE_INPUT_REQUIRED a diff (06) WHAT THE CLIENT CAN DO NEXT a new SendMessage with contextId dc423c62 to continue Nothing to poll, cancel, or subscribe to. The message is the whole answer. act on task e5c95b09 SendMessage with its taskId answers GetTask and SubscribeToTask follow it CancelTask asks to stop it
The same SendMessage to code-reviewer returns a message for a question and a task for a diff, and only the task gives the client something to follow. Read top down. The agent's own logic picks the shape, here the review_quick rule in capture/agents.py. Left, the reply in capture/out/02-message-reply.http. Right, the task in capture/out/06-input-required.http. Ids are shortened to 8 characters.

Returning at once and polling with GetTask

returnImmediately: a boolean in SendMessageConfiguration that asks the server to answer as soon as the task exists: "The operation MUST return immediately after creating the task, even if processing is still in progress" spec §3.2.2. The flag has no effect on a direct Message reply, on streaming operations, or on push notification configs spec §3.2.2.

Polling: reading a task with GetTask until its state tells you to stop. In 04-polling.http the kit sends with returnImmediately, then polls task d0c28826 twice with historyLength 0:

a send that returns at once, then two pollscapture/out/04-polling.httpjson
    "configuration": {
      "returnImmediately": true
    }
…
        "state": "TASK_STATE_SUBMITTED",
…
  "method": "GetTask",
  "params": {
    "id": "d0c28826-28b8-4b15-8b53-4e01ae7a5a3c",
    "historyLength": 0
  }
…
      "state": "TASK_STATE_WORKING",
…
            "text": "collected 12 items\n"
…
      "state": "TASK_STATE_COMPLETED",
The send and the first GetTask request are cut to their key lines. The second GetTask request matches the first apart from its JSON-RPC id. Each reply is cut to its state, and the first poll also shows its one log part.

The send returns TASK_STATE_SUBMITTED, while the specification's examples name TASK_STATE_WORKING and TASK_STATE_INPUT_REQUIRED spec §3.2.2, so switch on the state you get. The first poll finds TASK_STATE_WORKING and one log part. The second finds TASK_STATE_COMPLETED with every artifact. Figure 4.2 sets the blocking call beside the immediate return and its two polls.

Fig. 4.2a blocking call and a call that returns at oncetimeline
A blocking call and a call that returns at once Two timelines aligned at the request, on the kit clock. Track A: one blocking SendMessage for task 3d8f7801 stays open while the task passes TASK_STATE_SUBMITTED and TASK_STATE_WORKING, which the client never sees, and six artifact chunks arrive. The reply comes at TASK_STATE_COMPLETED, 09:00:00.750. Track B: SendMessage with returnImmediately for task d0c28826 returns at TASK_STATE_SUBMITTED, 09:00:01.000. A first GetTask sees TASK_STATE_WORKING at 09:00:01.250 with one log part, and a second GetTask sees TASK_STATE_COMPLETED at 09:00:01.500 with all artifacts. A · BLOCKING SendMessage with no configuration, task 3d8f7801 B · AT ONCE returnImmediately, then GetTask twice, task d0c28826 TASK_STATE_SUBMITTED TASK_STATE_WORKING TASK_STATE_COMPLETED KIT CLOCK 1 SendMessage, blocking: one reply, the completed task with test-log.txt and summary.json 2 SendMessage with returnImmediately: the reply holds TASK_STATE_SUBMITTED 3 GetTask, historyLength 0: TASK_STATE_WORKING and the first of five log parts 4 GetTask, historyLength 0: TASK_STATE_COMPLETED, five log parts, and summary.json 1 one request, held open until the reply not seen not seen 09:00:00.750 2 SendMessage 09:00:01.000 3 09:00:01.250 4 09:00:01.500
A blocking send holds one request open until this task completes, while returnImmediately answers at TASK_STATE_SUBMITTED and leaves the client to poll. Both tasks are aligned at the request and run through the same states on the kit's clock. Filled squares are states a reply showed the client, and hollow squares are states it never saw. Green ticks are artifact updates, and violet bars are open requests. From capture/out/03-blocking-task.http and 04-polling.http.

Dispatch on the result

The kit's planner tests which member is present before it touches either:

the planner's dispatch on a SendMessage resultcapture/planner.pypython
        while True:
            if "error" in reply:
                self.note(f"  error {reply['error']['code']}: {reply['error']['message']}")
                return None
            result = reply["result"]
            if "message" in result:
                self.note("  direct reply, no task to follow")
                return result["message"]
            task = result["task"] if "task" in result else result
            state = task["status"]["state"]
            self.note(f"  task {task['id'][:8]} is {state}")
            if state in TERMINAL:
                return task
The top of the loop in Planner.delegate. The interrupted states are handled below this cut.

The planner checks error first, because JSON-RPC errors arrive inside an HTTP 200 response. Then it checks message and stops. Only then does it branch on the task's state, as Task, TaskStatus, and TaskState defines it. A client walks through the rest of this loop.

Sources:spec §1.4, §3.1.1, §3.2.2, §3.4.2 (research/sources/specification.md); proto SendMessageRequest, SendMessageConfiguration, SendMessageResponse, Message (research/sources/a2a.proto); docs docs/topics/life-of-a-task.md at v1.0.1; sdk src/a2a/server/request_handlers/default_request_handler_v2.py (a2a-python 1.2.2); sdk-go a2asrv/handler.go at v2.6.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java at v1.4.0.Final; sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0; sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; capture/out/02-message-reply.http, 03-blocking-task.http, 04-polling.http, 06-input-required.http; capture/agents.py, capture/planner.py

4.2

Interrupted states: INPUT_REQUIRED and AUTH_REQUIRED

A task can stop and wait for you, and the two kinds of waiting are answered in different places: input on the task itself, authorization outside the protocol.

The planner sends code-reviewer a diff of payments-api and asks for a review. The reviewer cannot start, because it does not know which branch the diff is against. Later the planner asks deployer to deploy build 2026.10.06-1 to staging, and that task stops until an operator approves it.

Neither task has failed. When you finish this section, you can recognize an interrupted task, answer it in the right place, and detect the moment it resumes.

Two states that wait

Interrupted state: a state in which the agent has stopped work and needs something before it can continue. The proto marks exactly two states this way, TASK_STATE_INPUT_REQUIRED and TASK_STATE_AUTH_REQUIRED, each with the words "This is an interrupted state." proto TaskState

An interrupted task keeps its id, history, and artifacts, and its status message says what the agent needs. A blocking SendMessage returns as soon as the task reaches either state spec §3.2.2. TASK_STATE_INPUT_REQUIRED needs an answer, sent as a new message on the same task. TASK_STATE_AUTH_REQUIRED needs a credential or an approval, sent outside A2A, and the task can resume without a client message.

Answering on the same task

The reviewer creates task e5c95b09 for the planner's diff, stops, and the blocking call returns the task with its question:

the first reply: a task that asks a questioncapture/out/06-input-required.httpjson
    "task": {
      "id": "e5c95b09-6d4b-4b3b-8693-91e81472d12f",
      "contextId": "49c5a238-4158-455d-8d63-b2cb39c0ecac",
      "status": {
        "state": "TASK_STATE_INPUT_REQUIRED",
        "message": {
          …
          "role": "ROLE_AGENT",
          "parts": [
            {
              "text": "Which base branch should I compare this diff against?"
            }
          ]
        },
        "timestamp": "2026-10-06T09:00:00.500Z"
      },
      …
The envelope above the task, the ids inside the status message, and the history are cut. The history holds only the planner's first message.

The answer is an ordinary SendMessage that names the task: "The client continues the interaction by sending a new message with the same taskId and contextId" spec §3.4.3. The planner answers main with both ids. The second reply returns the same task in TASK_STATE_COMPLETED, with the review.json artifact. Figure 4.3 follows the whole exchange.

Fig. 4.3the review question and its answersequence
The review question and its answer A sequence with three lifelines: the planner, the code-reviewer agent, and the stored task e5c95b09. The planner sends a diff with no task id. The reviewer moves the task to TASK_STATE_INPUT_REQUIRED with the question which base branch, and the blocking call returns the task. The planner sends main with the same task and context ids. The reviewer moves the question into history, works, adds the review.json artifact, completes the task, and returns it with four history messages. planner client agent code-reviewer remote agent task e5c95b09 stored task record 1 SendMessage text and refunds.diff, no taskId 2 TASK_STATE_INPUT_REQUIRED asks: which base branch? 3 task, TASK_STATE_INPUT_REQUIRED the blocking call returns here 4 SendMessage: main taskId e5c95b09, contextId 49c5a238 5 history: question, main the question leaves the status 6 TASK_STATE_WORKING Comparing the diff against main 7 artifact review.json a text part and a data part 8 TASK_STATE_COMPLETED status message: 2 findings 9 task, TASK_STATE_COMPLETED review.json, four history messages
The question arrives as the status message of a paused task, and the answer is a new message that carries the same task and context ids. Read top to bottom. Steps 1 to 3 are the first SendMessage and its reply, and steps 4 to 9 answer the question on the same task. Ids are shortened to 8 characters. From capture/out/06-input-required.http.

Authorization that arrives outside A2A

An agent that needs permission partway through a task, such as "An agent requiring human approval before a destructive action is taken" spec §7.6, "MUST transition the TaskState to TASK_STATE_AUTH_REQUIRED" spec §7.6.1. The credential takes another road:

The deployer's third stream frame names the place where an operator approves:

frame 3: the deploy asks for approvalcapture/out/07-auth-required.events.jsonjson
          "statusUpdate": {
            "taskId": "ecf2dcb6-79c8-48f8-8868-24a0541781f4",
            "contextId": "9e8e6bcf-b117-48b9-992a-67971cddcb1b",
            "status": {
              "state": "TASK_STATE_AUTH_REQUIRED",
              "message": {
                …
                "role": "ROLE_AGENT",
                "parts": [
                  {
                    "text": "An operator must approve this deploy at http://localhost:41243/approve/ecf2dcb6-79c8-48f8-8868-24a0541781f4"
                  }
                ]
              },
              "timestamp": "2026-10-06T09:00:00.750Z"
            }
          }
The JSON-RPC envelope and the ids inside the status message are cut.

The approval never enters the protocol. In the kit an operator sends POST /approve/<task id> to the deployer, as the kit README lists. No client message follows: "The agent MAY immediately continue Task processing after receiving the credential, without a requirement that clients send a follow-up message." spec §7.6.1

A client that is itself an agent can pass the request upstream by moving its own task to TASK_STATE_AUTH_REQUIRED spec §7.6.2. Security schemes and in-task authorization covers what such a chain does to credentials.

Fig. 4.4a deploy that waits for an operatorsequence
A deploy that waits for an operator A sequence with three lifelines: the planner, the deployer agent, and an operator outside A2A. The planner opens a stream with SendStreamingMessage. Frames 1 to 3 carry the task in TASK_STATE_SUBMITTED, TASK_STATE_WORKING, and TASK_STATE_AUTH_REQUIRED with an approval link. The stream stays open with no frames while the operator posts to the approval endpoint over plain HTTP. Then frames 4 to 6 carry TASK_STATE_WORKING, the deploy-report.json artifact, and TASK_STATE_COMPLETED, and the stream closes. planner client agent deployer remote agent operator outside A2A 1 SendStreamingMessage deploy 2026.10.06-1 to staging 2 task, TASK_STATE_SUBMITTED frame 1 3 TASK_STATE_WORKING frame 2: Preparing build 4 TASK_STATE_AUTH_REQUIRED frame 3: link to /approve/ecf2dcb6 the stream stays open, no frames while it waits 5 POST /approve/ecf2dcb6 the approval, outside A2A 6 200, approved: true plain HTTP 7 TASK_STATE_WORKING frame 4: Approved by an operator 8 artifactUpdate frame 5: deploy-report.json 9 TASK_STATE_COMPLETED frame 6, then the stream closes
The deploy waits in TASK_STATE_AUTH_REQUIRED while the stream stays open, and the operator's approval travels outside A2A before the same stream carries the result. Read top to bottom. Steps 2 to 4 and 7 to 9 are the six frames of one stream. Steps 5 and 6 are plain HTTP outside A2A. Ids are shortened to 8 characters. From capture/out/07-auth-required.http.

When the stream closes

In this capture the stream stays open through the wait. Closing is a MUST only at terminal states spec §3.1.2, and the HTTP+JSON binding closes at "a terminal or interrupted state" spec §11.7. An agent that waits for a credential outside A2A "SHOULD maintain any active response streams with the client after setting the TaskState to TASK_STATE_AUTH_REQUIRED" spec §7.6.1. The specification does not settle this (conflict D3). Upstream, PR #2270 on branch dev-1.1, approved on 2026-09-29 and unreleased, makes both interrupted states non-terminal and says entering one "does not necessitate closing a streaming response". On main, §11.7 and the streaming guide still say "terminal or interrupted".

The kit closes at TASK_STATE_INPUT_REQUIRED and stays open through TASK_STATE_AUTH_REQUIRED, as the kit README lists. The SDKs split the same way:

SDKWhen the server closes a SendStreamingMessage stream
a2a-python 1.2.2when the agent's execute() call returns, so the agent code decides sdk src/a2a/server/agent_execution/active_task.py
a2a-goafter a Message, a terminal state, or TASK_STATE_INPUT_REQUIRED, and it stays open through TASK_STATE_AUTH_REQUIRED sdk-go internal/taskupdate/final.go at v2.6.0
a2a-javaafter a terminal state, a Message, or an error, and both interrupted states keep it open after execute() returns sdk-java events/EventConsumer.java at v1.4.0.Final
a2a-jsafter a terminal state, TASK_STATE_INPUT_REQUIRED, or a Message, and TASK_STATE_AUTH_REQUIRED keeps it open sdk-js src/server/events/execution_event_queue.ts at v1.3.0
a2a-dotnetwhen the handler returns or calls a TaskUpdater method for a terminal or interrupted state sdk-dotnet src/A2A/Server/TaskUpdater.cs at v1.0.0-preview2
a2a-rsafter a terminal event or when the executor's stream ends, with no rule for interrupted states sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1

A closed stream ends your watching, never the task. Call GetTask, and if the task is not terminal, act on its state and call SubscribeToTask, as SendStreamingMessage and SubscribeToTask shows.

Keep watching while someone else acts

Without an open stream, "the client risks missing Task updates" spec §7.6.2, so subscribe with SubscribeToTask, register a webhook, or poll with GetTask.

The kit's planner subscribes. Its blocking SendMessage returns task a31361b3 in TASK_STATE_AUTH_REQUIRED, and it calls SubscribeToTask at once, in exchanges 7 and 8 of 19-planner.http. It alerts the operator only after the first frame arrives, because the specification defines no replay of earlier events.

Sources:spec §3.1.2, §3.2.2, §3.4.3, §7.6, §7.6.1, §7.6.2, §11.7 (research/sources/specification.md); proto TaskState (research/sources/a2a.proto); sdk src/a2a/server/agent_execution/active_task.py (a2a-python 1.2.2); sdk-go internal/taskupdate/final.go at v2.6.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/events/EventConsumer.java at v1.4.0.Final; sdk-js src/server/events/execution_event_queue.ts at v1.3.0; sdk-dotnet src/A2A/Server/TaskUpdater.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; a2aproject/A2A PR #2270 (unreleased, branch dev-1.1); capture/out/06-input-required.http, 07-auth-required.http, 07-auth-required.events.json, 19-planner.http; capture/planner.py; capture/README.md

4.3

GetTask and ListTasks

GetTask and ListTasks are your recovery path, and both follow exact rules about which parts of a task they leave out.

The planner's stream to deployer drops. A webhook says a test run has finished. The planner restarts with nothing but task ids in its log. In each case the next step is the same: ask the agent for the task it has stored.

When you finish this section, you can fetch the history you need, page through an agent's tasks, and explain a missing field.

GetTask returns the stored task

GetTask takes the task id and an optional historyLength proto GetTaskRequest. It returns the Task as the server holds it: status, artifacts, and history.

An id you are not allowed to see gets TaskNotFoundError spec §3.1.3, so a not-found answer never proves that a task is gone.

historyLength

History: the messages a task has kept, apart from the current status message. The historyLength parameter sets how many a response carries, with the same meaning in every operation spec §3.2.4:

  • Unset: "No limit imposed; server returns its default amount of history (implementation-defined, may be all history)" spec §3.2.4.
  • A number above zero: "Return at most this many recent messages from the task's history" spec §3.2.4.
  • Zero: "No history should be returned; the history field SHOULD be omitted" spec §3.2.4.

With historyLength 1, the kit returns only the newest message of the review task:

GetTask with historyLength 1capture/out/13-history.httpjson
  "method": "GetTask",
  "params": {
    "id": "e5c95b09-6d4b-4b3b-8693-91e81472d12f",
    "historyLength": 1
  }
…
    "history": [
      {
        "messageId": "8f9ee658-e185-409a-9294-8d4d97ef77bb",
        …
        "role": "ROLE_AGENT",
        "parts": [
          {
            "text": "Comparing the diff against main"
          }
        ]
      }
    ],
The JSON-RPC envelopes, the reply's ids, status, and artifacts, and the ids inside the message are cut.

That message is not the newest thing the agent said. The final status message, "2 findings", stays in status.message. In the kit and the reference SDK, a status message joins history only when a newer one replaces it sdk src/a2a/server/tasks/task_manager.py. Read history and status.message together.

The server can also "apply a lower limit" proto GetTaskRequest, and "not all Messages are guaranteed to be persisted in the Task history" spec §3.7. The unset case is implementation-defined, and the SDKs also read 0 and a negative value in different ways:

SDKhistoryLength unset0below 0
a2a-python 1.2.2all history in both operationsan empty history arrayInvalidParamsError sdk src/a2a/utils/task.py
a2a-goall history in GetTask, the last 100 messages in ListTasksthe field is omittedthe field is omitted sdk-go a2asrv/handler.go at v2.6.0
a2a-javaall history in GetTask, none in ListTasksan empty listInvalidParamsError sdk-java requesthandlers/DefaultRequestHandler.java at v1.4.0.Final
a2a-jsall history in both operationsthe field is omittedomitted over JSON-RPC, HTTP 400 over REST sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0
a2a-dotnetall history in both operationsan empty array in GetTask, no field in ListTasksInvalidParamsError over JSON-RPC sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2
a2a-rsall history in both operationsan empty historyan empty history sdk-rust a2a-server/src/task_store/mod.rs at a2a-server-lf-v0.5.1

ListTasks returns pages, newest first

ListTasks filters by contextId, status, and statusTimestampAfter, and shapes the page with pageSize, pageToken, historyLength, and includeArtifacts proto ListTasksRequest. It returns "only tasks visible to the authenticated client" spec §3.1.4, under four rules:

  • Size: "If unspecified, at most 50 tasks will be returned" proto ListTasksRequest, and the maximum is 100.
  • Order: "Implementations MUST return tasks sorted by their status timestamp time in descending order (most recently updated tasks first)." spec §3.1.4
  • Token: "The nextPageToken field MUST always be present in the response" spec §3.1.4. On the last page it holds the empty string.
  • Artifacts: "When includeArtifacts is false (the default), the artifacts field MUST be omitted entirely" spec §3.1.4.

The kit asks test-runner, which holds five tasks, for pageSize 2 and historyLength 0:

the first page of ListTaskscapture/out/12-list-tasks.httpjson
    "tasks": [
      {
        "id": "f22ed7af-848d-4f4d-8cd1-3204d1e1bbaf",
        …
          "state": "TASK_STATE_CANCELED",
          …
          "timestamp": "2026-10-06T09:00:03.750Z"
      …
        "id": "8ae21dcb-7449-4d90-82a6-ac01b20fd782",
        …
          "state": "TASK_STATE_FAILED",
          …
          "timestamp": "2026-10-06T09:00:03.000Z"
      …
    ],
    "nextPageToken": "eyJ0cyI6IjIwMjYtMTAtMDZUMDk6MDA6MDMuMDAwWiIs…",
    "pageSize": 2,
    "totalSize": 5
Each task is cut to its id, state, and status timestamp, and the token is cut after its first 44 characters.

The second page holds the completed runs 728ba084 and d0c28826. Neither page carries artifacts, although both completed runs stored a log and a summary. A third request filters on TASK_STATE_CANCELED and gets one task, a pageSize of 50, and an empty nextPageToken. Figure 4.5 draws the pages beside the history regimes.

Fig. 4.5history regimes and list pagesstructure
History regimes and list pages Top: the stored history of review task e5c95b09, four messages from history[0] to history[3], plus the status message 2 findings, which is never part of history. With historyLength unset GetTask returns all four messages, with historyLength 1 only history[3], and with historyLength 0 no history field. Bottom: the five tasks on test-runner sorted by status timestamp, newest first. ListTasks with pageSize 2 returns f22ed7af and 8ae21dcb, then 728ba084 and d0c28826, and each page ends with an opaque cursor token for the next one. The fifth task, 3d8f7801, was not requested. Neither page carries artifacts or history. A third call filters on TASK_STATE_CANCELED with no historyLength and gets f22ed7af with its full history. A · HISTORY GetTask on the review task e5c95b09, held by code-reviewer historyLength unset 1 B · PAGES ListTasks on test-runner with pageSize 2 and historyLength 0 id state status.timestamp history[0] ROLE_USER Review this diff of payments-api. + refunds.diff history[1] ROLE_AGENT Which base branch should I compare this diff against? history[2] ROLE_USER main history[3] ROLE_AGENT Comparing the diff against main status.message 2 findings, returned every time, never in history history messages returned 4 1 historyLength 0, on task d0c28826 in 04-polling.http: the reply has no history field at all f22ed7af TASK_STATE_CANCELED 09:00:03.750 8ae21dcb TASK_STATE_FAILED 09:00:03.000 728ba084 TASK_STATE_COMPLETED 09:00:02.250 d0c28826 TASK_STATE_COMPLETED 09:00:01.500 3d8f7801 TASK_STATE_COMPLETED 09:00:00.750 page 1 returns cursor 1 page 2 returns cursor 2 not requested totalSize 5 on both pages, and no task carries artifacts or history filter on TASK_STATE_CANCELED, no historyLength: f22ed7af with its full history, token ""
GetTask trims history to the newest messages you ask for, and ListTasks returns newest-first pages that leave out artifacts until you ask for them. Top, the stored history of task e5c95b09 and what each historyLength returns. Bottom, the five tasks on test-runner in list order, grouped into the pages the kit requested. Ids are shortened to 8 characters. From capture/out/13-history.http, 04-polling.http, and 12-list-tasks.http.

The specification requires cursor-based pagination and gives the token no format spec §3.1.4, so each SDK makes its own:

SDKTokenPage sizeLast page
a2a-python 1.2.2URL-safe base64 of a JSON cursor with the status timestamp and the task id sdk src/a2a/utils/task.py50 by default, 1 to 100 or error -32602""
a2a-goURL-safe base64 of the update time and the task id sdk-go a2asrv/taskstore/inmemory.go at v2.6.050 by default, 1 to 100 or error -32600""
a2a-javaplain text, the status time in epoch milliseconds, a colon, and the task id sdk-java util/PageToken.java at v1.4.0.Final50 by default, 1 to 100 or error -32602""
a2a-jsbase64 of the status timestamp, a bar, and the task id sdk-js src/server/utils.ts at v1.3.050 by default, 1 to 100 or error -32602""
a2a-dotneta decimal offset such as 50 sdk-dotnet src/A2A/Server/InMemoryTaskStore.cs at v1.0.0-preview250 by default, and only JSON-RPC rejects a size outside 1 to 100""
a2a-rsa decimal offset over tasks sorted by id, against the order rule above sdk-rust a2a-server/src/task_store/inmemory.rs at a2a-server-lf-v0.5.150 by default, and a size above 100 is cut to 100""

Send every parameter

The third request sends no historyLength and gets the full history, as the reference SDK would sdk src/a2a/server/request_handlers/default_request_handler_v2.py. With 0, the SDK returns an empty history array where the specification asks for no field sdk src/a2a/server/routes/common.py. So treat a missing history and an empty one alike.

Pass back exactly the token you were given. The migration guide calls these fields cursor, limit, and nextCursor, names the proto does not define (conflict D12).

The bound of statusTimestampAfter is inclusive: "Only tasks with a status timestamp time greater than or equal to this value will be returned" proto ListTasksRequest. To fetch only what changed, send the newest status timestamp you have seen, and skip the boundary task you already hold.

Sources:spec §3.1.3, §3.1.4, §3.2.4, §3.7 (research/sources/specification.md); proto GetTaskRequest, ListTasksRequest (research/sources/a2a.proto); sdk src/a2a/server/tasks/task_manager.py, src/a2a/server/request_handlers/default_request_handler_v2.py, src/a2a/server/routes/common.py, src/a2a/utils/task.py (a2a-python 1.2.2); sdk-go a2asrv/handler.go, a2asrv/taskstore/inmemory.go at v2.6.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java, spec/src/main/java/org/a2aproject/sdk/spec/util/PageToken.java at v1.4.0.Final; sdk-js src/server/request_handler/default_request_handler.ts, src/server/utils.ts at v1.3.0; sdk-dotnet src/A2A/Server/A2AServer.cs, src/A2A/Server/InMemoryTaskStore.cs at v1.0.0-preview2; sdk-rust a2a-server/src/task_store/mod.rs, a2a-server/src/task_store/inmemory.rs at a2a-server-lf-v0.5.1; capture/out/04-polling.http, 12-list-tasks.http, 13-history.http

4.4

CancelTask and terminal states

Four terminal states end a task, the agent chooses three of them and CancelTask asks for the fourth, and no later call moves a task out of one.

The planner asks test-runner to run the unit tests at commit 9f3c2e1. One test fails, and the task comes back in TASK_STATE_COMPLETED. A planner that reads only the state deploys broken code, and a planner that reads the artifact stops the release.

When you finish this section, you can tell the four endings apart, call CancelTask, and predict what a finished task answers.

Terminal states

Terminal state: one of the four states that end a task: TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, and TASK_STATE_REJECTED. The proto marks each one with "This is a terminal state" proto TaskState.

A terminal task never changes again: "Once a task reaches a terminal state (completed, canceled, rejected, or failed), it cannot restart" docs life-of-a-task. Figure 4.6 sets the four endings side by side.

Fig. 4.6four ways a task endscomparison
Four ways a task ends Four cards, one per terminal state from the kit. Completed: test-runner task 3d8f7801, chosen by the agent because the suite ran, status message 1 failed, 11 passed, with test-log.txt and summary.json. Failed: task 8ae21dcb, chosen by the agent because commit deadbee does not exist, no artifacts. Canceled: task f22ed7af, chosen by the client with CancelTask, two log chunks kept. Rejected: deployer task 23cf3710, refused by policy, no artifacts. Below, a message to the canceled task gets UnsupportedOperationError and a second cancel gets TaskNotCancelableError. FOUR TERMINAL STATES FROM THE KIT TASK_STATE_COMPLETED test-runner, task 3d8f7801 chosen by the agent: the suite ran status message 1 failed, 11 passed artifacts: test-log.txt, summary.json TASK_STATE_FAILED test-runner, task 8ae21dcb chosen by the agent: no such commit status message Commit deadbee does not exist in payments-api. artifacts: none TASK_STATE_CANCELED test-runner, task f22ed7af chosen by the client: CancelTask status message Canceled at the client's request. artifacts: test-log.txt, two chunks TASK_STATE_REJECTED deployer, task 23cf3710 chosen by the agent: its policy status message Production deploys are outside this agent's policy. Use the release train. artifacts: none AFTER THE END calls to the canceled task f22ed7af, from 10-cancel.http SendMessage, taskId f22ed7af UnsupportedOperationError, -32004 CancelTask, a second time TaskNotCancelableError, -32002
The agent chose three of these endings and the client one, and once a task ends, the server refuses any further message or cancel. Each card is one task from the kit: who chose the ending, the final status message, and the artifacts left behind. The bottom rows show what the canceled task answers afterwards. Ids are shortened to 8 characters. From capture/out/03-blocking-task.http, 09-failed.http, 10-cancel.http, and 08-rejected.http.

The agent's three endings

Task 3d8f7801 ran the suite at 9f3c2e1. One of twelve tests failed, and the task still ended in TASK_STATE_COMPLETED:

a completed task that reports a failing testcapture/out/03-blocking-task.httpjson
    "task": {
      "id": "3d8f7801-f421-4e4e-aa2d-8b6da17d9281",
      …
        "state": "TASK_STATE_COMPLETED",
        …
              "text": "1 failed, 11 passed"
      …
          "artifactId": "summary",
          "name": "summary.json",
          "parts": [
            {
              "data": {
                "passed": 11,
                "failed": 1,
Cut to the state, the status message, and the start of the summary.json data part.

Completed "Indicates that a task has finished successfully" proto TaskState, and the job was to run the suite. Whether the code is good is a result, and results travel in artifacts spec §3.7. The kit's planner reads the failed count in summary.json before a deploy.

Commit deadbee does not exist, so task 8ae21dcb ended in TASK_STATE_FAILED, which "Indicates that a task has finished with an error" proto TaskState. The same request fails the same way, so fix the input and start a new task. If the agent crashes, the reference SDK stores the task as failed, yet a blocking send gets an error sdk src/a2a/server/agent_execution/active_task.py. Treat that error as an unknown outcome.

The planner asked deployer for a production deploy. Task 23cf3710 went straight to TASK_STATE_REJECTED, which "Indicates that the agent has decided to not perform the task" proto TaskState. A loop that stops only on completed, failed, and canceled polls a rejected task forever. The same request gets the same refusal, so follow the status message: "Production deploys are outside this agent's policy. Use the release train."

CancelTask

CancelTask: the operation that asks the server to stop a task, given its id proto CancelTaskRequest. Task f22ed7af was running the full suite when the planner called it. The reply was the task in TASK_STATE_CANCELED, with the two log chunks it had already written.

A cancel is a request: "The server will attempt to cancel the task, but success is not guaranteed" spec §3.1.5. The specification does not say which unfinished states a server must cancel, and the reference SDK cancels any task that is not terminal sdk src/a2a/server/request_handlers/default_request_handler_v2.py.

A second CancelTask gets TaskNotCancelableError, -32002, from the kit and the reference SDK sdk src/a2a/server/request_handlers/default_request_handler_v2.py, as §3.1.5 lists for a task "already completed, failed, or canceled" spec §3.1.5. Section 3.3.1 disagrees: "Cancel Task operations are idempotent - multiple cancellation requests have the same effect" spec §3.3.1, and a repeat on a purged task "MAY return TaskNotFoundError" spec §3.3.1.

So after a cancel that errors, call GetTask. Canceled means an earlier cancel worked, another terminal state means the task ended first, and running means the server cannot stop it spec §3.1.5.

Calls on a finished task

A finished task still answers. GetTask returns the final task spec §3.1.3. SendMessage with its taskId gets UnsupportedOperationError, -32004 spec §3.1.1, and SubscribeToTask gets the same error spec §3.1.6. CancelTask gets TaskNotCancelableError spec §3.1.5. UnsupportedOperationError also covers unrelated failures, so read its details, as Errors explains.

The official SDKs send the same error, with two differences in the other bindings:

SDKJSON-RPCHTTP+JSONgRPC
a2a-python 1.2.2 sdk src/a2a/server/agent_execution/active_task.py sdk src/a2a/utils/errors.py-32004400FAILED_PRECONDITION
a2a-go sdk-go a2asrv/agentexec.go at v2.6.0-32004400FAILED_PRECONDITION
a2a-java sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java at v1.4.0.Final-32004400UNIMPLEMENTED
a2a-js sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0-32004400FAILED_PRECONDITION
a2a-dotnet sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2-32004400, a problem-details bodynone at this release
a2a-rs sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1-32004400FAILED_PRECONDITION

a2a-java answers gRPC with UNIMPLEMENTED where §5.4 maps the error to FAILED_PRECONDITION spec §5.4. a2a-go in its experimental cluster mode answers InvalidParamsError, -32602, instead sdk-go internal/taskexec/distributed_manager.go at v2.6.0.

To go on, start a new task in the same context, naming the finished one in referenceTaskIds spec §3.4.3, as contextId shows.

Sources:spec §3.1.1, §3.1.3, §3.1.5, §3.1.6, §3.3.1, §3.4.3, §3.7, §5.4 (research/sources/specification.md); proto TaskState, CancelTaskRequest (research/sources/a2a.proto); docs life-of-a-task (research/sources/docs.md); sdk src/a2a/server/agent_execution/active_task.py, src/a2a/server/request_handlers/default_request_handler_v2.py, src/a2a/utils/errors.py (a2a-python 1.2.2); sdk-go a2asrv/agentexec.go, internal/taskexec/distributed_manager.go (a2a-go v2.6.0); sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java (a2a-java v1.4.0.Final); sdk-js src/server/request_handler/default_request_handler.ts (a2a-js v1.3.0); sdk-dotnet src/A2A/Server/A2AServer.cs (a2a-dotnet v1.0.0-preview2); sdk-rust a2a-server/src/handler.rs (a2a-rs a2a-server-lf-v0.5.1); capture/out/03-blocking-task.http, 08-rejected.http, 09-failed.http, 10-cancel.http, 14-resubscribe.http; capture/planner.py

4.5

SendStreamingMessage and SubscribeToTask

A stream is a live view of one task, so a dropped connection costs you the frames sent while you were away and never the task itself.

The planner asks test-runner to run the full suite of payments-api at commit 9f3c2e1 and watches the log arrive on a stream. Four frames in, the client closes the connection. The suite keeps running, and the planner still wants the rest of the log.

When you finish this section, you can open a stream with either operation, tell when it has ended, and resume it after a drop.

What a stream carries

Stream: an HTTP response of type text/event-stream whose body is a series of Server-Sent Events spec §9.4.2 spec §11.7. Every event carries one StreamResponse, which holds exactly one of task, message, statusUpdate, or artifactUpdate proto StreamResponse.

Two operations open a stream: SendStreamingMessage sends a message and watches the work it starts, and SubscribeToTask watches a task that already exists spec §3.5.1.

the first stream, closed by the client after four framescapture/out/14-resubscribe.httpsse
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"jsonrpc": "2.0", "id": 1, "result": {"task": {… "state": "TASK_STATE_SUBMITTED", …

data: {"jsonrpc": "2.0", "id": 1, "result": {"statusUpdate": {… "state": "TASK_STATE_WORKING", …

data: {"jsonrpc": "2.0", "id": 1, "result": {"artifactUpdate": {… "name": "test-log.txt", …

data: {"jsonrpc": "2.0", "id": 1, "result": {"artifactUpdate": {… "append": true}}}

# the client closes the connection after 4 events
Each frame is cut inside its line. The last line is the kit's own note in the transcript, written when the client drops the connection.

The streaming guide calls each payload "a JSON-RPC 2.0 Response object, typically a SendStreamingMessageResponse" docs streaming-and-async, a type the proto does not define proto A2AService (conflict D33). The migration guide names the update members taskStatusUpdate and taskArtifactUpdate docs whats-new-v1, where the proto and these frames say statusUpdate and artifactUpdate (conflict D9). Match on the proto names.

The specification defines no event: types, id: lines, retry: value, or keep-alive spec §11.7. The reference SDK sends keep-alive comments and marks a late error with event: error sdk src/a2a/server/routes/jsonrpc_dispatcher.py, so skip comments and check each payload for error.

Order and the close

"All implementations MUST deliver events in the order they were generated" spec §3.5.2, and "Events MUST be broadcast to all active streams for that task" spec §3.5.2. None of those streams owns the task:

Version 1.0 has no end marker: "final boolean field removed from TaskStatusUpdateEvent" docs whats-new-v1. A task stream "MUST close when the task reaches a terminal state" spec §3.1.2, and so does a SubscribeToTask stream spec §3.1.6. After a message frame the stream closes at once spec §3.1.2.

At interrupted states, §11.7 and the streaming guide close the stream, while §7.6.1 asks agents to keep it open for TASK_STATE_AUTH_REQUIRED spec §11.7 docs streaming-and-async spec §7.6.1 (conflict D3). Interrupted states lists where each SDK closes, so a close by itself means only that reading has ended.

A dropped connection and SubscribeToTask

Fig. 4.7a stream dropped after four frames and picked back upsequence
A dropped stream and its subscription Three lifelines: planner, test-runner and its task store. The planner opens SendStreamingMessage and receives four frames: the task in TASK_STATE_SUBMITTED, the working status, and two artifactUpdate chunks of test-log.txt. The planner then closes the connection. test-runner appends chunks 03 and 04 to the stored task while no stream is open, so no client sees them. The planner calls SubscribeToTask, test-runner reads the stored task, and the first frame is a Task snapshot whose test-log.txt already holds four parts. Frames 2 to 5 carry chunks 05 to 08, frame 6 the completed status, and the stream closes. A second SubscribeToTask on the finished task fails with -32004 UnsupportedOperationError. planner client agent test-runner localhost:41241 task store inside test-runner 1 SendStreamingMessage run the full suite at 9f3c2e1 2 frame 1 · task SUBMITTED task 25239d4c 3 frame 2 · statusUpdate WORKING 4 frame 3 · artifactUpdate test_suite_01, names test-log.txt 5 frame 4 · artifactUpdate test_suite_02, append: true client closes the connection 6 append test_suite_03 no stream is open 7 append test_suite_04 nobody receives a frame 8 SubscribeToTask 25239d4c 9 stored task log parts 01 to 04 so far 10 frame 1 · task WORKING snapshot: test-log.txt, 4 parts 11 frames 2 to 5 · artifactUpdate test_suite_05 to 08, lastChunk on 08 12 frame 6 · statusUpdate COMPLETED 96 passed, then the stream closes 13 SubscribeToTask 25239d4c again, after the end 14 -32004 UnsupportedOperationError the task is TASK_STATE_COMPLETED
A dropped stream loses the frames sent while it was closed, and SubscribeToTask resumes with a Task snapshot that already holds their content. Read top to bottom. Steps 1 to 5 are the first stream, and only the writes nobody saw are drawn to the task store. Frame numbers count per stream. From capture/out/14-resubscribe.http, ids shortened to 8 characters.

After frame 4 the agent appends chunks 03 and 04 to test-log.txt, and no open connection carries their artifactUpdate events, as figure 4.7 shows. The events are gone, and the stored task holds both chunks.

SubscribeToTask: the operation that opens a new stream on an existing task, given its id proto SubscribeToTaskRequest. Its first frame is a snapshot: "The operation MUST return a Task object as the first event in the stream" spec §3.1.6.

the first frame after SubscribeToTask, a full Task snapshotcapture/out/14-resubscribe.events.jsonjson
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "task": {
      "id": "25239d4c-798b-4754-be06-d1f94d0645b1",
…
      "status": {
        "state": "TASK_STATE_WORKING",
…
      "artifacts": [
        {
          "artifactId": "test-log",
          "name": "test-log.txt",
          "parts": [
            {
              "text": "tests/test_suite_01.py ........\n"
…
              "text": "tests/test_suite_04.py ........\n"
The status message and the history are cut, and so are parts 02 and 03 of the log.

Frames 2 to 5 append chunks 05 to 08, and frame 6 carries TASK_STATE_COMPLETED with 96 passed. Nothing is replayed, and the specification defines no cursor. So replace your partial test-log.txt with the snapshot's artifact, then append the frames that follow.

The snapshot holds only the current status, so a reconnecting client "MAY not receive all status update messages" spec §3.7, and results belong in artifacts.

A second SubscribeToTask after the task completes fails with UnsupportedOperationError, -32004, as plain JSON with no stream spec §3.1.6 sdk src/a2a/server/routes/jsonrpc_dispatcher.py. So stop reconnecting and read the final task with GetTask, as GetTask and ListTasks shows.

The subscribe verb over HTTP+JSON

The HTTP+JSON route is /tasks/{id}:subscribe, and the specification names two verbs for it. The proto binds it to GET proto A2AService, and the method table and the URL list show POST spec §5.3 spec §11.3.2 (conflict D1). On 2026-09-29 the project's steering committee chose GET in PR #2068, which matches the proto. That change is unreleased on branch dev-1.1, and PR #1891 for POST stays open on main. The SDKs split three ways:

SDKServer acceptsClient sends
a2a-python 1.2.2 sdk src/a2a/server/routes/rest_routes.pyGET and POSTPOST
a2a-go sdk-go a2asrv/rest.go at v2.6.0GET and POSTPOST
a2a-java sdk-java reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java at v1.4.0.FinalPOSTPOST
a2a-js sdk-js src/server/express/rest_handler.ts at v1.3.0GET and POSTPOST
a2a-dotnet sdk-dotnet src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs at v1.0.0-preview2POSTPOST
a2a-rs sdk-rust a2a-server/src/rest.rs at a2a-server-lf-v0.5.1GET and POSTGET

A POST reaches all six servers, and a GET reaches four, so send POST as five of the six clients do. A server should accept both.

Sources:spec §3.1.2, §3.1.6, §3.5.1, §3.5.2, §3.7, §5.3, §7.6.1, §9.4.2, §11.3.2, §11.7 (research/sources/specification.md); proto StreamResponse, SubscribeToTaskRequest, A2AService (research/sources/a2a.proto); docs whats-new-v1, streaming-and-async (research/sources/docs.md); a2aproject/A2A PR #2068 and PR #1891, checked 2026-10-06; sdk src/a2a/server/routes/jsonrpc_dispatcher.py, src/a2a/server/routes/rest_routes.py, src/a2a/client/transports/rest.py (a2a-python 1.2.2); sdk-go a2asrv/rest.go, a2aclient/rest.go (a2a-go v2.6.0); sdk-java reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java, client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java (a2a-java v1.4.0.Final); sdk-js src/server/express/rest_handler.ts, src/client/transports/rest_transport.ts (a2a-js v1.3.0); sdk-dotnet src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs, src/A2A/Client/A2AHttpJsonClient.cs (a2a-dotnet v1.0.0-preview2); sdk-rust a2a-server/src/rest.rs, a2a-client/src/rest.rs (a2a-rs a2a-server-lf-v0.5.1); capture/out/14-resubscribe.http, 14-resubscribe.events.json

4.6

Push notification configs

A TaskPushNotificationConfig tells an agent where to POST the same StreamResponse objects a stream carries, with credentials you chose, after your request has returned.

A test run can outlast any connection the planner wants to hold open. So the planner sends SendMessage with returnImmediately: true and a webhook, gets the task back in TASK_STATE_SUBMITTED, and closes the connection. From then on test-runner posts eight notifications to http://localhost:41250/a2a-events.

When you finish this section, you can register a webhook, authenticate what arrives, and build a receiver that stays correct when a notification comes twice.

The configuration object

TaskPushNotificationConfig: the object that tells an agent where to post updates for one task and how to authenticate them proto TaskPushNotificationConfig. Only url is REQUIRED. The optional token is "A token unique for this task or session" proto TaskPushNotificationConfig, and the optional authentication holds a scheme such as Bearer and its credentials proto AuthenticationInfo. The server assigns the config's id. The prose calls this object PushNotificationConfig, a type the proto does not define, so use the proto name spec §4.3.1 (conflict D4).

a webhook registered inside SendMessagecapture/out/15-push.httphttp
POST /a2a/jsonrpc HTTP/1.1
…
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "85750621-02fb-4d4f-b57f-bc5af71a1bfc",
…
    "configuration": {
      "returnImmediately": true,
      "taskPushNotificationConfig": {
        "url": "http://localhost:41250/a2a-events",
        "token": "planner-run-0006",
        "authentication": {
          "scheme": "Bearer",
          "credentials": "hook_secret_91d2"
        }
The headers and the message parts are cut. The configuration carries no task id, because the task does not exist yet.

Inline, as here, the config rides in configuration.taskPushNotificationConfig proto SendMessageConfiguration. That leaves no gap, because the kit and the reference SDK store it before the first event sdk src/a2a/server/request_handlers/default_request_handler_v2.py. For a task that already exists, CreateTaskPushNotificationConfig takes the config itself, with its taskId proto A2AService. The prose and Appendix A name a CreateTaskPushNotificationConfigRequest instead, which the proto does not define spec §10.4.7 (conflict D5).

Agents SHOULD validate the url spec §13.2, as Security requirements explains.

What arrives at the webhook

Each notification is an HTTP POST whose body is one StreamResponse spec §4.3.3. The JSON is always the HTTP+JSON form, whatever binding the agent speaks spec §3.5.1. So test-runner posts a plain statusUpdate with no JSON-RPC envelope, as figure 4.8 shows.

Fig. 4.8one test run reported through a webhooksequence
A webhook for one test run Three lifelines: planner, test-runner and the planner webhook receiver on port 41250. The planner sends SendMessage with returnImmediately and a push configuration, and gets back the task in TASK_STATE_SUBMITTED at once. test-runner then posts eight notifications to the receiver: the working status with an Authorization Bearer header, five artifactUpdate chunks of test-log.txt, the summary.json artifact, and the completed status. The receiver answers each POST with 204 No Content. After the task has finished, the planner lists, reads, and deletes the push configuration, whose server-assigned id is 92c697ef. planner client agent test-runner localhost:41241 webhook receiver :41250/a2a-events 1 SendMessage returnImmediately, webhook config 2 task d7d3dceb SUBMITTED the call returns at once 3 POST statusUpdate WORKING Authorization: Bearer hook_secret_91d2 4 5 POSTs · artifactUpdate five chunks of test-log.txt 5 POST artifactUpdate summary.json, a data part 6 POST statusUpdate COMPLETED 1 failed, 11 passed 7 204 No Content the answer to every POST 8 ListTaskPushNotificationConfigs sent after the task finished 9 one config, id 92c697ef the id the server assigned 10 GetTaskPushNotificationConfig taskId and id 11 the same config 12 DeleteTaskPushNotificationConfig 13 result: {} no more POSTs for this config
With a webhook registered, test-runner posts eight StreamResponse bodies to the planner's receiver after the SendMessage call has already returned. Read top to bottom in the order the kit ran the steps. The capture file prints the eight deliveries last, but they arrived before the config calls. From capture/out/15-push.http, ids shortened to 8 characters.
the first notification, as the receiver saw itcapture/out/15-push.httphttp
POST /a2a-events HTTP/1.1
Content-Type: application/a2a+json
Authorization: Bearer hook_secret_91d2
X-A2A-Notification-Token: planner-run-0006

{
  "statusUpdate": {
    "taskId": "d7d3dceb-7333-4676-90ce-36542116a45c",
    "contextId": "e051d022-3c39-4f5c-bd9c-7a902ae474cd",
    "status": {
      "state": "TASK_STATE_WORKING",
…
      "timestamp": "2026-10-06T09:00:05.000Z"
The status message is cut. The kit's receiver records only these three headers.

"The agent MUST include authentication credentials in the request headers as specified in the PushNotificationConfig.authentication field" spec §4.3.3. The specification gives the token no transport, and the kit sends it in X-A2A-Notification-Token, the reference SDK's header sdk src/a2a/server/tasks/base_push_notification_sender.py.

The specification's request format uses application/a2a+json spec §4.3.3, and the reference SDK posts application/json sdk src/a2a/server/tasks/base_push_notification_sender.py, as the kit README lists. The SDKs also differ on the token header and the timeout, and none retries:

SDKContent-TypeToken headerTimeoutRetries
a2a-python 1.2.2 sdk src/a2a/server/tasks/base_push_notification_sender.pyapplication/jsonX-A2A-Notification-Tokenset by the httpx client the server passes innone
a2a-go sdk-go a2asrv/push/sender.go at v2.6.0application/jsonA2A-Notification-Token30 snone
a2a-java sdk-java server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java at v1.4.0.Finalapplication/jsonX-A2A-Notification-Tokennone setnone
a2a-js sdk-js src/server/push_notification/default_push_notification_sender.ts at v1.3.0application/a2a+jsonX-A2A-Notification-Token, only when authentication is empty5 snone
a2a-dotnet sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2no sender: every config operation returns -32003
a2a-rs sdk-rust a2a-server/src/push/sender.rs at a2a-server-lf-v0.5.1application/jsonA2A-Notification-Token30 snone

Every sender in the table posts one StreamResponse per event. So accept both content types, read either token header, and put the secret you rely on in authentication. The specification does not say which events are posted, and the reference SDK also posts the first Task sdk src/a2a/server/tasks/push_notification_sender.py, so handle every StreamResponse member.

Delivery is attempted at least once

At least once means a notification can arrive twice, so the receiver has duties too: "Clients MUST respond with HTTP 2xx status codes to acknowledge successful receipt", "Clients SHOULD process notifications idempotently, as duplicate deliveries may occur", and "Clients MUST validate the task ID matches an expected task" spec §4.3.3.

A status update is safe to apply twice, keyed by taskId and timestamp. An artifactUpdate with append: true is not, and the specification defines no delivery id. So treat each notification as a signal and read the task with GetTask, as the project's guide suggests docs streaming-and-async.

Retries are optional: "Agents MAY implement retry logic with exponential backoff for failed deliveries" spec §4.3.3. No SDK in the table retries, so a receiver that was down reads the task with GetTask when it comes back.

Managing configurations

Four operations manage the stored configs of a task: create, get, list, and delete proto A2AService. The capture lists, reads, and deletes its config after the run, as figure 4.8 shows. A config "MUST persist until task completion or explicit deletion" spec §3.1.7, and a delete "MUST be idempotent" spec §3.1.10.

Get and list return the stored credentials unchanged, in the kit and the reference SDK sdk src/a2a/server/request_handlers/default_request_handler_v2.py, so whoever can call them can read the webhook secret. All four operations need pushNotifications on the card, or they return PushNotificationNotSupportedError spec §3.3.4.

Sources:spec §3.1.7, §3.1.10, §3.3.4, §3.5.1, §4.3.1, §4.3.3, §10.4.7, §13.2, Appendix A (research/sources/specification.md); proto TaskPushNotificationConfig, AuthenticationInfo, SendMessageConfiguration, A2AService (research/sources/a2a.proto); docs streaming-and-async (research/sources/docs.md); sdk src/a2a/server/tasks/base_push_notification_sender.py, src/a2a/server/tasks/push_notification_sender.py, src/a2a/server/request_handlers/default_request_handler_v2.py (a2a-python 1.2.2); sdk-go a2asrv/push/sender.go (a2a-go v2.6.0); sdk-java server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java (a2a-java v1.4.0.Final); sdk-js src/server/push_notification/default_push_notification_sender.ts (a2a-js v1.3.0); sdk-dotnet src/A2A/Server/A2AServer.cs (a2a-dotnet v1.0.0-preview2); sdk-rust a2a-server/src/push/sender.rs (a2a-rs a2a-server-lf-v0.5.1); capture/out/15-push.http; capture/run.py, capture/a2a_ref.py, capture/README.md

Part 5

Bindings

The same operations travel over JSON-RPC, HTTP+JSON, and gRPC, with two headers and one error model shared by all three.

  1. 5.1JSON-RPC
  2. 5.2HTTP+JSON
  3. 5.3gRPC
  4. 5.4A2A-Version and A2A-Extensions
  5. 5.5Errors
5.1

JSON-RPC

JSON-RPC posts every operation to one URL, names it in method, carries the request message in params, and returns the response message or an error in a 200 reply.

The planner asks code-reviewer a question before it sends any diff: "What languages can you review?". The card lists http://localhost:41242/a2a/jsonrpc with the binding JSONRPC, so the planner sends one POST to that URL. The reviewer answers with a message and no task, and the record is 02-message-reply.http.

When you finish this section, you can write any A2A call as a JSON-RPC request and read its reply, its stream frames, and its errors.

The envelope

Protocol binding: the form the eleven operations take on one transport, with its names, routes, envelopes, headers, and error shapes. Three are standard: JSON-RPC spec §9, gRPC spec §10, and HTTP+JSON spec §11, and a card names each one in protocolBinding proto AgentInterface. The Core Concepts guide still says "JSON-RPC 2.0 is used as the payload format for all requests and responses" docs key-concepts, which describes one binding of three (conflict D27).

A JSON-RPC request is one JSON object with four members spec §9.3. jsonrpc is always 2.0, the client chooses id, method holds the operation name, and params holds the request message. The names are "PascalCase method names matching gRPC conventions" spec §9.1, so method holds SendMessage or GetTask, the rpc names of A2AService proto A2AService. The template in §9.3 still shows the 0.3 style, "method": "category/action" spec §9.3, so copy the names from §9.1 or from operations by binding (conflict D14). The kit answers the old name message/send with -32601, as exchange 3 of 11-errors.http shows.

The reply repeats the request's id and holds the response message in result spec §9.4.1. For SendMessage that message is a SendMessageResponse with one task or one message proto SendMessageResponse:

a question and its direct answer over JSON-RPCcapture/out/02-message-reply.httphttp
POST /a2a/jsonrpc HTTP/1.1
Content-Type: application/json
…
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "d95bafc8-f2a4-427b-9cf4-bb99f4bea973",
      "role": "ROLE_USER",
…
HTTP/1.1 200 OK
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "message": {
      "messageId": "3b4b1206-4b6f-467f-b735-2d7e4f7f4bcd",
      "contextId": "dc423c62-d5f4-48e5-82c0-4963b9bc4334",
      "role": "ROLE_AGENT",
…
The Host and A2A-Version headers and the parts of both messages are cut. Send message explains when the result is a message and when it is a task.

One URL, one content type

Every call is a POST to the URL of the chosen interface, because the binding is "JSON-RPC 2.0 over HTTP(S)" spec §9.1. The path carries no operation name, and the kit serves all eleven operations at /a2a/jsonrpc. Both the request and the reply use application/json spec §9.1, where HTTP+JSON uses its own type, as HTTP+JSON explains.

The headers beside the body are the two service parameters, A2A-Version and A2A-Extensions, which "MUST be transmitted using standard HTTP request headers" in this binding spec §9.2. A2A-Version and A2A-Extensions gives their rules and what each SDK does without them.

Streams as SSE frames

SendStreamingMessage and SubscribeToTask answer with Content-Type: text/event-stream instead of one JSON body spec §9.4.2. Each data: line is a complete JSON-RPC response: the same id as the request, and one StreamResponse in result spec §9.4.2 proto StreamResponse. So one parser reads both the single reply and every frame.

the first two frames of a stream, each a full JSON-RPC responsecapture/out/05-streaming.httpsse
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"jsonrpc": "2.0", "id": 1, "result": {"task": {"id": "728ba084-97eb-422b-b94b-b0fe9153ce2c", …

data: {"jsonrpc": "2.0", "id": 1, "result": {"statusUpdate": {"taskId": "728ba084-97eb-422b-b94b-b0fe9153ce2c", …
The request and the inside of each frame are cut. The file holds all nine frames, and 05-streaming.events.json prints them in full.

An HTTP+JSON frame carries the StreamResponse alone, and gRPC returns a server-streaming rpc, as HTTP+JSON and gRPC show. In every binding the frames carry the same four payloads and end for the same reasons, which streaming and subscribing covers.

Errors in the same envelope

A failed call is still a JSON-RPC response: an error member with code, message, and data replaces result spec §9.5. The specification sets no HTTP status for that case, and the kit and the reference SDK send 200 sdk src/a2a/server/routes/jsonrpc_dispatcher.py. The A2A errors take the codes -32001 to -32009, and the detail that names each one is in data. Errors reads them in every binding.

Sources:spec §9, §9.1, §9.2, §9.3, §9.4.1, §9.4.2, §9.5, §10, §11 (research/sources/specification.md); proto AgentInterface, A2AService, SendMessageResponse, StreamResponse (research/sources/a2a.proto); docs key-concepts (research/sources/docs.md); sdk src/a2a/server/routes/jsonrpc_dispatcher.py (a2a-python 1.2.2); capture/out/02-message-reply.http, 05-streaming.http, 05-streaming.events.json, 11-errors.http; capture/a2a_ref.py

5.2

HTTP+JSON

HTTP+JSON names each operation with a verb and a path from the proto's google.api.http annotations, and sends the request and response messages as plain JSON bodies.

The planner repeats its first test run over the second interface of test-runner, http://localhost:41241/a2a/rest with the binding HTTP+JSON. The same agent and task store answer, in 16-rest-binding.http, and only the envelope differs from 03-blocking-task.http.

When you finish this section, you can map any operation to its route, set its content type, and send a tenant when the card asks.

Routes from the proto

Every rpc of A2AService carries an option (google.api.http) with a verb and a path template proto A2AService, and §11.3 lists the same routes spec §11.3. An operation with a request body is a POST with body: "*": /message:send, /message:stream, /tasks/{id}:cancel, and /tasks/{task_id}/pushNotificationConfigs. Reads are GET, and the configuration delete is DELETE. Operations by binding gives all eleven routes with their messages.

For subscribe the proto says get: "/tasks/{id=*}:subscribe" proto A2AService, and §5.3 and §11.3.2 say POST /tasks/{id}:subscribe spec §5.3 spec §11.3.2 (conflict D1). The reference SDK's server accepts both verbs, and its client sends POST sdk src/a2a/server/routes/rest_routes.py sdk src/a2a/client/transports/rest.py. The Java and .NET servers accept only POST sdk-java reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java at v1.4.0.Final sdk-dotnet src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs at v1.0.0-preview2. So accept both on a server, and send POST from a client.

SendMessage over HTTP+JSON, with no envelopecapture/out/16-rest-binding.httphttp
POST /a2a/rest/message:send HTTP/1.1
A2A-Version: 1.0
Content-Type: application/a2a+json

{
  "message": {
    "messageId": "48f165d5-7b00-47f4-b81e-f86f5c8cc1ab",
    "role": "ROLE_USER",
…
HTTP/1.1 200 OK
Content-Type: application/a2a+json

{
  "task": {
    "id": "bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3",
    "contextId": "41c0d462-fed8-4f6b-8569-6c9afe9497c3",
…
Host and the rest of both bodies are cut. Over JSON-RPC the same reply sits under result, in 03-blocking-task.http.

Query parameters and content types

GET and DELETE carry no body, so their request fields travel "as path parameters or query parameters" spec §11.5, in camelCase. GetTask puts the task id in the path and historyLength in the query. ListTasks takes contextId, status, pageSize, pageToken, historyLength, statusTimestampAfter, and includeArtifacts proto ListTasksRequest spec §11.5. A nested object cannot go in a query string, so that request must be a POST spec §11.5. Figure 5.1 sets both operations beside their JSON-RPC form.

Fig. 5.1SendMessage and GetTask in two JSON bindingscomparison
One operation in two JSON bindings Two rows, SendMessage and GetTask, each headed by its gRPC rpc line from a2a.proto. In each row the left column shows the JSON-RPC request, a POST to /a2a/jsonrpc whose body names the method and carries the request message in params, and a reply whose result holds the response message. The right column shows HTTP+JSON, where the verb and path name the operation, the body or the path and query carry the request, and the reply body is the response message itself. The steps appear in capture order: each request, then its reply. A · JSON-RPC B · HTTP+JSON gRPC rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) gRPC rpc GetTask(GetTaskRequest) returns (Task) POST /a2a/jsonrpc Content-Type: application/json "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": SendMessageRequest HTTP/1.1 200 OK "jsonrpc": "2.0", "id": 1, "result": SendMessageResponse POST /a2a/rest/message:send Content-Type: application/a2a+json body: SendMessageRequest HTTP/1.1 200 OK Content-Type: application/a2a+json body: SendMessageResponse POST /a2a/jsonrpc "jsonrpc": "2.0", "id": 3, "method": "GetTask", "params": {"id": "d0c28826", "historyLength": 0} HTTP/1.1 200 OK "jsonrpc": "2.0", "id": 3, "result": Task GET /a2a/rest/tasks/bbfb9ea5 ?historyLength=0 no body: the path and query carry the request HTTP/1.1 200 OK Content-Type: application/a2a+json body: Task
JSON-RPC posts every operation to one URL and names it in the body, HTTP+JSON names it with a verb and a path, and gRPC calls an rpc. Each row is one operation, headed by its rpc line from a2a.proto. Bold marks where each binding names the operation, and italic type names stand for the message that fills that place. Left, capture/out/03-blocking-task.http and 04-polling.http. Right, capture/out/16-rest-binding.http. Ids shortened to 8 characters.

"application/a2a+json SHOULD be used for requests and responses" spec §11.1, and the kit sends it on every body. The reference SDK sends its errors as application/json sdk src/a2a/utils/error_handlers.py, and the Java and Go servers send every body that way sdk-java transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java at v1.4.0.Final sdk-go a2asrv/rest.go at v2.6.0. So send application/a2a+json, and accept both types when you read.

Streams and errors

A stream is the same text/event-stream response as in JSON-RPC spec §11.7. Each data: line holds the StreamResponse alone, with no envelope. No capture records a stream in this binding, so streaming and subscribing shows the JSON-RPC frames.

An error uses the HTTP status code and the JSON form of google.rpc.Status in an error object spec §11.6. Exchanges 3 and 4 of the record show two, and Errors reads that body.

The tenant field

Tenant: an optional string on an interface, "An opaque string used for routing requests to a specific agent" proto AgentInterface or to one tenant when several agents share one endpoint. The same tenant field sits on all ten request messages, from SendMessageRequest to GetExtendedAgentCardRequest proto SendMessageRequest proto GetExtendedAgentCardRequest. A client copies the card's value into every request, and must "omit the field if tenant is not set in that entry" spec §8.3.2. The protocol gives the value no format, and no kit card sets one.

Here the value can also travel in the path. Every rpc has a second google.api.http form with a /{tenant} prefix, such as post: "/{tenant}/message:send" proto A2AService. Only the proto lists these routes, and §5.3, §11.3, and §11.5 show the plain ones spec §11.3. In JSON-RPC the value is params.tenant, and in gRPC it is the message field. The migration guide calls this "Native tenant scoping in gRPC requests" docs whats-new-v1, which understates it.

The project's guide names three routing keys for several agents on one host: a URL prefix, the credential, and the tenant field docs multi-tenancy. "The three approaches are not mutually exclusive." docs multi-tenancy

The reference SDK's client wraps the transport in a TenantTransportDecorator when the chosen interface sets a tenant sdk src/a2a/client/client_factory.py sdk src/a2a/client/transports/tenant_decorator.py. Its HTTP+JSON transport puts the value in the path prefix sdk src/a2a/client/transports/rest.py. Its server mounts every route again under /{tenant}, copies the value into the call context, and never compares it with the card sdk src/a2a/server/routes/rest_routes.py sdk src/a2a/server/routes/rest_dispatcher.py. The agent reads RequestContext.tenant and decides what it means sdk src/a2a/server/agent_execution/context.py.

Sources:spec §5.3, §8.3.2, §11.1, §11.3, §11.3.2, §11.5, §11.6, §11.7 (research/sources/specification.md); proto A2AService, AgentInterface, SendMessageRequest, GetTaskRequest, ListTasksRequest, GetExtendedAgentCardRequest (research/sources/a2a.proto); docs multi-tenancy, whats-new-v1 (research/sources/docs.md); sdk src/a2a/server/routes/rest_routes.py, src/a2a/server/routes/rest_dispatcher.py, src/a2a/server/agent_execution/context.py, src/a2a/client/client_factory.py, src/a2a/client/transports/tenant_decorator.py, src/a2a/client/transports/rest.py, src/a2a/utils/error_handlers.py (a2a-python 1.2.2); sdk-java reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java, transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java at v1.4.0.Final; sdk-dotnet src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs at v1.0.0-preview2; sdk-go a2asrv/rest.go at v2.6.0; capture/out/16-rest-binding.http, 03-blocking-task.http, 04-polling.http; capture/a2a_ref.py

5.3

gRPC

The gRPC binding is the proto itself: the eleven rpcs of A2AService in package lf.a2a.v1, carried over HTTP/2 with TLS, with the service parameters in metadata.

The planner reads three cards and finds only JSONRPC and HTTP+JSON interfaces, so no call in 19-planner.http uses gRPC. The kit is standard library only, and the standard library has no gRPC, so no capture in this manual shows a gRPC call. The reference SDK's sample agent lists a third interface, protocol_binding='GRPC' at a plain host and port sdk samples/hello_world_agent.py.

When you finish this section, you can read a GRPC interface, call A2AService from a generated stub, and read its errors and streams.

The service

The binding has four requirements: "gRPC over HTTP/2 with TLS", the normative definition in specification/a2a.proto, "Protocol Buffers version 3", and the A2AService service spec §10.1. The file declares package lf.a2a.v1, so the full service name is lf.a2a.v1.A2AService proto A2AService. Its eleven rpcs carry the same PascalCase names as the JSON-RPC methods, and operations by binding lists them with their messages.

Two rpcs return stream StreamResponse: SendStreamingMessage and SubscribeToTask proto A2AService spec §10.7. The other nine are unary, and none takes a client stream. Each rpc also carries the google.api.http annotation that defines its HTTP+JSON route, as HTTP+JSON shows:

the streaming send, as the proto defines itresearch/sources/a2a.prototext
  // Sends a streaming message to an agent, allowing for real-time interaction and status updates.
  // Streaming version of `SendMessage`
  rpc SendStreamingMessage(SendMessageRequest) returns (stream StreamResponse) {
    option (google.api.http) = {
      post: "/message:stream"
      body: "*"
      additional_bindings: {
        post: "/{tenant}/message:stream"
        body: "*"
      }
    };
  }
The rpc with its HTTP annotation, complete. The additional_bindings block is the tenant route.

A card names this binding with protocolBinding set to GRPC proto AgentInterface. At the tag the proto wants an absolute HTTPS URL for every interface, while a gRPC client dials a host and port (conflict D34). AgentCard fields notes the same gap. PR #1997 changed the comment on main to the form hostname:port, the one change queued for release 1.0.2, not yet released.

Metadata, errors, and streams

The two service parameters "MUST be transmitted using gRPC metadata (headers)" spec §10.2. gRPC lowercases metadata keys, so the keys are a2a-version and a2a-extensions, and servers "SHOULD validate required service parameters (e.g., A2A-Version) from metadata" spec §10.2. The §10.2 sample sends a2a-version with the old example value 0.3 spec §10.2, so send 1.0 from a 1.0 client, as A2A-Version and A2A-Extensions explains.

An error is a google.rpc.Status with status.code, status.message, and status.details spec §10.6. For an A2A error the details "MUST include a google.rpc.ErrorInfo message" spec §10.6. Its reason is the error name in upper snake case without Error, such as TASK_NOT_FOUND, and its domain is a2a-protocol.org spec §10.6. The table in errors gives the status of each error, and FAILED_PRECONDITION covers six of the nine, so read the reason.

A stream is a server-streaming rpc, and each message is one StreamResponse with one of task, message, status_update, or artifact_update spec §10.7 proto StreamResponse. The stream ends when the rpc completes with its status, and the Go server returns OK when its event sequence ends sdk-go a2agrpc/v1/handler.go at v2.6.0. Figure 5.2 follows one streaming call from the request to that end.

Fig. 5.2one SendStreamingMessage call over gRPCsequence
One SendStreamingMessage call over gRPC A sequence with two lifelines, a client stub and the A2AService in package lf.a2a.v1. The client sends one SendStreamingMessage rpc with a SendMessageRequest and the a2a-version and a2a-extensions metadata keys. The server answers with a stream of StreamResponse messages: first the task, then status_update and artifact_update payloads, then a status_update with a terminal state. The rpc status ends the stream. The figure uses proto names only, because the kit has no gRPC capture. client generated stub A2AService lf.a2a.v1 1 SendStreamingMessage(SendMessageRequest) metadata: a2a-version, a2a-extensions 2 StreamResponse { task } the Task as the server created it 3 StreamResponse { status_update } TaskStatusUpdateEvent 4 StreamResponse { artifact_update } TaskArtifactUpdateEvent, one chunk 5 StreamResponse { status_update } a terminal TaskState 6 rpc status the stream ends with the rpc
One rpc sends the request with its metadata, the server streams StreamResponse messages until the task ends, and the rpc status closes the stream. Read top to bottom. The client sends one SendMessageRequest with a2a-version in its metadata, and each reply is one StreamResponse payload. Names are the proto's, because the kit has no gRPC capture, so no values are shown. From research/sources/a2a.proto.

SDK support

SDKgRPC servergRPC clientHow to enable it
a2a-python 1.2.2GrpcHandler sdk src/a2a/server/request_handlers/grpc_handler.pyGrpcTransport sdk src/a2a/client/transports/grpc.pypip install a2a-sdk[grpc], register the handler as the servicer, and list GRPC in the client factory sdk src/a2a/client/client_factory.py
a2a-js 1.3.0grpcService, Node only sdk-js src/server/grpc/grpc_service.ts at v1.3.0GrpcTransportadd GrpcTransportFactory, which the default factory omits sdk-js src/client/factory.ts at v1.3.0
a2a-go 2.6.0a2agrpc/v1.NewHandler sdk-go a2agrpc/v1/handler.go at v2.6.0a2agrpc/v1.WithGRPCTransportadd the transport option, which the default client omits sdk-go a2aclient/factory.go at v2.6.0
a2a-java 1.4.0.Finaltransport/grpc and the Quarkus reference/grpc serverclient/transport/grpcadd the Maven modules sdk-java README.md at v1.4.0.Final
a2a-dotnet 1.0.0-preview2none, only the name constant sdk-dotnet src/A2A/Client/ProtocolBindingNames.cs at v1.0.0-preview2noneA2A.Grpc.AspNetCore and A2A.Grpc exist on main, not yet released
a2a-rs, a2a-grpc 0.3.9GrpcHandler in the a2a-grpc crateGrpcTransport in the same crateregister it yourself, because the client factory lists JSON-RPC and HTTP+JSON only sdk-rust a2a-client/src/factory.rs at a2a-server-lf-v0.5.1

The servers differ on the version check: the Python, Go, and Rust gRPC servers never read a2a-version, as the table in A2A-Version and A2A-Extensions shows.

One custom binding exists in the project today, SLIMRPC: Protocol Buffers RPC over SLIM instead of HTTP/2. A card names it https://a2a-protocol.org/bindings/experimental-slimrpc/v1, and its repository marks it experimental. A custom binding "SHOULD be identified by a URI" spec §5.8, and the project's guide gives the rules for one docs custom-protocol-bindings.

Sources:spec §5.8, §10, §10.1, §10.2, §10.6, §10.7 (research/sources/specification.md); proto A2AService, AgentInterface, StreamResponse (research/sources/a2a.proto); docs custom-protocol-bindings (research/sources/docs.md); sdk samples/hello_world_agent.py, src/a2a/server/request_handlers/grpc_handler.py, src/a2a/client/transports/grpc.py, src/a2a/client/client_factory.py (a2a-python 1.2.2); sdk-go a2agrpc/v1/handler.go, a2aclient/factory.go at v2.6.0; sdk-js src/server/grpc/grpc_service.ts, src/client/factory.ts at v1.3.0; sdk-java README.md at v1.4.0.Final; sdk-dotnet src/A2A/Client/ProtocolBindingNames.cs at v1.0.0-preview2; sdk-rust a2a-client/src/factory.rs at a2a-server-lf-v0.5.1; capture/out/01-agent-cards.http, 19-planner.http; capture/README.md

5.4

A2A-Version and A2A-Extensions

Two service parameters travel as HTTP headers or gRPC metadata on every request, and a missing A2A-Version means 0.3, which a 1.0 agent must refuse.

The kit sends test-runner a SendMessage with only Host and Content-Type headers, as a client that forgot the version would. The agent answers -32009 and reports the request as 0.3 (no header), in exchange 2 of 11-errors.http. Every other request in the captures carries A2A-Version: 1.0.

When you finish this section, you can set both headers in any binding and predict what each SDK does without a version.

Service parameters

Service parameter: "A key-value map for passing horizontally applicable context or parameters with case-insensitive string keys and case-sensitive string values." spec §3.2.6 The specification defines two, A2A-Version and A2A-Extensions, and reserves the prefix a2a- for its own spec §3.2.6. Each binding says how they travel: as HTTP request headers in JSON-RPC and HTTP+JSON spec §9.2 spec §11.2, and as metadata keys in gRPC spec §10.2. A custom binding "MUST document how service parameters are transmitted" spec §12.3.

The IANA templates for both headers cite "Section 3.2.5 of the A2A Protocol Specification" spec §14.2.1 spec §14.2.2, which is the metadata section, where the parameters are §3.2.6 (conflict D17). Issue #2305 tracks it, still open.

A2A-Version

A version is Major.Minor, such as 1.0, and "Patch version numbers SHOULD NOT be used in requests, responses and Agent Cards" spec §3.6. "Clients MUST send the A2A-Version header with each request" spec §3.6.1, and a 1.0 client sends 1.0. A server has two duties: process the request with the semantics of that version, and return VersionNotSupportedError when the interface does not support it spec §3.6.2. The empty case has a fixed meaning: "Agents MUST interpret empty value as 0.3 version" spec §3.6.2. So a client that forgets the header gets 0.3 semantics from an agent that still serves 0.3, and this error from one that does not:

no A2A-Version header, read as 0.3 and refusedcapture/out/11-errors.httphttp
POST /a2a/jsonrpc HTTP/1.1
Host: localhost:41241
Content-Type: application/json
…
HTTP/1.1 200 OK
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32009,
    "message": "Protocol version not supported",
…
          "requested": "0.3 (no header)",
          "supported": "1.0"
The request body and the error details are cut down. The request carries only Host and Content-Type, and the kit speaks only 1.0.

VersionNotSupportedError is -32009 in JSON-RPC, 400 in HTTP+JSON, and FAILED_PRECONDITION in gRPC, with the reason VERSION_NOT_SUPPORTED spec §5.4, as errors lists. The version to send comes from the card: each supportedInterfaces entry declares its own protocolVersion, and "Agents CAN expose multiple interfaces for the same transport with different versions" spec §3.6.2, as AgentCard fields shows.

Three samples in the specification send A2A-Version: 0.3 spec §9.2 spec §11.2 spec §14.2.1, and the gRPC sample in §10.2 sends the same value as metadata spec §10.2. Never copy a sample's value into a 1.0 client (conflict D39). §3.6.1 also says "Clients MAY provide the A2A-Version as a request parameter instead of a header" spec §3.6.1, while both JSON bindings require the header (conflict D31). The reference SDK reads only the header sdk src/a2a/utils/version_validator.py, and the kit reads both, so send the header.

What each SDK does without the header

ImplementationMissing or empty headerAccepted valuesWhere it checks
kit, a2a_ref.pyrefused as 0.3 (no header)exactly 1.0, from the header or the query parameterJSON-RPC and HTTP+JSON
a2a-python 1.2.2read as 0.3, then refused1, 1.0, 1.0.0, and 1.5, because only the major must be 1 sdk src/a2a/utils/version_validator.pyJSON-RPC and HTTP+JSON, never gRPC sdk src/a2a/server/request_handlers/grpc_handler.py
a2a-js 1.3.0read as 0.3, refused unless the card lists itonly the strings the card declares for that binding, so 1.0.0 is refused sdk-js src/server/version.ts at v1.3.0all three bindings
a2a-java 1.4.0.Finalread as 0.3, then refusedthe same major, in major.minor form, so 1 is refused sdk-java server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java at v1.4.0.Finalall three, with gRPC UNIMPLEMENTED instead of FAILED_PRECONDITION
a2a-go 2.6.0acceptedanything, because no server code reads the header sdk-go a2a/svcparams.go at v2.6.0nowhere
a2a-dotnet 1.0.0-preview2accepted as 1.01.0 or 0.3, with 0.3 processed as 1.0 sdk-dotnet src/A2A.AspNetCore/A2AJsonRpcProcessor.cs at v1.0.0-preview2JSON-RPC only
a2a-rs, a2a-server 0.5.1read as 0.3, then refusedmajor 1 sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1JSON-RPC only

Every client in the list sends 1.0 on each call, and the Python and JS clients send it on the card fetch too. So send the literal string 1.0, never a patch number, and never rely on a server to refuse a missing header.

A2A-Extensions

A2A-Extensions is a "Comma-separated list of extension URIs that the client wants to use for the request" spec §3.2.6, one header field in both JSON bindings spec §9.2 spec §11.2 and one metadata entry in gRPC spec §10.2. §4.6.1 also allows "JSON-RPC request parameters" as the mechanism spec §4.6.1, while §9.2 requires the header, so send the header (conflict D30). The reference SDK's with_a2a_extensions helper joins the URIs with commas on the client sdk src/a2a/client/service_parameters.py, and its server reads them into the call context sdk src/a2a/server/routes/common.py. What the server does with them, the payload keyed by URI, and ExtensionSupportRequiredError are in extensions.

Sources:spec §3.2.6, §3.6, §3.6.1, §3.6.2, §4.6.1, §5.4, §9.2, §10.2, §11.2, §12.3, §14.2.1, §14.2.2 (research/sources/specification.md); sdk src/a2a/utils/version_validator.py, src/a2a/server/request_handlers/grpc_handler.py, src/a2a/client/service_parameters.py, src/a2a/server/routes/common.py (a2a-python 1.2.2); sdk-js src/server/version.ts at v1.3.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java at v1.4.0.Final; sdk-go a2a/svcparams.go at v2.6.0; sdk-dotnet src/A2A.AspNetCore/A2AJsonRpcProcessor.cs at v1.0.0-preview2; sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1; capture/out/11-errors.http, 01-agent-cards.http; capture/a2a_ref.py

5.5

Errors

Nine A2A errors map into three bindings, and because their status codes collide, the ErrorInfo reason in the details is what tells them apart.

The planner asks test-runner for a task id that does not exist, once over each JSON binding. JSON-RPC answers HTTP/1.1 200 OK with code -32001, and HTTP+JSON answers HTTP/1.1 404 Not Found. Both carry the same ErrorInfo, with the reason TASK_NOT_FOUND, as figure 5.3 shows.

When you finish this section, you can read an error in any binding, name the A2A error behind it, and choose what to do next.

Fig. 5.3TaskNotFoundError in two JSON bindingscomparison
One error in two JSON bindings Two columns. Left, a JSON-RPC GetTask for task 00000000 posted to /a2a/jsonrpc gets HTTP 200 OK with an error member: code -32001, message Task not found, and a data array. Right, the same lookup as GET /a2a/rest/tasks/00000000 gets HTTP 404 Not Found with a google.rpc.Status body: code 404, status NOT_FOUND, the same message, and a details array. Lines from data and details lead to one shared box, the google.rpc.ErrorInfo with reason TASK_NOT_FOUND, domain a2a-protocol.org, and the task id in metadata. A · JSON-RPC B · HTTP+JSON POST /a2a/jsonrpc A2A-Version: 1.0 "jsonrpc": "2.0", "id": 1, "method": "GetTask", "params": {"id": "00000000"} HTTP/1.1 200 OK Content-Type: application/json "jsonrpc": "2.0", "id": 1, "error": { "code": -32001, "message": "Task not found", "data": [ ErrorInfo ] GET /a2a/rest/tasks/00000000 A2A-Version: 1.0 no body: the id is in the path HTTP/1.1 404 Not Found Content-Type: application/a2a+json "error": { "code": 404, "status": "NOT_FOUND", "message": "Task not found", "details": [ ErrorInfo ] data[0] details[0] google.rpc.ErrorInfo, the same object in both bindings "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "TASK_NOT_FOUND", "domain": "a2a-protocol.org", "metadata": {"taskId": "00000000"}
The same TaskNotFoundError arrives as HTTP 200 with code -32001 in JSON-RPC and as HTTP 404 in HTTP+JSON, carrying the same ErrorInfo. Left, GetTask over JSON-RPC from capture/out/11-errors.http. Right, the same lookup over HTTP+JSON from capture/out/16-rest-binding.http. The box at the bottom is the detail both replies carry. Ids shortened to 8 characters.

The detail that names the error

The specification defines nine A2A errors and maps each one into every binding spec §3.3.2 spec §5.4. JSON-RPC numbers them -32001 to -32009 spec §9.5, HTTP answers 400 for seven, and gRPC answers FAILED_PRECONDITION for six, as the error table lists.

Every error carries a code, a message, and optional details, each detail naming its type in @type spec §3.3.2. For A2A errors the deciding detail is a google.rpc.ErrorInfo:

Its reason is the error's name in upper snake case without the Error suffix, such as TASK_NOT_FOUND, and its domain is a2a-protocol.org spec §11.6. The gRPC binding carries the same MUST spec §10.6, and JSON-RPC only a SHOULD spec §9.5. The metadata is free-form: the kit fills it, and the reference SDK leaves it empty, as the kit README lists.

JSON-RPC errors arrive with HTTP 200

A JSON-RPC error is an ordinary response with HTTP 200. Its error member holds code, message, and data where result would be spec §9.5. The five standard JSON-RPC codes, -32700 and -32600 to -32603, keep their meaning spec §9.5.

Validation errors carry a different detail type, a google.rpc.BadRequest that names the field:

a message without messageId, refused with a BadRequest detailcapture/out/11-errors.httphttp
    "message": {
      "role": "ROLE_USER",
…
HTTP/1.1 200 OK
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 4,
  "error": {
    "code": -32602,
    "message": "Invalid parameters",
    "data": [
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "message.messageId",
            "description": "messageId is required"
The request is cut to the start of its message, which has no messageId.

Do not match on message texts: the reference SDK answers the same case with "Validation failed" and names the field message.message_id sdk src/a2a/utils/proto_utils.py.

Authentication can fail before JSON-RPC runs, with HTTP 401 as the specification's example spec §3.3.2, so read the status first, as Security schemes shows.

HTTP+JSON errors are google.rpc.Status

An HTTP+JSON error uses the HTTP status and a JSON google.rpc.Status spec §11.6. Its error object repeats the status in code, names the gRPC status in status, and adds message and details:

a cancel of a completed task over HTTP+JSONcapture/out/16-rest-binding.httphttp
POST /a2a/rest/tasks/bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3:cancel HTTP/1.1
…
HTTP/1.1 400 Bad Request
Content-Type: application/a2a+json

{
  "error": {
    "code": 400,
    "status": "FAILED_PRECONDITION",
    "message": "Task cannot be canceled",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "TASK_NOT_CANCELABLE",
        "domain": "a2a-protocol.org",
        "metadata": {
          "taskId": "bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3",
          "state": "TASK_STATE_COMPLETED"
The request headers and body are cut. Over JSON-RPC this error is code -32002, as in 10-cancel.http.

Two samples in the specification still show application/problem+json bodies in the older RFC 9457 style spec §6.4 spec §6.5 (conflict D13). PR #1641 and PR #1689 replace those samples, and both are still open. The migration guide gives the type as application/json docs whats-new-v1, where §11.6 shows application/a2a+json. The SDKs send all three, and all answer JSON-RPC errors with HTTP 200:

SDKJSON-RPC errorsHTTP+JSON errors
a2a-python 1.2.2application/json sdk src/a2a/server/routes/jsonrpc_dispatcher.pyapplication/json sdk src/a2a/utils/error_handlers.py
a2a-goapplication/json sdk-go a2asrv/jsonrpc.go at v2.6.0application/json sdk-go a2asrv/rest.go at v2.6.0
a2a-javaapplication/json sdk-java reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java at v1.4.0.Finalapplication/json sdk-java transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java at v1.4.0.Final
a2a-jsapplication/json sdk-js src/server/express/json_rpc_handler.ts at v1.3.0application/a2a+json sdk-js src/server/express/rest_handler.ts at v1.3.0
a2a-dotnetapplication/json sdk-dotnet src/A2A.AspNetCore/JsonRpcResponseResult.cs at v1.0.0-preview2application/problem+json, a problem-details body sdk-dotnet src/A2A.AspNetCore/A2AHttpProcessor.cs at v1.0.0-preview2
a2a-rsapplication/json sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1application/problem+json sdk-rust a2a-server/src/rest.rs at a2a-server-lf-v0.5.1

Send application/a2a+json, and accept all three types when you read. Parse the body as google.rpc.Status whatever the type says. The exception is a2a-dotnet at this release, whose problem-details body carries no ErrorInfo. The gRPC binding puts the same three parts in status.code, status.message, and status.details spec §10.6.

What to retry

UnsupportedOperationError covers three problems with one code, -32004, and one reason, UNSUPPORTED_OPERATION spec §3.3.4 spec §3.1.1 spec §3.1.6. The card lacks streaming or the extended card, or the task is terminal, as in 10-cancel.http. Only message and metadata tell the cases apart, and the specification fixes neither. So when a task operation gets -32004, read the task with GetTask before you decide.

Most A2A errors describe the request or the task, so resending the same bytes gets the same answer:

  • -32603, HTTP 500 or 503, or a dropped connection: retry with backoff, and honor Retry-After spec §3.3.2.
  • HTTP 401: get a fresh credential, then retry.
  • -32600, -32602, -32005, -32009, and their 400 forms: fix the request before you send it again.
  • -32001: stop, because the id is wrong, purged, or hidden from you spec §3.3.2.
  • -32002, or -32004 on a terminal task: start a new task, as CancelTask and terminal states shows.
  • -32003, -32007, or -32004 from a capability check: read the card again.

A retried SendMessage can start a second task, because duplicate detection by messageId is optional spec §3.3.1. Send the same messageId anyway, as Message explains.

Sources:spec §3.1.1, §3.1.6, §3.3.1, §3.3.2, §3.3.4, §5.4, §6.4, §6.5, §9.5, §10.6, §11.6 (research/sources/specification.md); docs whats-new-v1 (research/sources/docs.md); a2aproject/A2A PR #1641 and PR #1689, checked 2026-10-06; sdk src/a2a/utils/proto_utils.py, src/a2a/utils/error_handlers.py, src/a2a/server/routes/jsonrpc_dispatcher.py (a2a-python 1.2.2); sdk-go a2asrv/jsonrpc.go, a2asrv/rest.go (a2a-go v2.6.0); sdk-java reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java, transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java (a2a-java v1.4.0.Final); sdk-js src/server/express/json_rpc_handler.ts, src/server/express/rest_handler.ts (a2a-js v1.3.0); sdk-dotnet src/A2A.AspNetCore/JsonRpcResponseResult.cs, src/A2A.AspNetCore/A2AHttpProcessor.cs (a2a-dotnet v1.0.0-preview2); sdk-rust a2a-server/src/jsonrpc.rs, a2a-server/src/rest.rs (a2a-rs a2a-server-lf-v0.5.1); capture/out/11-errors.http, 16-rest-binding.http, 10-cancel.http, 14-resubscribe.http; capture/README.md

Part 6

Security and Extensions

The card declares how to authenticate and which extensions apply, and the specification lists what each side must check.

  1. 6.1Security schemes and in-task authorization
  2. 6.2Extensions
  3. 6.3Security requirements
6.1

Security schemes and in-task authorization

The card declares which credentials an agent accepts, HTTP checks them on every request, and a running task can still stop to ask for an approval.

The planner's last delegation goes to deployer, the only agent in the kit that guards its endpoint. A request without a token gets HTTP 401 before any A2A method runs. A request with the token starts a deploy that stops again, in TASK_STATE_AUTH_REQUIRED, until an operator approves it.

Figure 6.1 follows that credential from the card to the approval. When you finish this section, you can read a card's security fields, send what they ask for, and scope tasks to their owner.

Fig. 6.1one credential, from the card to an in-task approvalflow
From the card to an enforced request and an in-task approval Five steps for the deployer agent. Step one: the card declares a bearer scheme and requires it, and the platform team issues a token outside A2A. Step two: GetExtendedAgentCard without a token gets HTTP 401 with a WWW-Authenticate challenge. Step three: the same call with the token gets the extended card with a rollback skill. Step four: a streamed deploy with the token stops the task in TASK_STATE_AUTH_REQUIRED. Step five: an operator approves at the deployer outside A2A and the task works and completes on the same stream. 1 · THE CARD DECLARES, THE CLIENT OBTAINS 2 · HTTP CHECKS BEFORE ANY A2A METHOD RUNS 3 · THE TASK ASKS FOR MORE, OUTSIDE A2A deployer card securitySchemes: bearer httpAuthSecurityScheme, Bearer securityRequirements: bearer platform team issues the token dpl_test_7c1e4b outside A2A, before any request GetExtendedAgentCard no Authorization header HTTP 401 Unauthorized WWW-Authenticate: Bearer realm="deployer" refused GetExtendedAgentCard Authorization: Bearer dpl_test_7c1e4b 200 OK, extended card one more skill: rollback shown only to authenticated callers accepted SendStreamingMessage Deploy build 2026.10.06-1 to staging, with the token TASK_STATE_AUTH_REQUIRED task ecf2dcb6 waits, stream open an operator must approve deploy operator approves POST /approve/ecf2dcb6 at the deployer, not in A2A TASK_STATE_COMPLETED after TASK_STATE_WORKING on the same stream the approval link resumes
The card names the scheme, HTTP rejects a request without the token before A2A runs, and a running task can still stop for an approval. Read top to bottom, in five steps. From step 2 on, the left box is what the planner sends and the right box is what deployer answers. Grey boxes sit outside A2A. The task id is shortened to 8 characters. From capture/out/01-agent-cards.http, 18-extended-card.http, and 07-auth-required.http.

What the card declares

Only the deployer's card carries security fields, so test-runner and code-reviewer accept any caller.

the security fields on the deployer cardcapture/out/01-agent-cards.httpjson
  "securitySchemes": {
    "bearer": {
      "httpAuthSecurityScheme": {
        "description": "A token issued by the platform team.",
        "scheme": "Bearer"
      }
    }
  },
  "securityRequirements": [
    {
      "schemes": {
        "bearer": {
          "list": []
        }
      }
    }
  ],
Cut from the deployer card. The two fields sit between capabilities and defaultInputModes.

Security scheme: one entry in the card's securitySchemes map. The key, here bearer, is a name the agent picks. The value holds exactly one of five variants: apiKeySecurityScheme, httpAuthSecurityScheme, oauth2SecurityScheme, openIdConnectSecurityScheme, or mtlsSecurityScheme proto SecurityScheme.

Security requirement: one entry in securityRequirements. Its schemes map names schemes from securitySchemes, and each name maps to "the required scopes" proto SecurityRequirement in a StringList whose only field is list. The deployer names bearer with an empty list, so the token alone is enough.

The §8.5 sample card still says security where the proto says securityRequirements spec §8.5 (conflict D6). The migration guide calls the implicit and password flows removed docs whats-new-v1, while the proto keeps both as deprecated proto OAuthFlows (conflict D11), so offer neither.

How credentials travel

The client obtains the credential outside A2A and sends it "in protocol-appropriate headers or metadata for every A2A request" spec §7.3. That header carries a secret, and "Production deployments MUST use encrypted communication (HTTPS for HTTP-based bindings, TLS for gRPC)" spec §7.1. The specification recommends TLS 1.3 or later spec §7.1, and the enterprise guide TLS 1.2 or later docs enterprise-ready (conflict D18).

Without it, deployer answers HTTP 401 with a WWW-Authenticate challenge before any A2A method runs, as extended cards and signatures shows.

Authorization scope: a task you cannot see does not exist

"Authorization logic is implementation-specific" spec §7.5. The specification fixes how a refusal looks: "Servers MUST NOT reveal the existence of resources the client is not authorized to access" spec §3.3.2. So another caller's task gets the same TaskNotFoundError as a task that never existed. The same section lists "Attempting to access a task created by another user" spec §3.3.2 as a case for an authorization error, which would confirm that the task exists (conflict D36). Answer with the not-found error.

The check runs first: "Authorization checks MUST occur before any database queries or operations that could leak information" spec §13.1. ListTasks must scope its results to the caller even when the request has no filter spec §13.1. The SDKs differ on who the owner is:

ImplementationOwner scopeAnother caller's task
Kitnone, and the deployer needs one shared bearer tokenreturned
a2a-python 1.2.2task stores keyed by the call context's user_name, and every unauthenticated caller has the empty name sdk src/a2a/server/owner_resolver.py sdk src/a2a/auth/user.pyTaskNotFoundError
a2a-js 1.3.0tenant and owner in every store, with unknown for an anonymous caller sdk-js src/server/owner_resolver.ts at v1.3.0TaskNotFoundError
a2a-java 1.4.0.Finala TaskAuthorizationProvider interface with no implementation in the release, and without one every task operation fails closed sdk-java server-common/src/main/java/org/a2aproject/sdk/server/auth/TaskAuthorizationProvider.java at v1.4.0.FinalTaskNotFoundError
a2a-go 2.6.0the in-memory store records the call's user name, and an empty name makes ListTasks fail with ErrUnauthenticated sdk-go a2asrv/taskstore/inmemory.go at v2.6.0ErrTaskNotFound once a name is set
a2a-dotnet 1.0.0-preview2none, because the store and the handler carry no principal sdk-dotnet src/A2A/Server/ITaskStore.cs at v1.0.0-preview2returned
a2a-rs a2a-server-lf-v0.5.1none in the stores, and an opt-in RequestAuthorizer sees only the headers and the task id sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1returned

Without an authentication middleware that fills in the user, every task is visible to every caller.

In-task authorization

In-task authorization: an agent's request for permission partway through a task, through TASK_STATE_AUTH_REQUIRED spec §7.6, which interrupted states follows frame by frame. The kit's POST /approve/<task id> checks no credentials, so anyone who learns the task id can approve. A real approval channel authenticates the operator and records who approved.

A client that is itself an agent can pass the request up a chain of tasks in TASK_STATE_AUTH_REQUIRED spec §7.6.2. An in-band credential then passes through every agent in the chain spec §7.6.3, so bind it to the agent that asked, "such that only this agent is able to use the credentials" spec §7.6.3.

PR #2081 added §7.6.4 to main on 2026-07-30, after v1.0.1, and no release carries it yet. It says: "Agents MUST NOT treat the TASK_STATE_AUTH_REQUIRED state transition, by itself, as authorization for any particular operation." spec-main §7.6.4 A credential obtained there does not authorize later messages unless the implementation, the issuer, or an extension says so spec-main §7.6.4. The kit keys each approval by task id, so one approval resumes one deploy, and its bearer check runs on every later request.

Sources:spec §3.3.2, §7.1, §7.3, §7.4, §7.5, §7.6, §7.6.2, §7.6.3, §8.5, §13.1 (research/sources/specification.md); spec-main §7.6.4 (research/sources/specification-main.md, main 679ab3a, unreleased); proto SecurityScheme, SecurityRequirement, StringList, OAuthFlows (research/sources/a2a.proto); docs/topics/enterprise-ready.md and docs/whats-new-v1.md at v1.0.1; sdk src/a2a/server/owner_resolver.py, src/a2a/auth/user.py, src/a2a/server/tasks/inmemory_task_store.py at a2a-python 1.2.2; sdk-js src/server/owner_resolver.ts at v1.3.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/auth/TaskAuthorizationProvider.java at v1.4.0.Final; sdk-go a2asrv/taskstore/inmemory.go at v2.6.0; sdk-dotnet src/A2A/Server/ITaskStore.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; a2aproject/A2A PR #2081 (unreleased, on main); capture/out/01-agent-cards.http, 07-auth-required.http, 18-extended-card.http; capture/planner.py, capture/a2a_ref.py, capture/README.md

6.2

Extensions

An extension is a URI that a card declares, a client activates per request with A2A-Extensions, and both sides carry as metadata keyed by that URI.

The three cards in 01-agent-cards.http declare no extension, so the planner sends plain requests. A reviewer that returned a citation with each finding would need data the core protocol lacks, and an extension adds it under a URI. When you finish this section, you can read an extension declaration, activate one on a request, and handle a missing required one.

AgentExtension in the card

Extension: an addition to the protocol, identified by a URI and defined by its own specification docs extensions. An agent declares each one as an AgentExtension in capabilities.extensions proto AgentCapabilities, with four optional fields proto AgentExtension.

  • uri, "The unique URI identifying the extension" proto AgentExtension.
  • description, how this agent uses it.
  • required, true when "the client must understand and comply with the extension's requirements" proto AgentExtension.
  • params, "Extension-specific configuration parameters" proto AgentExtension, as a JSON object.

Section 4.6.1 puts the array on the AgentCard itself spec §4.6.1, and the migration guide reads agentCard.extensions docs whats-new-v1. The proto puts it under capabilities, so read capabilities.extensions.

two optional extensions on the §4.6.1 sample cardresearch/sources/specification.mdjson
  "capabilities": {
    "streaming": false,
    "pushNotifications": false,
    "extensions": [
      {
        "uri": "https://standards.org/extensions/citations/v1",
        "description": "Provides citation formatting and source verification",
        "required": false
      },
      {
        "uri": "https://example.com/extensions/geolocation/v1",
        "description": "Location-based search capabilities",
        "required": false
      }
    ]
  },
The sample card, cut to its capabilities. The same sample sends protocolVersion 0.3 and has a trailing comma (conflict D24).

Activating an extension on a request

The client lists the URIs it wants in the A2A-Extensions service parameter, a "Comma-separated list of extension URIs that the client wants to use for the request" spec §3.2.6. A2A-Version and A2A-Extensions says how that parameter travels in each binding.

Message.extensions lists "The URIs of extensions that are present or contributed to this Message" proto Message, and the payload sits in metadata under the same URI as key. The specification shows that keying by example only spec §4.6.2. Artifact.extensions works the same way proto Artifact.

the §4.6.1 request: the header names the extension, and metadata carries its dataresearch/sources/specification.mdhttp
POST /message:send HTTP/1.1
Host: agent.example.com
Content-Type: application/a2a+json
Authorization: Bearer token
A2A-Extensions: https://example.com/extensions/geolocation/v1,https://standards.org/extensions/citations/v1

{
  "message": {
    "role": "ROLE_USER",
    "parts": [{"text": "Find restaurants near me"}],
    "extensions": ["https://example.com/extensions/geolocation/v1"],
    "metadata": {
      "https://example.com/extensions/geolocation/v1": {
        "latitude": 37.7749,
        "longitude": -122.4194
      }
    }
  }
}
The HTTP sample under the card in §4.6.1. It omits the REQUIRED messageId (conflict D23).

The guide adds a third step: the response "SHOULD include the A2A-Extensions header, listing all extensions that were successfully activated for that request" docs extensions. The specification has no such rule, and the SDKs differ. The a2a-js server echoes the header on all three bindings sdk-js src/server/express/json_rpc_handler.ts at v1.3.0. The a2a-go and a2a-java servers echo it only as gRPC response metadata sdk-go a2agrpc/v1/handler.go at v2.6.0 sdk-java reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/A2AExtensionsInterceptor.java at v1.4.0.Final. The a2a-python, a2a-dotnet, and a2a-rs servers never write it sdk src/a2a/extensions/common.py sdk-dotnet src/A2A/Models/AgentExtension.cs at v1.0.0-preview2 sdk-rust a2a/src/lib.rs at a2a-server-lf-v0.5.1. Figure 6.2 shows the exchange.

Fig. 6.2activating an extension, and the error when a required one is missingsequence
Activating an extension, and the error when a required one is missing A sequence with two lifelines: a client and the Research Assistant Agent from the sample card in section 4.6.1 of the specification. The client fetches the card and reads two extensions under capabilities.extensions: https://standards.org/extensions/citations/v1 and https://example.com/extensions/geolocation/v1. It sends a message with the A2A-Extensions header naming the geolocation extension and with latitude and longitude in metadata under that URI. The agent answers 200 and echoes the header, which the extensions guide recommends and the SDKs do differently. A second message without the header gets ExtensionSupportRequiredError, code -32008, when the card marks the extension required. client A2A client Research Assistant Agent the §4.6.1 sample card 1 GET /.well-known/agent-card.json the client reads capabilities.extensions 2 card: capabilities.extensions citations/v1 and geolocation/v1 3 POST /message:send A2A-Extensions: geolocation/v1 metadata keyed by the URI: latitude, longitude 4 200 OK, A2A-Extensions echoed the guide says SHOULD, and SDKs differ 5 POST /message:send, no A2A-Extensions when the card marks the extension required 6 ExtensionSupportRequiredError -32008, HTTP 400, FAILED_PRECONDITION
The card lists the extension, the request names it in A2A-Extensions and keys its data by the URI, and a missing required extension answers -32008. Read top to bottom. Steps 1 to 4 follow the §4.6.1 sample card and request, and the activation steps of the extensions guide. Steps 5 and 6 show the §3.3.4 rule for a card that marks an extension required, which the sample does not. The kit declares no extension, so no capture is shown. URIs are shortened to their last two path segments.

Required extensions and versions

When a card marks an extension required: true and the client does not declare it, "the agent MUST return ExtensionSupportRequiredError" spec §3.3.4: -32008, HTTP 400, or gRPC FAILED_PRECONDITION spec §5.4, as errors tables. Only a2a-java has the check built in sdk-java server-common/src/main/java/org/a2aproject/sdk/server/extensions/A2AExtensions.java at v1.4.0.Final. The reference SDK only exposes the requested URIs as requested_extensions sdk src/a2a/server/routes/common.py and never raises the error sdk src/a2a/utils/errors.py.

"Extensions SHOULD include version information in their URI identifier" spec §4.6.3, and "A new URI MUST be created for breaking changes to an extension" spec §4.6.3. An agent ignores an unknown version of an optional extension, errors on a required one, and "MUST NOT fall back to a previous version of the extension automatically" spec §4.6.3.

Kinds, governance, and what exists

The extensions guide names four kinds docs extensions. Data-only extensions add structured data to the card. Profile extensions narrow the values that core messages carry. Method extensions add RPC methods. State machine extensions add states, which task and task state rejects (conflict D29).

Two tiers govern extensions in the a2aproject organization docs extension-and-binding-governance: official ones in ext-{name} repositories under the URI prefix https://a2a-protocol.org/extensions/, and experimental ones in experimental-ext-{name}. For SDKs, "Extensions MUST be disabled by default and require explicit opt-in" docs extension-and-binding-governance, and "Extension support is not required for protocol conformance" docs extension-and-binding-governance.

On 2026-10-06 the organization had no ext- repository, so no official extension exists. One experimental extension exists: experimental-ext-oid4vp-auth, for in-task authorization with OpenID for Verifiable Presentations. Its agent moves the task to TASK_STATE_AUTH_REQUIRED and puts an authorization request in metadata under the extension URI, for a wallet to answer. It builds on in-task authorization, and its README allows breaking changes. The four example extensions that the guide lists are samples in a2a-samples, outside the tiers: timestamp, traceability, secure-passport, and agp.

Sources:spec §3.2.6, §3.3.2, §3.3.4, §4.6, §4.6.1, §4.6.2, §4.6.3, §5.4 (research/sources/specification.md); proto AgentCapabilities, AgentExtension, Message, Artifact (research/sources/a2a.proto); docs extensions, whats-new-v1, extension-and-binding-governance (research/sources/docs.md); sdk src/a2a/extensions/common.py, src/a2a/server/routes/common.py, src/a2a/utils/errors.py at a2a-python 1.2.2; sdk-js src/server/express/json_rpc_handler.ts at v1.3.0; sdk-go a2agrpc/v1/handler.go at v2.6.0; sdk-java reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/A2AExtensionsInterceptor.java, server-common/src/main/java/org/a2aproject/sdk/server/extensions/A2AExtensions.java at v1.4.0.Final; sdk-dotnet src/A2A/Models/AgentExtension.cs at v1.0.0-preview2; sdk-rust a2a/src/lib.rs at a2a-server-lf-v0.5.1; a2aproject/experimental-ext-oid4vp-auth at e86356d; a2aproject/a2a-samples at 6603ba3; capture/out/01-agent-cards.http

6.3

Security requirements

Sections 13 and 14 list what a server and a client MUST and SHOULD check, and most of those checks fall on data that another agent sent.

The planner reads review.json from code-reviewer and the test-log.txt chunks from test-runner, registers a webhook at localhost:41250, and sends a bearer token to deployer. Each step crosses a boundary between two parties, and no field in A2A marks what crosses it as safe. Sections 13 and 14 of the specification say what each side must check at those boundaries.

When you finish this section, you can name the rule that applies at each boundary, and say whether the kit and the SDKs meet it. Figure 6.3 maps six boundaries in the kit, with the check at each.

Fig. 6.3trust boundaries in the kit, with a threat and a check at eachlayers
Trust boundaries in the kit Six rows, one per place where data crosses between parties in the kit, shown in the order the planner meets them. Agent card: a spoofed card sends messages elsewhere, so the planner verifies signatures over HTTPS. Parts in replies such as review.json: text written as orders, so the planner validates data parts and decides on typed fields. File and URL parts: bad files, or an agent that fetches any URL it is given, so the agent checks type, size, and URLs. Webhook URL: SSRF, so the agent rejects private hosts. Incoming notifications: forgery, so the planner checks Authorization and the task id. Stored records: secrets kept in clear, so agents redact, scope reads, and set retention. WHERE DATA CROSSES THREAT CHECK the planner checks the remote agent checks agent card /.well-known/agent-card.json spoofed card messages go to its endpoint verify signatures kid deployer-key-1, HTTPS parts in replies review.json, test-log.txt injected orders text that reads as orders validate data parts decide on typed fields file and URL parts refunds.diff, screenshot.png bad files, SSRF the agent fetches any URL check type and size validate URLs first webhook URL localhost:41250/a2a-events SSRF posts to internal hosts reject private hosts or keep an allowlist POST /a2a-events a notification arrives forged notification acts on a fake event check Authorization and that taskId is yours stored records history, configs, logs secrets kept in clear hook_secret_91d2 echoed redact, scope reads set a retention policy
Data crosses six boundaries in the kit's system, and each needs a check of its own, because A2A labels nothing it carries as safe. Read each row left to right: where data crosses, what can go wrong, and the check the specification asks for. Rows appear in the order the planner meets them. Violet checks belong to the planner and blue checks to the remote agent. From capture/out/15-push.http, 06-input-required.http, 11-errors.http, and 17-signed-card.txt.

The requirements, and who meets them

The table lists each rule in §13 and §14 that binds a server or a client with a MUST or a SHOULD. The kit column says where the kit meets the rule or does not. The SDK column gives the comparison of the six official SDKs where one was made, with a link to the section that owns the detail.

RuleThe kitThe SDKs
Servers "MUST implement authorization checks on every A2A Protocol Operations request" spec §13.1none per caller: the deployer needs one shared token, and the other two agents accept anyonenone includes authentication, and owner scoping differs, as security schemes tables
GetExtendedAgentCard "MUST require authentication" spec §13.3the deployer checks its bearer token before every methodnone in the core handlers, as extended cards tables
Agents "MUST include authentication credentials in webhook requests" spec §13.2Authorization: Bearer hook_secret_91d2 on every POSTevery SDK that pushes. a2a-go and a2a-rs send only the Bearer and Basic schemes sdk-go a2asrv/push/sender.go at v2.6.0 sdk-rust a2a-server/src/push/sender.rs at a2a-server-lf-v0.5.1, and a2a-dotnet has no sender sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2
Agents "SHOULD validate webhook URLs to prevent SSRF" spec §13.2accepts http://localhost:41250/a2a-events, because every process runs on one machinea2a-go, a2a-rs, and a2a-java reject private hosts by default sdk-java server-common/src/main/resources/META-INF/a2a-defaults.properties at v1.4.0.Final. a2a-python has an opt-in validator sdk src/a2a/utils/push_url_validator.py, and the notes record no check in a2a-js
Agents "SHOULD implement reasonable timeout values for webhook requests" and "SHOULD implement retry logic" spec §13.210 s, and no retry5 s in a2a-js, 30 s in a2a-go and a2a-rs, the HTTP client default in a2a-python and a2a-java, and none retries
Clients "MUST validate webhook authenticity using the provided authentication credentials" spec §13.2the planner's receiver records the headers and checks nothingnot compared, and push notifications lists the receiver's duties
Clients "SHOULD use unique, single-purpose tokens for each push notification configuration" spec §13.2one token per run, and a read returns it in clearthe reference SDK returns stored credentials in clear too, as push notifications shows
Agents "MUST validate all input parameters before processing" and "SHOULD sanitize or validate file content types and reject unexpected media types" spec §13.4a missing field answers -32602 with a BadRequest detail, and code-reviewer refuses a url part with image/pngthe reference SDK's media type check is off by default, as the kit README lists
"Implementations MUST sanitize user-provided content to prevent injection attacks" spec §14.1.1the planner decides on typed data fields onlynot compared
"File references within A2A messages MUST be validated to prevent server-side request forgery (SSRF)" spec §14.1.1code-reviewer fetches no URLnot compared
"Logs MUST NOT include sensitive information (credentials, personal data) unless required and properly protected" spec §13.4the capture files hold dpl_test_7c1e4b and hook_secret_91d2, which are test valuesnot compared
"Implementations SHOULD support HTTPS to ensure authenticity and integrity of the Agent Card" and "Clients SHOULD verify signatures when present" spec §14.3the planner fetches cards over plain HTTP and verifies nothing, and 17-signed-card.txt shows the checknot compared, and extended cards and signatures covers verification
Agents "SHOULD implement rate limiting on all operations" and "SHOULD log security-relevant events" spec §13.4nonenot compared

Three of those rules deserve more than a row, because the data they guard arrives in every task.

Text is data, even when it reads like an order

Prompt injection: text that arrives as data and that a model on the receiving side follows as an instruction. No field of a part says how far to trust its content proto Part, and review.json was built from a diff that other people wrote.

The kit's planner decides from data parts. It reads failed from summary.json and severity from each finding in review.json, and ignores the free text beside them. Its one use of remote text is a substring test on the reviewer's question. A planner built on a model reads much more. So act only on typed fields you have checked. Keep side effects behind an approval that a person or a policy controls, as the deployer does.

URLs aim the receiver's own requests

Server-side request forgery (SSRF): a server sends a request to an address that an outside caller chose, such as an internal service. A url part invites it, and so does a webhook URL, which the agent calls for every later event of the task.

The kit's code-reviewer refuses a url part with media type image/png, because its input modes do not list it, and fetches nothing. For webhooks the specification asks agents to "Reject private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)" spec §13.2 and to "Reject localhost and link-local addresses" spec §13.2. The kit accepts http://localhost:41250/a2a-events only because every process runs on one machine. The reference SDK's validator rejects hosts that resolve to loopback, private, link-local, reserved, multicast, or unspecified addresses sdk src/a2a/utils/push_url_validator.py. It runs only when the host turns it on. Push notifications covers the receiver's checks.

Records that outlive the task

A read of a stored configuration returns its secret in clear:

the configuration read back with its secret in clearcapture/out/15-push.httpjson
    "configs": [
      {
        "id": "92c697ef-4abc-4e86-82ba-dc6bce4077af",
        "taskId": "d7d3dceb-7333-4676-90ce-36542116a45c",
        "url": "http://localhost:41250/a2a-events",
        "token": "planner-run-0006",
        "authentication": {
          "scheme": "Bearer",
          "credentials": "hook_secret_91d2"
The ListTaskPushNotificationConfigs reply, cut to one configuration.

These reads need the task's own access check spec §13.1, and security schemes covers that scope. The capture files hold dpl_test_7c1e4b and hook_secret_91d2 in clear, which is safe only because they are test values. The refunds.diff part stays in the review task's history, and "Sensitive information in task history and artifacts MUST be protected according to applicable data protection regulations" spec §14.1.1. Set a retention period for all of it, as §13.4 recommends spec §13.4.

Sources:spec §13.1, §13.2, §13.3, §13.4, §14.1.1, §14.3 (research/sources/specification.md); proto Part (research/sources/a2a.proto); sdk src/a2a/utils/push_url_validator.py at a2a-python 1.2.2; sdk-go a2asrv/push/sender.go at v2.6.0; sdk-rust a2a-server/src/push/sender.rs at a2a-server-lf-v0.5.1; sdk-java server-common/src/main/resources/META-INF/a2a-defaults.properties at v1.4.0.Final; sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2; capture/out/06-input-required.http, 11-errors.http, 15-push.http, 17-signed-card.txt; capture/planner.py, capture/a2a_ref.py, capture/README.md

Part 7

Implementing A2A

A client and a server follow the rules of the earlier parts, and the official test kit checks the server against them.

  1. 7.1A client
  2. 7.2A server
  3. 7.3Conformance testing
  4. 7.4From 0.3 to 1.0
7.1

A client

A working client is a loop that reads cards, picks an agent for each skill, and has a branch for every state a task can reach.

The planner has one job: deliver a change to payments-api. It must run the tests on commit a41d7c3, get the diff reviewed, and deploy build 2026.10.06-1 to staging. It starts with three port numbers, a token for the deployer, and the name of the base branch. The file capture/planner.py does the whole job in 86 lines, and 19-planner.log records each decision.

When you finish this section, you can write a client that delegates by skill and handles every state a task reaches.

Discover: read every card first

Before it sends anything, the planner reads the card on each port. It takes the interface whose protocolBinding is JSONRPC at protocolVersion 1.0, and it files each skill id under the agent that offers it. It also notes each card's streaming flag and securityRequirements.

The specification's rule is positional, to "select the first supported transport" spec §8.3.2. Both rules agree here, because JSON-RPC comes first on all three cards. Discovery and supported interfaces gives the full selection rule.

Delegate: send once, then switch on the reply

Each delegation is one SendMessage without returnImmediately. It blocks until the task reaches a terminal or an interrupted state, as SendMessage explains. The bearer token goes only to agents whose card asked for one, as security schemes describes. A loop then switches on the reply.

delegate, the loop that switches on every replycapture/planner.pypython
        while True:
            if "error" in reply:
                self.note(f"  error {reply['error']['code']}: {reply['error']['message']}")
                return None
            result = reply["result"]
            if "message" in result:
                self.note("  direct reply, no task to follow")
                return result["message"]
            task = result["task"] if "task" in result else result
            state = task["status"]["state"]
            self.note(f"  task {task['id'][:8]} is {state}")
            if state in TERMINAL:
                return task
            question = task["status"]["message"]["parts"][0]["text"]
            if state == "TASK_STATE_INPUT_REQUIRED":
                answer = self.answer(question)
                self.note(f"  it asks: {question} The planner answers from its own context: {answer}")
                _, reply = self.client.rpc(f"SendMessage: answer {agent['name']}", agent["port"], "SendMessage", {"message": self.client.message(answer, task_id=task["id"], context_id=task["contextId"])}, headers=headers)
                continue
            if state == "TASK_STATE_AUTH_REQUIRED":
                self.note(f"  it needs approval: {question} The planner cannot approve, so it asks the operator and subscribes.")
                return self.follow(agent, task, headers)
The loop of the delegate method, complete. The lines above it, which pick the agent and send the message, are cut.

The reply has three shapes, checked in order: an error, a direct message, or a task proto SendMessageResponse. The TERMINAL tuple holds all four terminal states, TASK_STATE_REJECTED included, the one clients forget most often.

On TASK_STATE_INPUT_REQUIRED, the planner answers on the same task, with its taskId and contextId, as interrupted states explains. On TASK_STATE_AUTH_REQUIRED, the planner has nothing to send, so it passes the task to follow.

Follow: subscribe, then call the operator

The first frame of a subscription is a snapshot of the task, as streaming shows. The planner subscribes first and calls the operator only when that snapshot arrives.

follow, until the stream endscapture/planner.pypython
    def follow(self, agent, task, headers):
        def on_frame(index, event):
            if index == 1:
                self.operator(task)

        _, events = self.client.rpc(f"SubscribeToTask on {agent['name']}", agent["port"], "SubscribeToTask", {"id": task["id"]}, headers=headers, on_frame=on_frame)
        final = events[-1]["result"]["statusUpdate"]["status"]["state"]
        self.note(f"  the stream ends at {final}")
        return {"id": task["id"], "status": {"state": final}}
The whole method. The operator call stands in for a person who approves outside A2A.

The operator call is a POST /approve/<task id>, outside A2A. The stream is open before the operator acts, so no update after the approval can pass the planner unseen spec §7.6.2. Three more frames follow the snapshot, and the last one carries TASK_STATE_COMPLETED.

The run, decision by decision

Between delegations, the ship method decides whether to go on. It stops if the test task did not complete, if any test failed, or if any finding has high severity. Figure 7.1 draws the run in 19-planner.log as three lanes, with the decisions below them.

Fig. 7.1the planner's run, decision by decisionflow
The planner run, decision by decision The planner reads three cards and maps skills to agents. Lane one: SendMessage for run-tests on commit a41d7c3 returns task 954267b7 completed, and summary.json shows 13 passed and 0 failed. Lane two: SendMessage for review-diff returns TASK_STATE_INPUT_REQUIRED asking for the base branch, the planner answers main on the same task, the task completes, and review.json holds one low-severity finding. Lane three: SendMessage with a bearer token for deploy returns TASK_STATE_AUTH_REQUIRED, the planner subscribes, an operator approves outside A2A on the first frame, and the stream ends at TASK_STATE_COMPLETED. The planner decides after each lane and would stop on a failed test or a high-severity finding. DISCOVER: ONE CARD PER PORT, SKILLS MAPPED TO AGENTS DECISIONS stop if the test task does not complete, a test fails, or a finding is high severity test-runner run-tests code-reviewer review-diff, answer-question deployer deploy, needs a bearer token delegate run-tests SendMessage commit a41d7c3 TASK_STATE_COMPLETED task 954267b7 summary.json passed 13, failed 0 tests 13 passed, go on delegate review-diff SendMessage refunds.diff as a raw part TASK_STATE_INPUT_REQUIRED which base branch? answer SendMessage, same task answer from context: main TASK_STATE_COMPLETED task 80bc7784 review.json 1 finding, severity low review 1 finding, 0 blocking delegate deploy SendMessage build 2026.10.06-1, token TASK_STATE_AUTH_REQUIRED an operator must approve follow SubscribeToTask frame 1: the Task snapshot on frame 1 operator approves POST /approve, not A2A 3 more frames TASK_STATE_COMPLETED task a31361b3, stream ends deploy TASK_STATE_COMPLETED
The planner reads three cards, delegates three skills in turn, answers a question, escalates an approval, and decides from data parts before each next step. Read the three lanes left to right, then the decisions row. Each lane is one delegation, top to bottom, with the states the replies carried. Task ids are shortened to 8 characters. From capture/out/19-planner.log and 19-planner.http.

Each decision reads a data part, never free text: passed and failed from summary.json, and severity from each finding in review.json. On commit 9f3c2e1, one test fails, and the planner would stop before the review.

What a production planner adds

Against agents it does not control, a planner needs more than the kit's loop.

  • Safe retries. Resend a failed message with the same messageId spec §3.3.1. Keep the first reply, because a resend without taskId creates a second task in the reference SDK sdk src/a2a/server/agent_execution/active_task.py.
  • Timeouts. The kit's client gives each request 15 seconds, in capture/wire.py, and the specification sets none. For long work, set returnImmediately and follow the task.
  • Card checks. Cache cards with standard HTTP caching spec §8.6, and verify the deployer's signatures as extended and signed cards shows.
  • Capability checks. The planner records streaming and never reads it. Against an agent without streaming, its SubscribeToTask gets UnsupportedOperationError spec §3.3.4.
  • A branch for every state. The loop has no branch for TASK_STATE_SUBMITTED or TASK_STATE_WORKING. A server that answers a blocking send at once (conflict D2) would leave it looping on the same reply.
  • Checks on every result. The ship method reads the review artifact without checking that the review task completed, and follow needs a GetTask call when a stream drops.
  • A real operator channel. Route each approval to a person through an authenticated channel, and record who approved.

Sources:spec §3.3.1, §3.3.4, §7.6.2, §8.3.2, §8.6 (research/sources/specification.md); proto SendMessageResponse (research/sources/a2a.proto); sdk src/a2a/server/agent_execution/active_task.py at a2a-python 1.2.2; capture/planner.py, capture/wire.py, capture/run.py, capture/agents.py; capture/out/19-planner.http, 19-planner.log

7.2

A server

A server is a card, one handler per binding, three checks before any work, an executor, an owner-scoped task store, and one event queue that feeds streams and webhooks.

The three agents in the kit come from one file, capture/a2a_ref.py, 675 lines of the Python standard library. Each agent serves a card, answers both JSON bindings, runs its own logic in a thread, and keeps its tasks in a dictionary. When you finish this section, you can name each part a server must build, and find it in a2a_ref.py and in the reference SDK.

Figure 7.2 follows one SendStreamingMessage through those parts, from the HTTP request to the stream frames and the webhook POST.

Fig. 7.2the parts of a server, in the order a request meets themstructure
The parts of a server, in the order a request meets them One SendStreamingMessage request enters the kit server at Handler.jsonrpc or Handler.rest. It passes authenticate, which answers HTTP 401 for a bad token, check_version, which answers VersionNotSupportedError -32009, and the capability check on the card, which answers UnsupportedOperationError -32004. Agent.send then validates the message and stores a task in Agent.tasks as TASK_STATE_SUBMITTED. Agent.work runs the behavior, the agent logic, in a thread. Agent.apply writes each event to the task record and Agent.broadcast copies it to every subscriber queue, which Handler.pump writes as SSE data frames, and to Agent.deliver, which POSTs it to each webhook URL. THE REQUEST, TOP TO BOTTOM WHAT A FAILED CHECK RETURNS SendStreamingMessage request POST /a2a/jsonrpc, A2A-Version: 1.0 Handler.jsonrpc, Handler.rest one method per binding, same checks authenticate bearer token, deployer only HTTP 401 WWW-Authenticate: Bearer check_version A2A-Version must be 1.0 VersionNotSupportedError -32009, FAILED_PRECONDITION card["capabilities"] streaming, pushNotifications UnsupportedOperationError -32004, or -32003 for push Agent.send validate_message, then one task Agent.tasks the task record, TASK_STATE_SUBMITTED Agent.work, behavior the agent logic, in its own thread a thread per request Agent.apply, Agent.broadcast the record, then every subscriber yields events writes the task Handler.pump data: frames on each SSE stream Agent.deliver POST to each webhook URL each event each event
A request passes authentication, the version check, and the capability check before Agent.send stores a task, and every event then leaves through one broadcast. Read the left column top to bottom. Each box is a method in capture/a2a_ref.py. Rose boxes on the right are the errors each check returns, with their JSON-RPC codes from capture/out/11-errors.http. From capture/a2a_ref.py.

The card and one handler per binding

Card route: the GET on /.well-known/agent-card.json that returns the card spec §8.2. The kit's Handler.do_GET answers it from published_card. The SDK's create_agent_card_routes serves the same path, named by AGENT_CARD_WELL_KNOWN_PATH sdk src/a2a/server/routes/agent_card_routes.py sdk src/a2a/utils/constants.py.

Binding handler: one entry point per binding, which parses the request, runs the checks, and calls the same agent methods. The kit has Handler.jsonrpc at /a2a/jsonrpc and Handler.rest under /a2a/rest/, and no gRPC, as gRPC explains. The SDK has create_jsonrpc_routes, create_rest_routes, and GrpcHandler sdk src/a2a/server/routes/jsonrpc_routes.py sdk src/a2a/server/routes/rest_routes.py sdk src/a2a/server/request_handlers/grpc_handler.py. All three call one DefaultRequestHandler, which is DefaultRequestHandlerV2 sdk src/a2a/server/request_handlers/__init__.py.

the JSON-RPC handler, from the token to the first two methodscapture/a2a_ref.pypython
    def jsonrpc(self, raw):
        request_id = None
        if not self.authenticate():
            return None
        try:
            try:
                request = json.loads(raw)
            except ValueError:
                raise A2AError("JSONParseError")
            if not isinstance(request, dict) or request.get("jsonrpc") != "2.0" or "method" not in request:
                raise A2AError("InvalidRequestError")
            request_id = request.get("id")
            self.check_version()
            method = request["method"]
            params = request.get("params") or {}
            agent = self.agent
            if method == "SendMessage":
                return self.write_json(200, {"jsonrpc": "2.0", "id": request_id, "result": agent.send(params)}, "application/json")
            if method == "SendStreamingMessage":
                if not agent.card["capabilities"].get("streaming"):
                    raise A2AError("UnsupportedOperationError")
                stream = queue.Queue()
                agent.send(params, stream)
                self.open_stream()
                return self.pump(stream, lambda event: {"jsonrpc": "2.0", "id": request_id, "result": event})
            …
        except A2AError as error:
            return self.write_json(200, {"jsonrpc": "2.0", "id": request_id, "error": error.jsonrpc()}, "application/json")
The head of the jsonrpc method and its error branch. The other nine method branches are cut.

Three checks before any work

Authentication runs first. The kit's authenticate answers HTTP 401 with a WWW-Authenticate challenge before any method, on the deployer only. The SDK checks no credential itself: ServerCallContext.user comes from the web framework's middleware sdk src/a2a/server/routes/common.py. Security schemes says what to check.

The version check runs second. check_version accepts exactly 1.0 and raises VersionNotSupportedError for anything else, because "Agents MUST interpret empty value as 0.3 version." spec §3.6.2. The SDK's validate_version decorator wraps every JSON-RPC and REST dispatcher method and compares the major version sdk src/a2a/utils/version_validator.py. It does not wrap the gRPC servicer. The version header gives the rules and each SDK's behavior.

The capability check runs third. Before a stream or a push operation, the kit reads card["capabilities"] and raises UnsupportedOperationError or PushNotificationNotSupportedError spec §3.3.4. The SDK does the same with a validate decorator on on_message_send_stream and on the push methods sdk src/a2a/server/request_handlers/default_request_handler_v2.py.

The executor and the task store

Executor: the code that runs the agent for one request and emits its events. The kit's Agent.send validates the message, creates the task in TASK_STATE_SUBMITTED, and starts Agent.work in a thread. work calls behavior, the agent's own logic, and applies each event it yields. The SDK's AgentExecutor.execute receives a RequestContext and an EventQueue, and returns when the work is done or the task enters an interrupted state sdk src/a2a/server/agent_execution/agent_executor.py sdk src/a2a/server/agent_execution/context.py.

What the executor calls stays hidden from the client. Figure 7.3 draws that line for the three kit agents.

Fig. 7.3A2A between agents, tools inside each onelayers
A2A between agents, tools inside each one The planner, the client agent, sends SendMessage over A2A to three remote agents: test-runner on port 41241 with skill run-tests, code-reviewer on port 41242 with skills review-diff and answer-question, and deployer on port 41243 with skill deploy. The planner sees only these cards and endpoints. Below a dashed line, hidden from the planner, each agent runs its own logic and calls its own systems as tools: Git and a CI runner for test-runner, Git for code-reviewer, and a deploy API for deployer. CLIENT AGENT planner delivers payments-api: tests, review, deploy A2A · WHAT THE PLANNER SEES HIDDEN FROM THE PLANNER The kit simulates these systems in Python. A real agent calls them through MCP or any API. SendMessage test-runner :41241 run-tests SendMessage code-reviewer :41242 review-diff answer-question SendMessage deployer :41243 deploy its own logic model and code its own logic model and code its own logic model and code tool call Git, CI runner check out, run the suite tool call Git fetch the base branch tool call deploy API roll out to staging
The planner sees only each agent's card and endpoint, while each agent calls its own logic and tools below a line that A2A never crosses. Read top to bottom. Above the dashed line is what the planner sees over A2A. Below it is what each agent runs on its own, which the kit simulates in Python. From capture/agents.py and capture/out/01-agent-cards.http.

Task store: the record of every task, read by GetTask, ListTasks, and each follow-up message. The kit's Agent.tasks is one dictionary with no owner, and the deployer's bearer token is its only guard. The SDK's TaskStore has save, get, list, and delete, each given the ServerCallContext sdk src/a2a/server/tasks/task_store.py. InMemoryTaskStore keys tasks by owner, and the default OwnerResolver returns context.user.user_name sdk src/a2a/server/owner_resolver.py sdk src/a2a/server/tasks/inmemory_task_store.py. Another caller's task is then TaskNotFoundError, but every caller without a user name shares one scope. "Servers MUST implement authorization checks on every A2A Protocol Operations request" spec §13.1, so the integrator adds the middleware, as security schemes shows for each SDK.

One event queue for streams and webhooks

The kit's Agent.apply updates the task record. Agent.broadcast then puts a copy of the event on each subscriber queue and calls deliver for each push config. Handler.pump reads one subscriber queue and writes each event as a data: frame. broadcast closes the stream at a terminal state or TASK_STATE_INPUT_REQUIRED, and deliver POSTs the event to the webhook URL with X-A2A-Notification-Token.

The SDK's ActiveTask does the same with two queues. The executor writes to _event_queue_agent. A consumer reads it, saves the task, forwards each event to _event_queue_subscribers, and calls PushNotificationSender.send_notification sdk src/a2a/server/agent_execution/active_task.py sdk src/a2a/server/tasks/push_notification_sender.py. Streaming and push notification configs show both paths frame by frame.

Sources:spec §3.3.4, §3.6.2, §8.2, §13.1 (research/sources/specification.md); sdk src/a2a/server/routes/agent_card_routes.py, src/a2a/utils/constants.py, src/a2a/server/routes/jsonrpc_routes.py, src/a2a/server/routes/rest_routes.py, src/a2a/server/routes/common.py, src/a2a/server/request_handlers/grpc_handler.py, src/a2a/server/request_handlers/__init__.py, src/a2a/server/request_handlers/default_request_handler_v2.py, src/a2a/utils/version_validator.py, src/a2a/server/agent_execution/agent_executor.py, src/a2a/server/agent_execution/context.py, src/a2a/server/agent_execution/active_task.py, src/a2a/server/tasks/task_store.py, src/a2a/server/tasks/inmemory_task_store.py, src/a2a/server/owner_resolver.py, src/a2a/server/tasks/push_notification_sender.py at a2a-python 1.2.2; capture/a2a_ref.py, capture/agents.py, capture/README.md; capture/out/01-agent-cards.http, 11-errors.http

7.3

Conformance testing

a2a-tck grades one server against v1.0.0 on three bindings, and a run against the capture kit shows which failures are the server's and which are the TCK's.

On 2026-10-06 the kit's test-runner served on port 41251 for two runs of a2a-tck 1.0.0.alpha2. The first run, with the TCK as published, scored 32.3 percent. The second, after a two-line change to the TCK's JSON-RPC client, scored 66.7 percent. When you finish this section, you can run the TCK against your server and tell a real failure from a defect in the test.

Running a2a-tck

The TCK is a pytest project. It reads {sut-host}/.well-known/agent-card.json, builds one client for each supportedInterfaces entry, and runs each test on each binding the card declares. It checks the v1.0.0 branch, not the 1.0.1 patch this manual pins.

git clone --filter=blob:none https://github.com/a2aproject/a2a-tck.git
cd a2a-tck
git checkout 1.0.0.alpha2
uv venv && source .venv/bin/activate
uv pip install -e .
./run_tck.py --sut-host http://localhost:9999

run_tck.py builds one pytest command, and --transport, --level, and --webhook-host narrow it. A MUST test is a hard failure. A SHOULD test is an xfail, and a MAY test is skipped when the card does not declare the capability. compatibility.json holds one verdict per requirement, and the percentages count only PASS and FAIL.

What the kit's run showed

In the first run, 63 of the 85 JSON-RPC records failed for one reason. The TCK's JSON-RPC client posts to the card's url plus a trailing slash, and the kit answers HTTP 404 for that path. The second run posted to the exact url and reached 62 PASS, 31 FAIL, 11 SKIPPED, and 25 NOT TESTED of 129 requirements. All 72 gRPC records were skipped, because the kit has no gRPC interface.

Of the 31 failures, 21 are skips inside a test body, which the tag records as FAIL and main records as SKIPPED. Most expect a task parked in TASK_STATE_INPUT_REQUIRED, which test-runner never does. With the main rule the run reads 62 PASS, 10 FAIL, and 32 SKIPPED, or 86.1 percent.

One of the ten was the kit's. Its Handler.rest served no route under tasks/{id}/pushNotificationConfigs, although the card declares pushNotifications on both JSON bindings. Every declared binding must offer the same operations spec §5.1. The kit now serves those four routes, and the six HTTP+JSON push records pass. Each requirement still fails on its JSON-RPC side, where the TCK sends task_id instead of taskId.

Two SHOULD failures are the card's missing Cache-Control and ETag headers spec §8.6. The rest are the TCK's own, or its sample inputs.

six push requirements after the kit fixresearch/tck-summary.txttext
PUSH-CREATE-001 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=FAIL FAIL
PUSH-CREATE-002 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=FAIL FAIL
PUSH-DEL-001 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=FAIL FAIL
PUSH-DEL-002 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=FAIL FAIL
PUSH-GET-001 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=FAIL FAIL
PUSH-GET-002 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=PASS PASS
PUSH-LIST-001 MUST grpc=SKIPPED,http_json=PASS,jsonrpc=FAIL FAIL
One line per requirement: its id, its level, the verdict on each binding, and the verdict the TCK records. The six HTTP+JSON records pass. The JSON-RPC records fail on the TCK's snake_case parameters, so the TCK records each requirement as FAIL.

research/tck-run.md classifies every row of the run.

Where the TCK departs from v1.0.1

The TCK expectsThe specification saysEffect on the kit
JSON-RPC requests at <url>/the interface is at url spec §8.3.163 records in run 1
snake_case parameters such as task_id and history_length on JSON-RPCJSON field names MUST be camelCase spec §5.5PUSH-CREATE-001 fails on JSON-RPC, GetTask ignores history_length
a Content-Type that contains application/json on RESTapplication/a2a+json SHOULD be used spec §11.1HTTP_JSON-SVC-001 and HTTP_JSON-ERR-001 fail
any error on CORE-SEND-003 counts as a failurean unsupported part gets ContentTypeNotSupportedError spec §3.3.2fails on both bindings
HTTP 409 for TaskNotCancelableError, 415 for ContentTypeNotSupportedError, 502 for InvalidAgentResponseError, and gRPC UNIMPLEMENTED for three errors400, 400, 500, and FAILED_PRECONDITION spec §5.4not reached in this run
a hard assertion on the SHOULD and MAY caching testsSHOULD is an xfail in the TCK's own READMEthree card-cache failures

Errors has the full mapping.

What the TCK does not test

Five behaviors that split the SDKs never run. The REST subscribe verb (B1): the client always sends POST. Pagination (B5): one ListTasks call, checked against the schema only. An append to an unknown artifact (B11): no test sends an artifact update. Another caller's task (B12): AUTH-SCOPE-001 to 003 have no test. A resent messageId (B14): every test mints a new id.

The 25 NOT TESTED requirements also cover in-task authorization, TLS, card signatures, binding equivalence, and the client's version header. Security requirements and signed cards give those rules.

Three more tools

a2a-inspector is a browser debugger. It fetches a card, checks its structure, sends messages, and shows each event. Its only release, v0.1.0, predates 1.0 and requires a top-level url. Use main, which accepts 0.3 and 1.0 payloads and builds its client from the card's first interface. Run uv sync and npm install in frontend, then bash scripts/run.sh, and open http://127.0.0.1:5001. It decides nothing about conformance.

a2a-cli is the official command-line client, v0.3.0, built on a2a-go. a2a card get <url> reads a card, and a2a send -a <url> "text" sends a message, with --stream or --async. a2a task get, list, cancel, and subscribe cover the rest, and --svc-param A2A-Version=0.3 sets a header by hand. It is a client, not a grader.

a2a-itk has no release. It is a cross-SDK harness: each agent forwards a nested instruction to the next peer over the scenario's binding. uv run run_tests.py runs the bundled set. Its known_failures.yaml records the B1 split: the Rust client sends GET, and the .NET server accepts POST only.

Sources:spec §3.3.2, §5.1, §5.4, §5.5, §8.3.1, §8.6, §11.1 (research/sources/specification.md); the TCK run of 2026-10-06 (research/tck-run.md, research/tck-summary.txt); a2a-tck at tag 1.0.0.alpha2 (README.md, run_tck.py, tck/requirements/base.py, tck/transport/jsonrpc_client.py, tests/compatibility/conftest.py) and main 263b9cf; a2a-inspector main 8aa0646 (README.md, backend/validators.py, backend/app.py); a2a-cli v0.3.0 (README.md, internal/README.md, specification/SPEC.md); a2a-itk main b57c533 (README.md, matrix.yaml, known_failures.yaml); capture/a2a_ref.py, capture/agents.py, capture/README.md

7.4

From 0.3 to 1.0

Version 1.0 renames every operation, changes every enum value, flattens the card and the parts, and rejects a request without A2A-Version, so a 0.3 client needs a compatibility layer.

The kit's error scenario sends two requests that a 0.3 client would send today. One omits A2A-Version and gets VersionNotSupportedError. The other calls message/send and gets MethodNotFoundError. When you finish this section, you can map a 0.3 request to its 1.0 form, and turn on the 0.3 layer in the reference SDK.

a 0.3 method name on a 1.0 endpointcapture/out/11-errors.httphttp
POST /a2a/jsonrpc HTTP/1.1
Host: localhost:41241
Content-Type: application/json
A2A-Version: 1.0

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "message/send",
  …
}

HTTP/1.1 200 OK
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}
The third record of the error scenario. The message in the params is cut.

Before and after

The project's migration guide lists the changes docs whats-new-v1, because "Migration guidance MUST be provided via an ancillary document when introducing major breaking changes" spec §1.4. The table keeps the changes that reach code.

Area0.31.0
Method namesmessage/send, tasks/get, tasks/resubscribeSendMessage, GetTask, SubscribeToTask, the same PascalCase name on every binding spec §9.1
Push config methodstasks/pushNotificationConfig/set, and /get, /list, /deleteCreateTaskPushNotificationConfig, and Get, List (plural Configs), Delete
Extended cardagent/getAuthenticatedExtendedCard, REST GET /v1/cardGetExtendedAgentCard, REST GET /extendedAgentCard, no /v1 prefix
Task states"working", "input-required"TASK_STATE_WORKING, TASK_STATE_INPUT_REQUIRED proto TaskState
Roles"user", "agent"ROLE_USER, ROLE_AGENT proto Role
Part shapeTextPart, FilePart, DataPart, each with kindone Part whose text, raw, url, or data member names the type proto Part
File fieldsfile.mimeType, file.name, file.fileWithUrimediaType, filename, url, flat on the part
Stream frames"kind": "status-update"a statusUpdate or artifactUpdate member proto StreamResponse
End of a streamfinal: true on the last status updatethe binding closes the stream
Card endpointurl, protocolVersion, preferredTransport, additionalInterfacessupportedInterfaces[], each with url, protocolBinding, protocolVersion proto AgentInterface
Extended card flagsupportsAuthenticatedExtendedCardcapabilities.extendedAgentCard
Card securitysecuritysecurityRequirements, each with a schemes map proto AgentCard
ErrorsRFC 9457 problem detailsgoogle.rpc.Status with ErrorInfo, plus -32008 and -32009 spec §5.4
Version headernoneA2A-Version: 1.0 on every request spec §3.6.1
the five card fields that movedresearch/sources/docs.mdjson
{
  "protocolVersion": "0.3",
  "url": "https://agent.example.com/a2a",
  "preferredTransport": "JSONRPC",
  "supportsAuthenticatedExtendedCard": true,
  "additionalInterfaces": [...]
}
The 0.3 card example from the migration guide. The first four fields became supportedInterfaces, and the fifth became capabilities.extendedAgentCard.

The guide misses one change. The 0.3 flag blocking was false by default, so a send returned at once. Its replacement returnImmediately is also false by default, so a send now waits proto SendMessageConfiguration spec §3.2.2. SendMessage shows both modes.

The missing header

"Agents MUST interpret empty value as 0.3 version." spec §3.6.2 A 1.0-only interface must then answer VersionNotSupportedError spec §5.4, as the kit does.

The SDKs differ. JS and Java treat a missing header as 0.3 and reject it on every binding sdk-js src/server/version.ts at v1.3.0 sdk-java server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java at v1.4.0.Final. Python does so on JSON-RPC and REST sdk src/a2a/utils/version_validator.py, and Rust on JSON-RPC only sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1. Go never reads the header, and .NET accepts a missing one sdk-go a2a/svcparams.go at v2.6.0 sdk-dotnet src/A2A.AspNetCore/A2AJsonRpcProcessor.cs at v1.0.0-preview2. The version header has the full table.

The 0.3 layer in each SDK

A server can "Accept both legacy and current request message forms during the overlap period" spec §A.2.

  • a2a-python 1.2.2. Pass enable_v0_3_compat=True to create_jsonrpc_routes and create_rest_routes, and add an AgentInterface with protocolVersion 0.3 to the card sdk src/a2a/server/routes/jsonrpc_routes.py sdk src/a2a/server/routes/rest_routes.py. The dispatcher routes by method name, so message/send reaches JSONRPC03Adapter before the version check sdk src/a2a/server/routes/jsonrpc_dispatcher.py. For gRPC it adds CompatGrpcHandler on the old package a2a.v1 sdk src/a2a/compat/v0_3/grpc_handler.py. The client needs no switch: ClientFactory picks a compat transport for an interface below 1.0 sdk src/a2a/client/client_factory.py.
  • a2a-js 1.3.0. Set legacyCompat: { enabled: true } on each handler. It routes by A2A-Version, with an absent header as 0.3 sdk-js src/server/express/json_rpc_handler.ts at v1.3.0.
  • a2a-go 2.6.0. Mount a second handler from a2acompat/a2av0 under its own URL and advertise a second interface. It routes by URL, never by header sdk-go a2acompat/a2av0/jsonrpc_server.go at v2.6.0.
  • a2a-java 1.4.0.Final. Add the multiversion-jsonrpc or multiversion-rest reference module. Its VersionRouter sends 0.3 or an absent header to the 0.3 handler sdk-java docs/content/dev/compatibility.md at v1.4.0.Final.
  • a2a-dotnet 1.0.0-preview2. Call MapA2AWithV03Compat instead of MapA2A. It routes by A2A-Version sdk-dotnet src/A2A.V0_3Compat/V03ServerCompatEndpointExtensions.cs at v1.0.0-preview2.
  • a2a-rs 0.5.1. None sdk-rust a2a-client/src/factory.rs at a2a-server-lf-v0.5.1.

Where the migration guide is wrong

The proto is normative spec §1.4, so follow it where the guide names a field or a value the proto lacks.

  • The guide named the stream members taskStatusUpdate and taskArtifactUpdate. The proto has statusUpdate and artifactUpdate (conflict D9). PR #2056 fixed it on main after v1.0.1, not yet released.
  • The guide added Task.createdAt, Task.lastModified, configId, and an artifact index that the proto lacks (conflict D10). The same PR removed all but index.
  • Appendix A.2.2 calls the old flag supportsExtendedAgentCard. The guide and the SDK say supportsAuthenticatedExtendedCard (conflict D40), and main has no fix.
  • The guide puts extensions at agentCard.extensions. The proto puts them at capabilities.extensions proto AgentCapabilities, as extensions explains.

After v1.0.1

No tag newer than v1.0.1 exists. Main has one proto change since the tag: the gRPC url comment, now hostname:port (PR #1997), which closes D34. It is the only entry in the pending 1.0.2 release (PR #2072). Documentation commits on main close D8, D9, D12, and D32, and part of D4, D5, D6, D10, and D23 (the conflict register). Main also adds §7.6.4 on in-task authorization scope (PR #2081), which security schemes quotes.

Branch dev-1.1 changes the task record. Task.generation counts every state change, and GetTaskRequest.currentGeneration lets a client wait for the next one. SendMessageConfiguration.ifGenerationMatch makes a send conditional, and a stale value gets TaskGenerationMismatchError, -32010 (PR #1810). Task.history is deprecated for a timeline of messages and statuses (PR #2129). None of this is released, and no 1.1 date is published.

Sources:spec §1.4, §3.2.2, §3.6.1, §3.6.2, §5.4, §9.1, §A.2 (research/sources/specification.md); proto TaskState, Role, Part, StreamResponse, AgentInterface, AgentCard, AgentCapabilities, SendMessageConfiguration (research/sources/a2a.proto); docs/whats-new-v1.md at v1.0.1 (research/sources/docs.md); sdk src/a2a/utils/version_validator.py, src/a2a/server/routes/jsonrpc_routes.py, src/a2a/server/routes/rest_routes.py, src/a2a/server/routes/jsonrpc_dispatcher.py, src/a2a/compat/v0_3/grpc_handler.py, src/a2a/client/client_factory.py at a2a-python 1.2.2; sdk-js src/server/version.ts, src/server/express/json_rpc_handler.ts at v1.3.0; sdk-go a2a/svcparams.go, a2acompat/a2av0/jsonrpc_server.go at v2.6.0; sdk-java server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java, docs/content/dev/compatibility.md at v1.4.0.Final; sdk-dotnet src/A2A.AspNetCore/A2AJsonRpcProcessor.cs, src/A2A.V0_3Compat/V03ServerCompatEndpointExtensions.cs at v1.0.0-preview2; sdk-rust a2a-server/src/jsonrpc.rs, a2a-client/src/factory.rs at a2a-server-lf-v0.5.1; capture/out/11-errors.http; capture/a2a_ref.py; a2aproject/A2A pull requests #1810, #1997, #2056, #2072, #2081, #2129, main at 679ab3a and branch dev-1.1 at db39eb5

Reference

Reference

Lookup tables for every name in the manual.

  1. R.1Operations by binding
  2. R.2Objects and fields
  3. R.3Task states
  4. R.4Errors
  5. R.5Glossary
  6. R.6Sources
  7. R.7Index of figures
R.1

Operations by binding

Every A2A operation has one name in JSON-RPC and gRPC, one route in HTTP+JSON, one request and one response message, and a fixed list of errors.

The proto defines eleven operations in one gRPC service, A2AService, and the JSON-RPC method carries the same name as the rpc proto A2AService spec §5.3. The routes are the proto's google.api.http templates proto A2AService. The first column links each operation to the section that explains it.

JSON-RPC methodgRPC rpcHTTP+JSON routeRequest in, response out
SendMessageSendMessagePOST /message:sendSendMessageRequest, SendMessageResponse
SendStreamingMessageSendStreamingMessagePOST /message:streamSendMessageRequest, a stream of StreamResponse
GetTaskGetTaskGET /tasks/{id=*}GetTaskRequest, Task
ListTasksListTasksGET /tasksListTasksRequest, ListTasksResponse
CancelTaskCancelTaskPOST /tasks/{id=*}:cancelCancelTaskRequest, Task
SubscribeToTaskSubscribeToTaskGET /tasks/{id=*}:subscribe in the proto, and POST in §5.3 and §11.3.2, so accept both verbs, as the reference SDK does (conflict D1) sdk src/a2a/server/routes/rest_routes.pySubscribeToTaskRequest, a stream of StreamResponse
CreateTaskPushNotificationConfigCreateTaskPushNotificationConfigPOST /tasks/{task_id=*}/pushNotificationConfigsTaskPushNotificationConfig in and out, with no request wrapper (conflict D5)
GetTaskPushNotificationConfigGetTaskPushNotificationConfigGET /tasks/{task_id=*}/pushNotificationConfigs/{id=*}GetTaskPushNotificationConfigRequest, TaskPushNotificationConfig
ListTaskPushNotificationConfigsListTaskPushNotificationConfigsGET /tasks/{task_id=*}/pushNotificationConfigsListTaskPushNotificationConfigsRequest, ListTaskPushNotificationConfigsResponse
DeleteTaskPushNotificationConfigDeleteTaskPushNotificationConfigDELETE /tasks/{task_id=*}/pushNotificationConfigs/{id=*}DeleteTaskPushNotificationConfigRequest, google.protobuf.Empty
GetExtendedAgentCardGetExtendedAgentCardGET /extendedAgentCardGetExtendedAgentCardRequest, AgentCard

Every rpc has a second route in its additional_bindings, the same path under a /{tenant} prefix, such as POST /{tenant}/message:send proto A2AService. The prose in §5.3, §11.3, and §11.5 lists only the plain routes (discrepancy N12). In JSON-RPC the same value travels as params.tenant, and in gRPC as the tenant field of the request. In every binding it must match the tenant of the chosen interface when that is set proto SendMessageRequest, as HTTP+JSON explains.

SendMessageResponse holds one task or one message. A send requires message, and get, cancel, and subscribe require the task id. The get, list, and delete configuration requests require taskId, get and delete also require id, and create requires url. JSON-RPC posts each call as one envelope, with the name in method and the request message in params spec §9.3.

Each operation can raise the errors §3.1 lists for it. The last column names the card capability that must be true. When it is false or absent, the push configuration methods fail with PushNotificationNotSupportedError and the others with UnsupportedOperationError spec §3.3.4.

MethodOther A2A errorsCapability flag
SendMessageContentTypeNotSupportedError, TaskNotFoundError, and UnsupportedOperationError for a terminal tasknone
SendStreamingMessageThe same three as SendMessagestreaming
GetTaskTaskNotFoundErrornone
ListTasksNone beyond the standard protocol errorsnone
CancelTaskTaskNotCancelableError, TaskNotFoundErrornone
SubscribeToTaskTaskNotFoundError, and UnsupportedOperationError for a terminal taskstreaming
The four push configuration methodsTaskNotFoundError, which GetTaskPushNotificationConfig also returns for a missing configurationpushNotifications
GetExtendedAgentCardExtendedAgentCardNotConfiguredError when the flag is on and no extended card is configuredextendedAgentCard

Any operation can also fail with VersionNotSupportedError, ExtensionSupportRequiredError, or an authentication or authorization error spec §3.6.2 spec §3.3.2. Errors gives their codes in every binding, and the gRPC binding shows one rpc with its full annotation.

Sources:proto A2AService with its google.api.http routes and additional_bindings, and the request and response messages from SendMessageRequest to ListTaskPushNotificationConfigsResponse (research/sources/a2a.proto); spec §3.1.1 to §3.1.11, §3.3.2, §3.3.4, §3.6.2, §5.3, §9.3, §11.3, §11.3.2, §11.5 (research/sources/specification.md); research/brief-spec.md sections 2.7, 2.8, 3.0, 7.5 and 12.2; research/migration.md section D.6 (N12); sdk src/a2a/server/routes/rest_routes.py at v1.2.2

R.2

Objects and fields

The proto defines every object in a request, a response, or a stream frame, and each field has one lowerCamelCase JSON name, one type, and a presence rule.

Bold rows name proto messages, the normative source for data objects, and the rows under each are its fields spec §1.4. Fields carry their lowerCamelCase JSON names spec §5.5, although the §5.5 example converts push_notification_config, a field the proto names task_push_notification_config (conflict D22).

Yes in the Req. column marks a REQUIRED field, which "MUST be present and set in valid messages" spec §5.7. A required array holds one element or more, yet an empty tasks list is legitimate (conflict D19). A type that starts with optional uses the proto keyword that records whether the field was set.

The tables leave out some optional fields. Task, Message, Part, Artifact, and both stream events carry a metadata object, and Message and Artifact carry extensions, a list of URIs. A part can name a filename, an artifact a description, a skill its examples, and a card a documentationUrl and an iconUrl. Every request message, from SendMessageRequest to GetExtendedAgentCardRequest, carries an optional tenant string that must match the chosen interface's tenant when that is set proto SendMessageRequest. Operations by binding lists the request and response messages.

Task and message objects

Message, Part, Artifact and chunks, Task, TaskStatus, and TaskState, SendMessage, and push notification configs define these objects.

FieldTypeReq.Meaning
TaskThe unit of work, created by the server
idstringyesMinted by the server
contextIdstringThe context. Both stream events require it.
statusTaskStatusyesThe state, status message, and time
artifactsrepeated ArtifactThe outputs. ListTasks needs includeArtifacts for them.
historyrepeated MessageThe kept messages, capped by historyLength
TaskStatusThe status of a task
stateTaskStateyesA value from Task states
messageMessageThe agent's note, such as a question
timestampgoogle.protobuf.TimestampWhen it was recorded, in ISO 8601 and UTC
MessageOne turn of conversation
messageIdstringyesMinted by the sender. Agents can use it to detect duplicates.
contextIdstringThe context. Every server message has it.
taskIdstringThe task. A client value names an existing task that matches contextId.
roleRoleyesROLE_USER from the client, ROLE_AGENT from the server
partsrepeated PartyesThe content, one part or more
referenceTaskIdsrepeated stringOther tasks the message refers to
PartExactly one of text, raw, url, and data
textstringText content
rawbytesFile content, base64 in JSON
urlstringA URL to the file content
datagoogle.protobuf.ValueAny JSON value
mediaTypestringThe media type, for every kind of part
ArtifactOne output of a task
artifactIdstringyesUnique in the task. Chunks match on it.
namestringA readable name, such as test-log.txt
partsrepeated PartyesThe content, one part or more
TaskStatusUpdateEventA change of task status
taskId, contextIdstringyesThe task and its context
statusTaskStatusyesThe new status
TaskArtifactUpdateEventA new artifact, or one chunk of it
taskId, contextIdstringyesThe task and its context
artifactArtifactyesThe artifact or the chunk
appendboolWhen true, add to the artifact with this artifactId
lastChunkboolWhen true, this is the last chunk
StreamResponseA frame or push body, one of four spec §3.2.3 spec §4.3.3
taskTaskThe task now. It opens task streams and subscriptions.
messageMessageA message-only stream holds one, then closes.
statusUpdateTaskStatusUpdateEventA change of task status
artifactUpdateTaskArtifactUpdateEventA new artifact or chunk
SendMessageConfigurationThe configuration of a send
acceptedOutputModesrepeated stringMedia types the client accepts
taskPushNotificationConfigTaskPushNotificationConfigA webhook to register, with no taskId
historyLengthoptional int32Unset means no limit, and 0 means none.
returnImmediatelyboolTrue returns once the task exists. The default waits.
TaskPushNotificationConfigA webhook for one task
tenantstringMust match the interface tenant, if set
idstringThe configuration id, assigned on create
taskIdstringThe task it belongs to
urlstringyesThe webhook URL
tokenstringA token for the task or session. No transport is defined.
authenticationAuthenticationInfoHow the agent authenticates: a required scheme, such as Bearer, and optional credentials

Agent card objects

AgentCard fields, Extended cards and signatures, Security schemes, and Extensions define these objects.

FieldTypeReq.Meaning
AgentCardWhat the agent offers, where, and what it demands
namestringyesA readable name, such as test-runner
descriptionstringyesWhat the agent does
supportedInterfacesrepeated AgentInterfaceyesWhere to reach it. The first entry is preferred.
providerAgentProviderThe provider: a required url and organization
versionstringyesThe agent's own version, such as 2.3.0
capabilitiesAgentCapabilitiesyesThe optional features it supports
securitySchemesmap<string, SecurityScheme>Accepted authentication schemes, by name
securityRequirementsrepeated SecurityRequirementScheme names mapped to scopes, such as {"schemes": {"bearer": {"list": []}}}
defaultInputModesrepeated stringyesMedia types accepted across all skills
defaultOutputModesrepeated stringyesMedia types produced across all skills
skillsrepeated AgentSkillyesWhat the agent does well
signaturesrepeated AgentCardSignatureJWS signatures over the canonical card
AgentInterfaceOne way to reach the agent
urlstringyesAn absolute HTTPS URL in production
protocolBindingstringyesJSONRPC, GRPC, HTTP+JSON, or a custom binding URI
tenantstringA routing value. When set, copy it into each request sent to this interface.
protocolVersionstringyesThe A2A version, such as 1.0
AgentCapabilitiesOptional features. An absent flag means false.
streamingoptional boolAllows SendStreamingMessage and SubscribeToTask
pushNotificationsoptional boolAllows the four push configuration operations
extensionsrepeated AgentExtensionThe protocol extensions it supports
extendedAgentCardoptional boolAllows GetExtendedAgentCard
AgentExtensionOne extension the agent supports, in capabilities.extensions
uristringThe URI that identifies the extension
descriptionstringHow this agent uses the extension
requiredboolWhen true, the client must understand and comply with the extension
paramsgoogle.protobuf.StructConfiguration for the extension, as a JSON object
AgentSkillOne thing the agent does well
idstringyesThe skill id, such as run-tests
namestringyesA readable name
descriptionstringyesWhat the skill does
tagsrepeated stringyesKeywords, one or more
inputModesrepeated stringInput media types that override the card's
outputModesrepeated stringOutput media types that override the card's
securityRequirementsrepeated SecurityRequirementRequirements for this skill

Names the proto does not define

Older material and stale pages of the docs still use these names, and none exists in the v1.0.1 proto (conflict D10).

NameUse instead
kind on a part or a stream eventThe member name, such as text or statusUpdate
TextPart, FilePart, DataPartOne Part with text, raw or url, or data
final on TaskStatusUpdateEventThe close of the stream
taskStatusUpdate, taskArtifactUpdatestatusUpdate, artifactUpdate
Task.createdAt, Task.lastModifiedstatus.timestamp
TaskArtifactUpdateEvent.indexartifactId, which chunks match on
PushNotificationConfig, CreateTaskPushNotificationConfigRequestTaskPushNotificationConfig, which the create operation takes as its request (conflict D4)
configId on a push configurationid
Card url, protocolVersion, preferredTransport, additionalInterfacessupportedInterfaces, with url and protocolVersion on each entry
Card supportsAuthenticatedExtendedCardcapabilities.extendedAgentCard
Card securitysecurityRequirements (conflict D6)
Card extensions at the top levelcapabilities.extensions (discrepancy N2)
cursor, limit, nextCursorpageToken, pageSize, nextPageToken

Sources:proto messages from Task to AgentSkill, with AgentExtension, AuthenticationInfo, AgentProvider, SecurityRequirement, and the request messages from SendMessageRequest to GetExtendedAgentCardRequest (research/sources/a2a.proto); spec §1.4, §3.1.4, §3.1.7, §3.2.2, §3.2.3, §3.3.4, §3.4.2, §4.3.3, §5.5, §5.6.1, §5.7, §A.2.1 (research/sources/specification.md); docs whats-new-v1 (research/sources/docs.md); research/brief-spec.md sections 2.0 to 2.8, 8.3 and 12.2; research/brief-docs.md; research/migration.md section D.6 (N2); capture/out/01-agent-cards.http

R.3

Task states

Nine values describe a task, and whether the current one is active, interrupted, or terminal tells the client what it can do next.

The proto marks four states terminal and two interrupted proto TaskState. Neither it nor the prose gives a transition table, so the agent decides the order. A blocking SendMessage returns at a terminal or interrupted state spec §3.2.2. Task, TaskStatus, and TaskState classifies the values and collects the transitions the kit makes. Interrupted states and CancelTask and terminal states explain the client's next step.

StateClassSet byThe client does next
TASK_STATE_UNSPECIFIEDnoneNobody: the zero valueTreat it, and any unknown name, as an error.
TASK_STATE_SUBMITTEDactiveThe server, on creationWait, poll, or subscribe.
TASK_STATE_WORKINGactiveThe agent, while it worksAs for a submitted task
TASK_STATE_INPUT_REQUIREDinterruptedThe agent, with a questionAnswer with a new message that carries the same taskId and contextId.
TASK_STATE_AUTH_REQUIREDinterruptedThe agent, for an authorizationPass the request to whoever approves it, outside A2A, and subscribe, register a webhook, or poll.
TASK_STATE_COMPLETEDterminalThe agent, when the work is doneRead the artifacts for the result. Follow up in a new task with referenceTaskIds.
TASK_STATE_FAILEDterminalThe agent, unable to do the workRead the status message, fix the input, and start a new task.
TASK_STATE_CANCELEDterminalThe agent, after CancelTaskStart a new task in the same context.
TASK_STATE_REJECTEDterminalThe agent, refusing the taskStop the loop, and follow the status message.

Until a task ends, CancelTask asks the server to stop it, and success is not guaranteed spec §3.1.5. A terminal task never changes. A message on its taskId and SubscribeToTask fail with UnsupportedOperationError, and CancelTask fails with TaskNotCancelableError spec §3.1.1 spec §3.1.5 spec §3.1.6. GetTask still returns it while the server keeps it spec §3.1.3.

A stream "MUST close when the task reaches a terminal state" spec §3.1.2. At an interrupted state the sources disagree (conflict D3). The kit closes its stream at TASK_STATE_INPUT_REQUIRED and keeps it open through TASK_STATE_AUTH_REQUIRED, as the kit README lists. So after any close, call GetTask and act on the state it returns.

Sources:proto TaskState (research/sources/a2a.proto); spec §3.1.1, §3.1.2, §3.1.3, §3.1.5, §3.1.6, §3.2.2, §3.3.2, §3.4.3, §7.6.1, §7.6.2 (research/sources/specification.md); docs life-of-a-task (research/sources/docs.md); research/brief-spec.md sections 4.1 to 4.5, 5.4 and 12.2; manuals/a2a-101/capture/README.md; capture/out/03-blocking-task.http, 04-polling.http, 05-streaming.http, 06-input-required.http, 07-auth-required.http, 08-rejected.http, 09-failed.http, 10-cancel.http

R.4

Errors

Every A2A error has a JSON-RPC code, an HTTP status, a gRPC status, and an ErrorInfo reason, and over HTTP the reason is what tells seven of them apart.

The nine A2A errors take their codes from the canonical table in §5.4 spec §5.4. Each also has an ErrorInfo reason: its name in upper snake case without Error, such as TASK_NOT_FOUND. The spec spells out only that one and TASK_NOT_CANCELABLE, and the reference SDK uses the same nine strings spec §11.6 sdk src/a2a/utils/errors.py.

ErrorJSON-RPCHTTPgRPC
TaskNotFoundError-32001404NOT_FOUND
TaskNotCancelableError-32002400FAILED_PRECONDITION
PushNotificationNotSupportedError-32003400FAILED_PRECONDITION
UnsupportedOperationError-32004400FAILED_PRECONDITION
ContentTypeNotSupportedError-32005400INVALID_ARGUMENT
InvalidAgentResponseError-32006500INTERNAL
ExtendedAgentCardNotConfiguredError-32007400FAILED_PRECONDITION
ExtensionSupportRequiredError-32008400FAILED_PRECONDITION
VersionNotSupportedError-32009400FAILED_PRECONDITION
JSONParseError-32700nonenone
InvalidRequestError-32600nonenone
MethodNotFoundError-32601nonenone
InvalidParamsError-32602400INVALID_ARGUMENT
InternalError-32603500 or 503INTERNAL or UNAVAILABLE
Authentication errorcustom401UNAUTHENTICATED
Authorization errorcustom403PERMISSION_DENIED

The last seven rows have no A2A code. Five are the standard JSON-RPC errors, whose messages are Invalid JSON payload, Request payload validation error, Method not found, Invalid parameters, and Internal error spec §9.5. §3.3.2 gives example HTTP and gRPC codes for two of them and for authentication and authorization errors spec §3.3.2. JSON-RPC reserves -32001 to -32099 for A2A errors and assigns only -32001 to -32009.

The official test kit, a2a-tck at tag 1.0.0.alpha2, keeps its own table and departs from §5.4 in six places. It expects HTTP 409 for TaskNotCancelableError, 415 for ContentTypeNotSupportedError, and 502 for InvalidAgentResponseError. It expects gRPC UNIMPLEMENTED for UnsupportedOperationError, PushNotificationNotSupportedError, and VersionNotSupportedError. A server that follows §5.4 fails those checks, and a2a-java matches the kit on UnsupportedOperationError. Conformance testing shows how to run the kit.

Operations by binding lists the operations that raise each A2A error, and none lists InvalidAgentResponseError spec §3.3.2. UnsupportedOperationError covers a terminal task, streaming that is off, and an extended card that is off, so read its message and metadata spec §3.1.1 spec §3.3.4. Servers "MUST NOT reveal the existence of resources the client is not authorized to access" spec §3.3.2, so answer a task the caller cannot access with TaskNotFoundError.

Every error carries a code, a message, and optional details, each with an @type key spec §3.3.2. JSON-RPC carries them in error.code, error.message, and error.data, with an HTTP status the spec does not set spec §9.5. The kit and the SDK send 200 OK sdk src/a2a/server/routes/jsonrpc_dispatcher.py. The gRPC binding sends google.rpc.Status, and HTTP+JSON sends its JSON form in an error object. For an A2A error, both hold an ErrorInfo with reason and the domain a2a-protocol.org spec §10.6 spec §11.6. Errors reads one of each.

Sources:spec §3.1.1 to §3.1.11, §3.3.2, §3.3.4, §5.4, §9.5, §10.6, §11.6 (research/sources/specification.md); research/brief-spec.md sections 7.1 to 7.5 and 12.1; sdk src/a2a/utils/errors.py and src/a2a/server/routes/jsonrpc_dispatcher.py at v1.2.2; a2a-tck tck/requirements/base.py and tests/compatibility/http_json/test_http_status.py at tag 1.0.0.alpha2 (research/conformance-tools.md); capture/out/10-cancel.http, 11-errors.http, 16-rest-binding.http

R.5

Glossary

Each term below has one meaning across the manual, and its link names the section that defines it.

TermMeaning in this manualDefined in
A2A errorOne of the nine errors the specification defines and maps into every binding, such as TaskNotFoundError5.5
Agent cardThe JSON document that describes an agent, usually at /.well-known/agent-card.json2.1
ArtifactAn output of a task, made of parts. A long one arrives in chunks.3.3
Blocking callA send that waits for a terminal or interrupted state, the default4.1
CapabilityA card flag, such as streaming, that allows a group of operations2.1
Card signatureAn entry in signatures: a JSON Web Signature over the card's RFC 8785 canonical form2.3
ContextA conversation that groups tasks and messages under one contextId3.5
Direct replyA Message that is the whole result of a send, with no task4.1
Extended cardA fuller card that GetExtendedAgentCard returns to an authenticated client2.3
ExtensionA protocol addition named by a URI, declared in capabilities.extensions and activated with A2A-Extensions6.2
HistoryThe messages a task keeps, capped by historyLength4.3
In-task authorizationAn agent's request for permission partway through a task. The task waits in TASK_STATE_AUTH_REQUIRED for an answer that arrives outside A2A.6.1
InterfaceOne entry in supportedInterfaces: a URL, a protocol binding, a protocol version, and an optional tenant2.1
Interrupted stateTASK_STATE_INPUT_REQUIRED or TASK_STATE_AUTH_REQUIRED. The task waits.4.2
MessageOne turn, with a messageId, a role, and parts3.1
OpaqueHidden from the caller. An opaque agent shows only what it sends back.1.1
PartThe smallest unit of content: one of text, raw, url, or data3.2
PollingCalling GetTask again and again to follow a task4.1
Protocol bindingOne concrete form of the operations: JSON-RPC, gRPC, or HTTP+JSON5.1
Push notificationAn HTTP POST of a StreamResponse to a webhook the client registered4.6
Remote agentThe agent behind an A2A endpoint that receives messages and runs tasks1.1
Security requirementOne entry in securityRequirements, which maps scheme names to the scopes a request needs6.1
Security schemeA named way to authenticate, declared in a card's securitySchemes6.1
Service parameterA per-request key, such as A2A-Version, sent as an HTTP header or as gRPC metadata5.4
SkillOne entry in a card's skills, such as run-tests2.1
Status messageThe one message a TaskStatus can hold, such as a question or a reason3.1
StreamServer-Sent Events, or a gRPC stream, of StreamResponse frames. SubscribeToTask opens one on a task.4.5
TaskThe unit of work a server creates for a message, with an id and a TaskState3.4
TenantAn optional routing string on an interface, copied into every request sent to that interface5.2
Terminal stateTASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, or TASK_STATE_REJECTED. The task never changes again.4.4

Sources:spec §1, §1.2, §1.3, §2.2, §3.2.2, §3.2.6, §3.4.1, §3.4.2, §3.7, §4.6, §7.3, §7.6.1, §8.2, §8.3.2, §8.4, §B (research/sources/specification.md); proto TaskState, Role, Message, Part, Artifact, TaskStatus, TaskPushNotificationConfig, AgentCard, AgentCapabilities, AgentExtension, AgentInterface, SecurityRequirement (research/sources/a2a.proto); the definition paragraphs of sections 1.1 to 6.2; capture/planner.py, capture/agents.py, capture/out/05-streaming.events.json

R.6

Sources

The manual rests on one pinned release, one reference SDK, five more SDKs, one test kit, and one capture kit, and it names every place where those sources disagree.

How to read this manual ranks the sources and describes the kit. This page pins their versions, lists the conflicts between them, and records what the project changed after the tag.

The pin

ItemValue
ReleaseA2A 1.0.1, tag v1.0.1 of github.com/a2aproject/A2A, commit 3303592588e388e62e0f69f701af531d2f4e3991, tagged 2026-05-28
Protocol version1.0 in every request and card. Facts verified 2026-10-06.
Main branchCommit 679ab3a of 2026-10-05, read for changes after the tag, and quoted only for rules the tag lacks
Reference SDKa2a-sdk 1.2.2, tag v1.2.2 of github.com/a2aproject/a2a-python, commit 2f9e44af243df5c7c7a3da361c8d095324bcd5e3, Apache License 2.0
Capture kitmanuals/a2a-101/capture/, standard library only, described in the kit README

The changelog dates the release 2026-05-26, and the spec banner still names 1.0.0 as the latest release (conflict D16). The manual cites the reference SDK as sdk and a path under src/a2a/, and never quotes it.

Vendored files

The files in research/sources/ are copied from the tag under the Apache License 2.0 in LICENSE. The audit checks every quote of 24 characters or more against them.

FileUpstreamCited as
a2a.protospecification/a2a.proto, 811 lines, normativeproto and a message or enum name
specification.mddocs/specification.md, 3610 linesspec and a section number
specification-main.mddocs/specification.md on main at 679ab3a, 3618 lines, unreleasedspec-main and a section number, only for rules added after the tag
docs.mdFourteen project pages in 3019 lines, not normative. This edition adds multi-tenancy, custom-protocol-bindings, and extension-and-binding-governance.docs and a page name

Section 1.4 of the spec gives the proto path as spec/a2a.proto, which is wrong (conflict D15). Two briefs index these files by line: research/brief-spec.md, with 40 findings and the register below, and research/brief-docs.md, which flags stale pages.

The SDKs and the test kit

Body sections compare six official SDKs and the official test kit, each read at one tag. The manual cites the reference SDK as sdk, and the others as sdk-go, sdk-java, sdk-js, sdk-dotnet, and sdk-rust, with a file path and the tag. It paraphrases them and never quotes them.

RepositoryCited asTagCommitDate
a2a-pythonsdkv1.2.22f9e44a2026-10-05
a2a-gosdk-gov2.6.0ebf17c52026-09-25
a2a-javasdk-javav1.4.0.Final64081942026-09-28
a2a-jssdk-jsv1.3.029417a52026-09-29
a2a-dotnetsdk-dotnetv1.0.0-preview287fd4482026-04-09
a2a-rssdk-rusta2a-server-lf-v0.5.132c31f62026-09-30
a2a-tcka2a-tck and a file path1.0.0.alpha229063fe2026-05-27

The conflict register

This table condenses section 12.2 of research/brief-spec.md, and D36 to D40 were found while writing the manual. The last column says what the A2A repository did about each one after the tag, checked on 2026-10-06. Fixed on main means merged and unreleased. Partly fixed means that a residual remains on main.

IDThe sources sayThe manual followsDiscussed inUpstream
D1Subscribe verb. Proto GET. §5.3 and §11.3.2 POST.The proto. Accept both.front, 4.5, 5.2, R.1open PR #2068
D2Blocking. §3.1.1 returns at once. §3.2.2 and the proto wait.§3.2.2 and the protofront, 4.1, 7.1open issue #2135
D3Streams at interrupted states. §3.1.2 and §3.1.6 close at terminal states. §11.7 and the streaming guide also close at interrupted ones. §7.6.1 keeps auth streams open.Close at terminal states, and expect either at interrupted onesfront, 1.2, 4.2, 4.5, R.3open PR #2270
D4Push configuration name. Prose PushNotificationConfig. Proto TaskPushNotificationConfig.The proto name4.6, R.2partly fixed on main (PR #1981)
D5Create request. Prose and Appendix A CreateTaskPushNotificationConfigRequest. Proto TaskPushNotificationConfig.The proto4.6, R.1partly fixed on main (PR #1981)
D6Card security field. §3.1.11, §13.3, and the §8.5 sample security. Proto securityRequirements.The protofront, 2.1, 2.3, 6.1, R.2partly fixed on main (PR #2046)
D7Extended card flag number. Appendix A.2.2 field 5. Proto field 4.The proto2.3no activity
D8Protocol version. Appendix A.2.1 protocolVersions on the card. Proto protocolVersion per interface.One per interface2.1fixed on main, unreleased (PR #2165)
D9Stream event names. Migration guide taskStatusUpdate, taskArtifactUpdate. Proto statusUpdate, artifactUpdate.The proto4.5, 7.4fixed on main, unreleased (PR #2056)
D10Fields the proto lacks. Migration guide Task.createdAt, Task.lastModified, configId, TaskArtifactUpdateEvent.index.Do not send themR.2, 7.4partly fixed on main (PR #2056)
D11Deprecated OAuth flows. Migration guide: implicit and password removed. Proto: kept, deprecated.Present, never used6.1no activity
D12Pagination names. Migration guide cursor, limit, nextCursor. Proto pageToken, pageSize, nextPageToken.The proto4.3fixed on main, unreleased (PR #2165)
D13Error body. §6.4 and §6.5 samples application/problem+json. Migration guide application/json. §11.6 google.rpc.Status.§11.65.5open PR #1641 and PR #1689
D14JSON-RPC method template. §9.3 category/action. §9.1 and §9.4 PascalCase.PascalCase5.1no activity
D15Proto path. §1.4 spec/a2a.proto. §5.7, §10.1, and the repository specification/a2a.proto.The repository1.1, Vendored filesno activity
D16Release and date. Banner: 1.0.0 is latest. Changelog 2026-05-26. Tag message 2026-04-23. Tag commit 2026-05-28.1.0.1, tagged 2026-05-28The pinopen PR #2072
D17Header registrations. §14.2.1 and §14.2.2 cite Section 3.2.5. Service parameters are §3.2.6.§3.2.65.4open issue #2305
D18TLS version. Spec 1.3 or later. Enterprise guide 1.2 or later.The spec6.1no activity
D19Empty required arrays. §5.7 wants one element at least. The §8.4.1 example has empty skills, and empty tasks is legitimate.Empty tasks for no results, a skill on every real card2.1, R.2open issue #2122
D20The typ header. §8.4.2 lists typ among the MUST fields and words it as SHOULD.Send alg, kid, and typ set to JOSE2.3no activity
D21Signing steps. §8.4.2 and §8.4.3 remove default values. §8.4.1 keeps REQUIRED and set optional fields.§8.4.12.3open issue #2249
D22JSON naming example. §5.5 push_notification_config. Proto task_push_notification_config.The protoR.2no activity
D23Samples that break REQUIRED. Some omit messageId, artifactId, or an event's contextId.Never copy a sample as it is3.1, 6.2partly fixed on main (PR #2083)
D24Invalid JSON. The samples in §4.6.1, §6.7, and §9.4.1.Never copy a sample as it is3.2, 6.2open PR #1657
D25Layer 2 diagram. §1.3 shows Get Agent Card. §3.1 defines only Get Extended Agent Card.The diagram is informal1.1no activity
D26Mandatory operations. §3.1 requires them all. §3.3.4 lets flags decline streaming, push, and the extended card.The capability flags2.2no activity
D27Transport in a guide. Core Concepts: JSON-RPC 2.0 for everything. Spec: three bindings.The spec5.1no activity
D28Who creates the context id. Core Concepts: the server. Spec: a client can propose one.The spec3.5open issue #1317
D29States from extensions. The extensions guide lists extensions that add states, and forbids new enum values.Neither is normative. Add no states.3.4, 6.2no activity
D30Extension activation. §4.6.1 JSON-RPC parameters. §9.2 HTTP headers.The header5.4no activity
D31Version as a parameter. §3.6.1 allows a request parameter. §9.2 and §11.2 require headers.The header5.4open issue #2184
D32Old endpoint names. The what-is-a2a diagram: POST /sendMessage, /.well-known/agent-card.message:send, agent-card.json2.2fixed on main, unreleased (PR #2261)
D33Stream type in a guide. Streaming guide SendStreamingMessageResponse. Spec StreamResponse.The spec4.5no activity
D34gRPC interface URL. Proto: an absolute HTTPS URL. gRPC dials a host and a port.No rule at the tag2.1, 5.3fixed on main, unreleased (PR #1997)
D35Role spelling. §2.2 user and agent. Proto and §5.5 ROLE_USER, ROLE_AGENT.The enum names3.1no activity
D36Another client's task. §3.3.2 calls it an authorization error and forbids revealing that it exists.TaskNotFoundError6.1no activity
D37A 0.3 state name. §3.4.3 input-required. Proto TASK_STATE_INPUT_REQUIRED.The proto name3.4open PR #2155
D38Cancel transitions. Migration guide: clarified in 1.0. Spec: no transition table.Transitions are implementation behavior3.4open issue #1992
D39Version in samples. §9.2, §11.2, and §14.2.1 send A2A-Version: 0.3.Send 1.05.4no activity
D40Old extended card flag. Appendix A.2.2 supportsExtendedAgentCard. Migration guide and SDK supportsAuthenticatedExtendedCard.extendedAgentCard in capabilities2.3, 7.4no activity

Discrepancies found after the register

Research for this edition found twenty more places where a docs page, an appendix, or a sample disagrees with the proto or the spec. None changes what the manual follows. The last column says whether main still has the text, as checked on 2026-10-06.

IDThe sources sayThe manual followsDiscussed inOn main
N1Stream event kind in the guide. Migration guide, for 0.3: kind values taskStatusUpdate and taskArtifactUpdate. Appendix A.2.1 and the 0.3 schema: status-update and artifact-update.Appendix A.2.1R.6 onlyfixed
N2Extensions path. Migration guide agentCard.extensions. Proto capabilities.extensions.The proto6.2, R.2unchanged
N3Tenant scope. Migration guide: tenant scoping in gRPC requests. Proto: tenant on every request message, and a /{tenant} route for every rpc.The proto5.2unchanged
N4ListTasks in 0.3. Migration guide: not available in 0.3. The 0.3 prose lists tasks/list, and the 0.3 proto and schema do not.The guide, read as not in the 0.3 schemaR.6 onlyno change needed
N5Upgrade headings. Appendix A.2.1 says pre-0.3.x under the heading about the 1.0 change.Read as pre-1.0R.6 onlyunchanged
N6Removal timeline. Appendix A sets removal at 0.5.0 or later for names that 1.0 renamed, and calls the timeline an example.§1.4: the next major releaseR.6 onlyunchanged
N7Removed card field name. Appendix A.2.2 supports_extended_agent_card, field 13. The 0.3 proto supports_authenticated_extended_card, field 13.The 0.3 protoR.6 onlyunchanged
N8Version in the §10.2 sample. The gRPC sample sends a2a-version 0.3, which D39 does not list.Send 1.05.3unchanged
N9Config id name. Migration guide config_id. Proto id.The protoR.2fixed
N10Extension lists as new fields. Migration guide adds Message.extensions and Artifact.extensions in 1.0. Both exist in the 0.3 proto.Not newR.6 onlyunchanged
N11Mutual TLS as new. Migration guide adds mutual TLS in 1.0. The 0.3 proto has mtls_security_scheme.Not newR.6 onlyunchanged
N12Tenant routes. §5.3, §11.3, and §11.5 list no /{tenant} route. The proto adds one to every rpc.The proto5.2, R.1unchanged
N13Tenant field number. ListTaskPushNotificationConfigsRequest.tenant is field 4. Every other request has field 1.No conflict. Visible in gRPC only.R.6 onlyunchanged
N14filename on any part. Migration guide: filename on every part. Proto comment: for the file.The proto allows it on any part. Use it for files.3.2unchanged
N15Create request in Appendix A. The table names CreateTaskPushNotificationConfigRequest as current. It is the 0.3 type.The proto, as D54.6unchanged
N16Operation aliases. Migration guide: aliases during the transition. The proto and the method set define none. SDK compatibility layers do.The spec. Name the SDK layer.R.6 onlyunchanged
N17The flipped default. Migration guide shows returnImmediately as new. 0.3 blocking defaulted to false, no wait. 1.0 returnImmediately defaults to false, a wait.§3.2.2 and the proto4.1unchanged
N18Card sample in the extensions guide. A top-level url, the 0.3 shape.supportedInterfacesR.6 onlyfixed
N19A gRPC-specific wrapper. §10.5.1 calls TaskPushNotificationConfig a gRPC resource type. The proto uses it in every binding.The protoR.6 onlyunchanged
N20Security in the tenancy guide. The multi-tenancy page securitySchemes and security. Proto securityRequirements.The proto, as D6R.6 onlyunchanged

After v1.0.1

The manual checked main of the A2A repository at commit 679ab3a, dated 2026-10-05, and found 73 commits after the tag. One changes the proto: PR #1997 lets a gRPC interface url be a hostname:port address, which settles D34. It is the only entry in the pending 1.0.2 release, PR #2072, still open.

PR #2081 added §7.6.4, In-Task Authorization Scope, which the manual quotes with the key spec-main. Branch dev-1.1, at commit db39eb5 of 2026-09-22, holds the 1.1 work: a generation field and a task timeline. The approved fixes for D1 and D3 target that branch and are still open. No tag newer than v1.0.1 existed on 2026-10-06.

Sources:manual.json pin, sources and quoteSources; research/sources/README.md and LICENSE; spec §1.4, §3.6, §5.7, §10.1 (research/sources/specification.md); research/sources/specification-main.md §7.6.4 (main at 679ab3a); research/brief-spec.md sections 1.1, 12.2 and 14; research/brief-docs.md; research/conflicts-upstream.md, research/migration.md section D.6, research/sdk-splits.md section 1, research/conformance-tools.md (research of 2026-10-06); a2aproject/A2A main at 679ab3a, branch dev-1.1 at db39eb5, and the PRs and issues in the Upstream column, checked 2026-10-06; the a2a-python checkout at tag v1.2.2 (pyproject.toml, LICENSE); the SDK and a2a-tck repositories at the tags in the SDK table; manuals/a2a-101/capture/README.md, capture/run.py

R.7

Index of figures

Every figure in the manual, by the claim it makes.

Each number links to its figure, and the claim is the bold sentence that opens the caption.

Fig.The claim it makes
0.1Each of the seven arrow styles marks one kind of exchange, and the label on each arrow is a real name from a run of the kit.
1.1A2A defines the card, the bindings, the operations, and the objects, while test-runner's server parts and agent logic below the dashed line belong to its owner.
1.2A2A fixes the messages, states, and events between agents, while trust, discovery, retention, and exactly-once effects stay with the host.
1.3A streamed task opens with the task itself, reports its work as status and artifact frames, and ends when the server closes the stream after the terminal status.
2.1One GET returns what a client needs before its first message: who the agent is, where to reach it, what it demands, and what it offers.
2.2Three routes lead to one card, and a client takes the first interface it speaks and checks a capability flag before each optional operation.
2.3The same GetExtendedAgentCard call fails with HTTP 401 before any A2A method runs, then returns a card with a second skill once a token is attached.
2.4The signature covers the RFC 8785 bytes of the card without signatures, so an edit to the description changes those bytes and verification fails.
3.1A message carries an id its sender mints, while its contextId and taskId come from the server and the client echoes them to continue.
3.2The kit's captures use all four kinds of part, and only the image/png part fails, because code-reviewer does not accept that media type.
3.3The five chunks of test-log.txt become five parts of one stored artifact in frame order, and lastChunk ends that artifact while the stream continues.
3.4A task carries its own id, its context, one current status, its artifacts, and its history, and every message and artifact inside it is made of parts.
3.5The kit's captures show nine transitions between task states, and none of them leaves a double-bordered terminal state.
3.6The review task holds both exchanges of the conversation, and a follow-up review joins the same context as a new task, because the review task is terminal.
4.1The same SendMessage to code-reviewer returns a message for a question and a task for a diff, and only the task gives the client something to follow.
4.2A blocking send holds one request open until this task completes, while returnImmediately answers at TASK_STATE_SUBMITTED and leaves the client to poll.
4.3The question arrives as the status message of a paused task, and the answer is a new message that carries the same task and context ids.
4.4The deploy waits in TASK_STATE_AUTH_REQUIRED while the stream stays open, and the operator's approval travels outside A2A before the same stream carries the result.
4.5GetTask trims history to the newest messages you ask for, and ListTasks returns newest-first pages that leave out artifacts until you ask for them.
4.6The agent chose three of these endings and the client one, and once a task ends, the server refuses any further message or cancel.
4.7A dropped stream loses the frames sent while it was closed, and SubscribeToTask resumes with a Task snapshot that already holds their content.
4.8With a webhook registered, test-runner posts eight StreamResponse bodies to the planner's receiver after the SendMessage call has already returned.
5.1JSON-RPC posts every operation to one URL and names it in the body, HTTP+JSON names it with a verb and a path, and gRPC calls an rpc.
5.2One rpc sends the request with its metadata, the server streams StreamResponse messages until the task ends, and the rpc status closes the stream.
5.3The same TaskNotFoundError arrives as HTTP 200 with code -32001 in JSON-RPC and as HTTP 404 in HTTP+JSON, carrying the same ErrorInfo.
6.1The card names the scheme, HTTP rejects a request without the token before A2A runs, and a running task can still stop for an approval.
6.2The card lists the extension, the request names it in A2A-Extensions and keys its data by the URI, and a missing required extension answers -32008.
6.3Data crosses six boundaries in the kit's system, and each needs a check of its own, because A2A labels nothing it carries as safe.
7.1The planner reads three cards, delegates three skills in turn, answers a question, escalates an approval, and decides from data parts before each next step.
7.2A request passes authentication, the version check, and the capability check before Agent.send stores a task, and every event then leaves through one broadcast.
7.3The planner sees only each agent's card and endpoint, while each agent calls its own logic and tools below a line that A2A never crosses.

Sources:the figure blocks in front.md and in every file under sections/; the figure sources in figures/src/ (manuals/a2a-101)

About this edition

A2A 101, edition 2026.10, by Rohit Ghumare. It is part of AI Engineering from Scratch, an open source course at aiengineeringfromscratch.com. The newest edition is always at aiengineeringfromscratch.com/manual-a2a-101.html.

A2A protocol 1.0.1 is the subject, at commit 3303592 of 2026-05-28, checked on 2026-10-06. Every listing comes from a recorded run of the capture kit in the manual's source directory, and every quoted rule is checked word for word against the vendored sources.

© 2026 Rohit Ghumare · MIT license. You can copy and share this manual. Keep this page and the copyright line with it. Report an error at github.com/rohitg00/ai-engineering-from-scratch/issues.