A2A 101
The Agent2Agent protocol, from its purpose to each request and response
- The Protocol on One Page
- AgentCard
- Data Model
- Operations
- Bindings
- Security and Extensions
- Implementing A2A
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.
| Rank | Source | What the manual takes from it | Cited as |
|---|---|---|---|
| 1 | specification/a2a.proto at v1.0.1 | every object, field, enum value, and method | proto and a message or enum name |
| 2 | docs/specification.md at v1.0.1 | every behavior rule, quoted word for word | spec and a section number |
| 3 | the project's documentation pages at v1.0.1 | the project's own framing, flagged where a page is out of date | docs and a page name |
| 4 | the reference SDK, a2a-python 1.2.2 | what a production implementation does where the specification is silent | sdk and a source file |
| 5 | the capture kit in capture/ | every request, response, and stream frame the manual shows | the 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.
| Agent | Port | What it does | What its runs show |
|---|---|---|---|
test-runner | 41241 | runs a test suite and streams the log | streaming, artifacts in chunks, polling, cancel, push notifications, both JSON bindings |
code-reviewer | 41242 | reviews a diff against a base branch | direct message replies, TASK_STATE_INPUT_REQUIRED, rejected content types, no push support |
deployer | 41243 | deploys a build to staging after an operator approves | bearer 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:
Host: localhost:41241
Content-Type: application/json
A2A-Version: 1.0
Accept: text/event-stream
…
Content-Type: text/event-stream
{"jsonrpc": "2.0", "id": 1, "result": {"task": {"id": "728ba084-97eb-422b-b94b-b0fe9153ce2c", … "state": "TASK_STATE_SUBMITTED", …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:
| Part | What it covers |
|---|---|
| 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. |
| 2 · AgentCard | Before a client sends a message, the AgentCard tells it what the agent offers, where to reach it, and what it requires. |
| 3 · Data Model | Every A2A exchange is built from five objects: Message, Part, Artifact, Task, and the contextId that groups tasks. |
| 4 · Operations | Eleven operations create, read, cancel, and follow tasks, and each one has rules that a client must know before it calls. |
| 5 · Bindings | The same operations travel over JSON-RPC, HTTP+JSON, and gRPC, with two headers and one error model shared by all three. |
| 6 · Security and Extensions | The card declares how to authenticate and which extensions apply, and the specification lists what each side must check. |
| 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. |
| R · Reference | Operations 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:
| violet | the client agent and what it sends |
| blue | remote agents, their cards, and the messages they send back |
| green | parts and artifacts: the content a task produces |
| amber | tasks, task states, and transitions |
| teal | the server's stored task records |
| indigo | streams, subscriptions, and push notifications |
| plum | the model or logic inside an agent, which A2A never shows |
| olive | tools and systems an agent calls on its own: CI, Git, MCP servers |
| rose | errors, failure, rejection, and cancellation |
| grey | the 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.
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
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.
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).
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.
The specification also contradicts itself in ten places that change your code, listed below and settled in the sources reference.
| Conflict | What disagrees |
|---|---|
| D1 | the HTTP verb for SubscribeToTask |
| D2 | whether a plain SendMessage waits |
| D3 | whether a stream closes at an interrupted state |
| D4 and D5 | the names of the push configuration objects |
| D6 | the name of the card's security field |
| D11 | deprecated OAuth flows |
| D12 | the page field names of ListTasks |
| D13 | the HTTP+JSON error body |
| D36 | the 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
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.
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:
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 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:
"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 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
artifactIdtest-log, thenametest-log.txt, and the first part. It has noappend, so it starts the artifact. - Frames 4, 5, and 6 repeat the
artifactId, leave out thename, and setappend: true. - Frame 7 sets both
append: trueandlastChunk: true, so the log is complete. - Frame 8 starts a second artifact,
summary, in one chunk: a data part with the media typeapplication/json, andlastChunk: 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
AgentCard
Before a client sends a message, the AgentCard tells it what the agent offers, where to reach it, and what it requires.
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.
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.
"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"
]
}
]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
"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 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
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.jsonfrom 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.
### 1 · discover test-runner
Host: localhost:41241
Accept: application/json
Content-Type: application/json
{
"name": "test-runner",
…
}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.
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 capabilities | Operations it allows | Error when the flag is absent or false |
|---|---|---|
streaming | SendStreamingMessage, SubscribeToTask | UnsupportedOperationError, -32004 |
pushNotifications | CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfig | PushNotificationNotSupportedError, -32003 |
extendedAgentCard | GetExtendedAgentCard | UnsupportedOperationError, -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:
### 6 · CreateTaskPushNotificationConfig: not supported
Host: localhost:41242
…
"method": "CreateTaskPushNotificationConfig",
"params": {
"taskId": "00000000-0000-4000-8000-000000000000",
…
…
"code": -32003,
"message": "Push notifications are not supported",
…
"reason": "PUSH_NOTIFICATION_NOT_SUPPORTED",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
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.
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.### 2 · GetExtendedAgentCard: with a token
…
Authorization: Bearer dpl_test_7c1e4b
…
…
{
"id": "rollback",
"name": "Roll back a deploy",
"description": "Return staging to the previous build. Shown only to authenticated callers.",
"tags": [
"deploy",
"rollback"
]
}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.
| SDK | Who receives the extended card without a host check |
|---|---|
| Python 1.2.2 | Any 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.0 | Any caller. A CallInterceptor or the card producer can reject the call sdk-go a2asrv/handler.go at v2.6.0. |
| Java v1.4.0.Final | Any 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.0 | An 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-preview2 | Nobody 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.1 | Any 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:
- Field presence. "Fields marked with
REQUIREDMUST always be present, even if the field value matches the default." spec §8.4.1 A field with theoptionalkeyword stays whenever it was set, and any other field at its default value is dropped. - RFC 8785. The JSON Canonicalization Scheme sorts object keys, fixes one form for each value, and removes whitespace spec §8.4.1.
- No signatures. "The
signaturesfield 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.
# 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: FalseThe 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.
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
Data Model
Every A2A exchange is built from five objects: Message, Part, Artifact, Task, and the contextId that groups tasks.
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.
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:
"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."
}
]
}
}To continue a task, the client copies both ids from the task into its next message, as the planner's answer does:
"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"
}
]
}Three rules govern those two ids:
- A client
taskIdmust name a live task. "When a client includes ataskIdin a Message, it MUST reference an existing task" spec §3.4.2. An unknown id getsTaskNotFoundError, and CancelTask and terminal states covers a finished task. - The two ids must agree. "Agents MUST reject messages containing mismatching
contextIdandtaskId" spec §3.4.3. The spec names no error for this, and both the kit and the reference SDK answer-32602sdk src/a2a/server/request_handlers/default_request_handler_v2.py. - The context follows the task. "Agents MUST infer
contextIdfrom the task if onlytaskIdis 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.
| SDK | Message with only taskId | Same messageId sent twice to one task |
|---|---|---|
| Python 1.2.2 | Does not read the task, and stamps a fresh contextId on the message sdk src/a2a/server/agent_execution/context.py | Accepted. The history skips a second copy, and the agent runs again sdk src/a2a/server/agent_execution/active_task.py |
| Go v2.6.0 | The agent context gets the task's contextId, and the stored message stays as sent sdk-go a2asrv/agentexec.go at v2.6.0 | Accepted. The history skips a second copy, and the agent runs again sdk-go a2asrv/agentexec.go at v2.6.0 |
| Java v1.4.0.Final | The 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.Final | Accepted. 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.0 | The 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.0 | Accepted. 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-preview2 | The 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-preview2 | Accepted. 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.1 | The 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.1 | Accepted. 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:
referenceTaskIdslists "task IDs that this message references for additional context" proto Message.extensionslists "The URIs of extensions that are present or contributed to this Message" proto Message, and extensions covers them.metadatais 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
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:
textholds a string.rawholds the bytes of a file. "In JSON serialization, this is encoded as a base64 string." proto Parturlholds "Aurlpointing to the file's content" proto Part.dataholds "Arbitrary structureddataas 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:
"parts": [
{
"text": "Review this diff of payments-api."
},
{
"raw": "LS0tIGEvcGF5bWVudHMvcmVmdW5kcy5weQor…",
"filename": "refunds.diff",
"mediaType": "text/x-diff"
}
]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.
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:
| Field | Lives in | Set by | code-reviewer in the kit |
|---|---|---|---|
defaultInputModes | the agent card | the agent | text/plain, text/x-diff |
defaultOutputModes | the agent card | the agent | text/plain, application/json |
inputModes, outputModes | each skill on the card | the agent | review-diff takes text/x-diff and text/plain |
acceptedOutputModes | configuration of a send | the client | not sent anywhere in the kit |
mediaType | each part | whoever sends the part | text/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:
"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"
}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
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.
{…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "name": "test-log.txt", "parts": [{"text": "collected 12 items\n"}]}}}}
{…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "tests/test_charges.py ........ [ 66%]\n"}]}, "append": true}}}
…
{…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "1 failed, 11 passed in 4.21s\n"}]}, "append": true, "lastChunk": true}}}
{…"artifactUpdate": {…"artifact": {"artifactId": "summary", "name": "summary.json", "parts": [{"data": {"passed": 11, "failed": 1, …}, "mediaType": "application/json"}]}, "lastChunk": true}}}Figure 3.3 lines up all nine frames of the run with what each artifact frame did to the stored task.
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:
| Implementation | append: true for an unknown artifactId | Reads lastChunk |
|---|---|---|
| a2a-python | raises InvalidAgentResponseError sdk src/a2a/server/tasks/task_manager.py | no |
| a2a-go | an error, and the task ends in TASK_STATE_FAILED with no cause sdk-go a2aevent/event.go at v2.6.0 | no |
| a2a-java | drops the chunk, logs a warning, and keeps the task unchanged sdk-java sdk/spec/util/Utils.java at v1.4.0.Final | no |
| a2a-js | stores the chunk as a new artifact sdk-js src/server/result_manager.ts at v1.3.0 | no |
| a2a-dotnet | stores the chunk as a new artifact sdk-dotnet src/A2A/Server/TaskProjection.cs at v1.0.0-preview2 | no |
| a2a-rs | returns InvalidAgentResponseError, and the task keeps its last state sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1 | no |
| the kit | starts 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
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.
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.
| Value | Number | Class | In the kit |
|---|---|---|---|
TASK_STATE_UNSPECIFIED | 0 | none | never sent |
TASK_STATE_SUBMITTED | 1 | active | every new task, 04-polling.http |
TASK_STATE_WORKING | 2 | active | the suite runs, 05-streaming.http |
TASK_STATE_COMPLETED | 3 | terminal | the tests end, 03-blocking-task.http |
TASK_STATE_FAILED | 4 | terminal | commit deadbee, 09-failed.http |
TASK_STATE_CANCELED | 5 | terminal | after CancelTask, 10-cancel.http |
TASK_STATE_INPUT_REQUIRED | 6 | interrupted | the base branch question, 06-input-required.http |
TASK_STATE_REJECTED | 7 | terminal | a production deploy, 08-rejected.http |
TASK_STATE_AUTH_REQUIRED | 8 | interrupted | waiting 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-requiredstate" spec §3.4.3. That is the 0.3 name ofTASK_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.
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
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.
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
Operations
Eleven operations create, read, cancel, and follow tasks, and each one has rules that a client must know before it calls.
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:
| SDK | configuration.historyLength on the returned task |
|---|---|
| a2a-python 1.2.2 | applied 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-go | never applied, so the task returns with its full stored history sdk-go a2asrv/handler.go at v2.6.0 |
| a2a-java | applied 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-js | applied 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-dotnet | never 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-rs | applied 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.
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:
"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 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.
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:
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 taskThe 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
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:
"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 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.
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:
"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 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.
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:
| SDK | When the server closes a SendStreamingMessage stream |
|---|---|
| a2a-python 1.2.2 | when the agent's execute() call returns, so the agent code decides sdk src/a2a/server/agent_execution/active_task.py |
| a2a-go | after 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-java | after 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-js | after 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-dotnet | when 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-rs | after 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
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:
"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"
}
]
}
],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:
| SDK | historyLength unset | 0 | below 0 |
|---|---|---|---|
| a2a-python 1.2.2 | all history in both operations | an empty history array | InvalidParamsError sdk src/a2a/utils/task.py |
| a2a-go | all history in GetTask, the last 100 messages in ListTasks | the field is omitted | the field is omitted sdk-go a2asrv/handler.go at v2.6.0 |
| a2a-java | all history in GetTask, none in ListTasks | an empty list | InvalidParamsError sdk-java requesthandlers/DefaultRequestHandler.java at v1.4.0.Final |
| a2a-js | all history in both operations | the field is omitted | omitted over JSON-RPC, HTTP 400 over REST sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0 |
| a2a-dotnet | all history in both operations | an empty array in GetTask, no field in ListTasks | InvalidParamsError over JSON-RPC sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2 |
| a2a-rs | all history in both operations | an empty history | an 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:
"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": 5The 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.
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:
| SDK | Token | Page size | Last page |
|---|---|---|---|
| a2a-python 1.2.2 | URL-safe base64 of a JSON cursor with the status timestamp and the task id sdk src/a2a/utils/task.py | 50 by default, 1 to 100 or error -32602 | "" |
| a2a-go | URL-safe base64 of the update time and the task id sdk-go a2asrv/taskstore/inmemory.go at v2.6.0 | 50 by default, 1 to 100 or error -32600 | "" |
| a2a-java | plain text, the status time in epoch milliseconds, a colon, and the task id sdk-java util/PageToken.java at v1.4.0.Final | 50 by default, 1 to 100 or error -32602 | "" |
| a2a-js | base64 of the status timestamp, a bar, and the task id sdk-js src/server/utils.ts at v1.3.0 | 50 by default, 1 to 100 or error -32602 | "" |
| a2a-dotnet | a decimal offset such as 50 sdk-dotnet src/A2A/Server/InMemoryTaskStore.cs at v1.0.0-preview2 | 50 by default, and only JSON-RPC rejects a size outside 1 to 100 | "" |
| a2a-rs | a 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.1 | 50 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
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.
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:
"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,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:
| SDK | JSON-RPC | HTTP+JSON | gRPC |
|---|---|---|---|
| a2a-python 1.2.2 sdk src/a2a/server/agent_execution/active_task.py sdk src/a2a/utils/errors.py | -32004 | 400 | FAILED_PRECONDITION |
| a2a-go sdk-go a2asrv/agentexec.go at v2.6.0 | -32004 | 400 | FAILED_PRECONDITION |
| a2a-java sdk-java server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java at v1.4.0.Final | -32004 | 400 | UNIMPLEMENTED |
| a2a-js sdk-js src/server/request_handler/default_request_handler.ts at v1.3.0 | -32004 | 400 | FAILED_PRECONDITION |
| a2a-dotnet sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2 | -32004 | 400, a problem-details body | none at this release |
| a2a-rs sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1 | -32004 | 400 | FAILED_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
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.
Content-Type: text/event-stream
{"jsonrpc": "2.0", "id": 1, "result": {"task": {… "state": "TASK_STATE_SUBMITTED", …
{"jsonrpc": "2.0", "id": 1, "result": {"statusUpdate": {… "state": "TASK_STATE_WORKING", …
{"jsonrpc": "2.0", "id": 1, "result": {"artifactUpdate": {… "name": "test-log.txt", …
{"jsonrpc": "2.0", "id": 1, "result": {"artifactUpdate": {… "append": true}}}
# the client closes the connection after 4 eventsThe 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
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.
{
"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"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:
| SDK | Server accepts | Client sends |
|---|---|---|
| a2a-python 1.2.2 sdk src/a2a/server/routes/rest_routes.py | GET and POST | POST |
| a2a-go sdk-go a2asrv/rest.go at v2.6.0 | GET and POST | POST |
| a2a-java sdk-java reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java at v1.4.0.Final | POST | POST |
| a2a-js sdk-js src/server/express/rest_handler.ts at v1.3.0 | GET and POST | POST |
| a2a-dotnet sdk-dotnet src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs at v1.0.0-preview2 | POST | POST |
| a2a-rs sdk-rust a2a-server/src/rest.rs at a2a-server-lf-v0.5.1 | GET and POST | GET |
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
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).
…
"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"
}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.
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 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:
| SDK | Content-Type | Token header | Timeout | Retries |
|---|---|---|---|---|
| a2a-python 1.2.2 sdk src/a2a/server/tasks/base_push_notification_sender.py | application/json | X-A2A-Notification-Token | set by the httpx client the server passes in | none |
| a2a-go sdk-go a2asrv/push/sender.go at v2.6.0 | application/json | A2A-Notification-Token | 30 s | none |
| a2a-java sdk-java server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java at v1.4.0.Final | application/json | X-A2A-Notification-Token | none set | none |
| a2a-js sdk-js src/server/push_notification/default_push_notification_sender.ts at v1.3.0 | application/a2a+json | X-A2A-Notification-Token, only when authentication is empty | 5 s | none |
| a2a-dotnet sdk-dotnet src/A2A/Server/A2AServer.cs at v1.0.0-preview2 | no sender: every config operation returns -32003 | |||
| a2a-rs sdk-rust a2a-server/src/push/sender.rs at a2a-server-lf-v0.5.1 | application/json | A2A-Notification-Token | 30 s | none |
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
Bindings
The same operations travel over JSON-RPC, HTTP+JSON, and gRPC, with two headers and one error model shared by all three.
- 5.1JSON-RPC
- 5.2HTTP+JSON
- 5.3gRPC
- 5.4A2A-Version and A2A-Extensions
- 5.5Errors
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:
Content-Type: application/json
…
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "d95bafc8-f2a4-427b-9cf4-bb99f4bea973",
"role": "ROLE_USER",
…
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",
…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.
Content-Type: text/event-stream
{"jsonrpc": "2.0", "id": 1, "result": {"task": {"id": "728ba084-97eb-422b-b94b-b0fe9153ce2c", …
{"jsonrpc": "2.0", "id": 1, "result": {"statusUpdate": {"taskId": "728ba084-97eb-422b-b94b-b0fe9153ce2c", …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
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.
A2A-Version: 1.0
Content-Type: application/a2a+json
{
"message": {
"messageId": "48f165d5-7b00-47f4-b81e-f86f5c8cc1ab",
"role": "ROLE_USER",
…
Content-Type: application/a2a+json
{
"task": {
"id": "bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3",
"contextId": "41c0d462-fed8-4f6b-8569-6c9afe9497c3",
…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.
"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
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:
// 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: "*"
}
};
}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.
SDK support
| SDK | gRPC server | gRPC client | How to enable it |
|---|---|---|---|
| a2a-python 1.2.2 | GrpcHandler sdk src/a2a/server/request_handlers/grpc_handler.py | GrpcTransport sdk src/a2a/client/transports/grpc.py | pip 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.0 | grpcService, Node only sdk-js src/server/grpc/grpc_service.ts at v1.3.0 | GrpcTransport | add GrpcTransportFactory, which the default factory omits sdk-js src/client/factory.ts at v1.3.0 |
| a2a-go 2.6.0 | a2agrpc/v1.NewHandler sdk-go a2agrpc/v1/handler.go at v2.6.0 | a2agrpc/v1.WithGRPCTransport | add the transport option, which the default client omits sdk-go a2aclient/factory.go at v2.6.0 |
| a2a-java 1.4.0.Final | transport/grpc and the Quarkus reference/grpc server | client/transport/grpc | add the Maven modules sdk-java README.md at v1.4.0.Final |
| a2a-dotnet 1.0.0-preview2 | none, only the name constant sdk-dotnet src/A2A/Client/ProtocolBindingNames.cs at v1.0.0-preview2 | none | A2A.Grpc.AspNetCore and A2A.Grpc exist on main, not yet released |
| a2a-rs, a2a-grpc 0.3.9 | GrpcHandler in the a2a-grpc crate | GrpcTransport in the same crate | register 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
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:
Host: localhost:41241
Content-Type: application/json
…
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"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
| Implementation | Missing or empty header | Accepted values | Where it checks |
|---|---|---|---|
kit, a2a_ref.py | refused as 0.3 (no header) | exactly 1.0, from the header or the query parameter | JSON-RPC and HTTP+JSON |
| a2a-python 1.2.2 | read as 0.3, then refused | 1, 1.0, 1.0.0, and 1.5, because only the major must be 1 sdk src/a2a/utils/version_validator.py | JSON-RPC and HTTP+JSON, never gRPC sdk src/a2a/server/request_handlers/grpc_handler.py |
| a2a-js 1.3.0 | read as 0.3, refused unless the card lists it | only the strings the card declares for that binding, so 1.0.0 is refused sdk-js src/server/version.ts at v1.3.0 | all three bindings |
| a2a-java 1.4.0.Final | read as 0.3, then refused | the 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.Final | all three, with gRPC UNIMPLEMENTED instead of FAILED_PRECONDITION |
| a2a-go 2.6.0 | accepted | anything, because no server code reads the header sdk-go a2a/svcparams.go at v2.6.0 | nowhere |
| a2a-dotnet 1.0.0-preview2 | accepted as 1.0 | 1.0 or 0.3, with 0.3 processed as 1.0 sdk-dotnet src/A2A.AspNetCore/A2AJsonRpcProcessor.cs at v1.0.0-preview2 | JSON-RPC only |
| a2a-rs, a2a-server 0.5.1 | read as 0.3, then refused | major 1 sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1 | JSON-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
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.
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:
"message": {
"role": "ROLE_USER",
…
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"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:
…
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"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:
| SDK | JSON-RPC errors | HTTP+JSON errors |
|---|---|---|
| a2a-python 1.2.2 | application/json sdk src/a2a/server/routes/jsonrpc_dispatcher.py | application/json sdk src/a2a/utils/error_handlers.py |
| a2a-go | application/json sdk-go a2asrv/jsonrpc.go at v2.6.0 | application/json sdk-go a2asrv/rest.go at v2.6.0 |
| a2a-java | application/json sdk-java reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java at v1.4.0.Final | application/json sdk-java transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java at v1.4.0.Final |
| a2a-js | application/json sdk-js src/server/express/json_rpc_handler.ts at v1.3.0 | application/a2a+json sdk-js src/server/express/rest_handler.ts at v1.3.0 |
| a2a-dotnet | application/json sdk-dotnet src/A2A.AspNetCore/JsonRpcResponseResult.cs at v1.0.0-preview2 | application/problem+json, a problem-details body sdk-dotnet src/A2A.AspNetCore/A2AHttpProcessor.cs at v1.0.0-preview2 |
| a2a-rs | application/json sdk-rust a2a-server/src/jsonrpc.rs at a2a-server-lf-v0.5.1 | application/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, HTTP500or503, or a dropped connection: retry with backoff, and honorRetry-Afterspec §3.3.2.- HTTP
401: get a fresh credential, then retry. -32600,-32602,-32005,-32009, and their400forms: 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-32004on a terminal task: start a new task, as CancelTask and terminal states shows.-32003,-32007, or-32004from 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
Security and Extensions
The card declares how to authenticate and which extensions apply, and the specification lists what each side must check.
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.
What the card declares
Only the deployer's card carries security fields, so test-runner and code-reviewer accept any caller.
"securitySchemes": {
"bearer": {
"httpAuthSecurityScheme": {
"description": "A token issued by the platform team.",
"scheme": "Bearer"
}
}
},
"securityRequirements": [
{
"schemes": {
"bearer": {
"list": []
}
}
}
],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:
| Implementation | Owner scope | Another caller's task |
|---|---|---|
| Kit | none, and the deployer needs one shared bearer token | returned |
| a2a-python 1.2.2 | task 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.py | TaskNotFoundError |
| a2a-js 1.3.0 | tenant and owner in every store, with unknown for an anonymous caller sdk-js src/server/owner_resolver.ts at v1.3.0 | TaskNotFoundError |
| a2a-java 1.4.0.Final | a 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.Final | TaskNotFoundError |
| a2a-go 2.6.0 | the 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.0 | ErrTaskNotFound once a name is set |
| a2a-dotnet 1.0.0-preview2 | none, because the store and the handler carry no principal sdk-dotnet src/A2A/Server/ITaskStore.cs at v1.0.0-preview2 | returned |
| a2a-rs a2a-server-lf-v0.5.1 | none 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.1 | returned |
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
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.
"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
}
]
},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.
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 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.
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
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.
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.
| Rule | The kit | The SDKs |
|---|---|---|
| Servers "MUST implement authorization checks on every A2A Protocol Operations request" spec §13.1 | none per caller: the deployer needs one shared token, and the other two agents accept anyone | none includes authentication, and owner scoping differs, as security schemes tables |
GetExtendedAgentCard "MUST require authentication" spec §13.3 | the deployer checks its bearer token before every method | none in the core handlers, as extended cards tables |
| Agents "MUST include authentication credentials in webhook requests" spec §13.2 | Authorization: Bearer hook_secret_91d2 on every POST | every 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.2 | accepts http://localhost:41250/a2a-events, because every process runs on one machine | a2a-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.2 | 10 s, and no retry | 5 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.2 | the planner's receiver records the headers and checks nothing | not compared, and push notifications lists the receiver's duties |
| Clients "SHOULD use unique, single-purpose tokens for each push notification configuration" spec §13.2 | one token per run, and a read returns it in clear | the 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.4 | a missing field answers -32602 with a BadRequest detail, and code-reviewer refuses a url part with image/png | the 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.1 | the planner decides on typed data fields only | not compared |
| "File references within A2A messages MUST be validated to prevent server-side request forgery (SSRF)" spec §14.1.1 | code-reviewer fetches no URL | not compared |
| "Logs MUST NOT include sensitive information (credentials, personal data) unless required and properly protected" spec §13.4 | the capture files hold dpl_test_7c1e4b and hook_secret_91d2, which are test values | not compared |
| "Implementations SHOULD support HTTPS to ensure authenticity and integrity of the Agent Card" and "Clients SHOULD verify signatures when present" spec §14.3 | the planner fetches cards over plain HTTP and verifies nothing, and 17-signed-card.txt shows the check | not compared, and extended cards and signatures covers verification |
| Agents "SHOULD implement rate limiting on all operations" and "SHOULD log security-relevant events" spec §13.4 | none | not 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:
"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"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
Implementing A2A
A client and a server follow the rules of the earlier parts, and the official test kit checks the server against them.
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.
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 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.
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 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.
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
messageIdspec §3.3.1. Keep the first reply, because a resend withouttaskIdcreates 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, setreturnImmediatelyand follow the task. - Card checks. Cache cards with standard HTTP caching spec §8.6, and verify the deployer's
signaturesas extended and signed cards shows. - Capability checks. The planner records
streamingand never reads it. Against an agent without streaming, itsSubscribeToTaskgetsUnsupportedOperationErrorspec §3.3.4. - A branch for every state. The loop has no branch for
TASK_STATE_SUBMITTEDorTASK_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
shipmethod reads the review artifact without checking that the review task completed, andfollowneeds aGetTaskcall 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
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.
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.
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")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.
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
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.
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 FAILresearch/tck-run.md classifies every row of the run.
Where the TCK departs from v1.0.1
| The TCK expects | The specification says | Effect on the kit |
|---|---|---|
JSON-RPC requests at <url>/ | the interface is at url spec §8.3.1 | 63 records in run 1 |
snake_case parameters such as task_id and history_length on JSON-RPC | JSON field names MUST be camelCase spec §5.5 | PUSH-CREATE-001 fails on JSON-RPC, GetTask ignores history_length |
a Content-Type that contains application/json on REST | application/a2a+json SHOULD be used spec §11.1 | HTTP_JSON-SVC-001 and HTTP_JSON-ERR-001 fail |
any error on CORE-SEND-003 counts as a failure | an unsupported part gets ContentTypeNotSupportedError spec §3.3.2 | fails on both bindings |
HTTP 409 for TaskNotCancelableError, 415 for ContentTypeNotSupportedError, 502 for InvalidAgentResponseError, and gRPC UNIMPLEMENTED for three errors | 400, 400, 500, and FAILED_PRECONDITION spec §5.4 | not reached in this run |
a hard assertion on the SHOULD and MAY caching tests | SHOULD is an xfail in the TCK's own README | three 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
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.
Host: localhost:41241
Content-Type: application/json
A2A-Version: 1.0
{
"jsonrpc": "2.0",
"id": 3,
"method": "message/send",
…
}
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32601,
"message": "Method not found"
}
}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.
| Area | 0.3 | 1.0 |
|---|---|---|
| Method names | message/send, tasks/get, tasks/resubscribe | SendMessage, GetTask, SubscribeToTask, the same PascalCase name on every binding spec §9.1 |
| Push config methods | tasks/pushNotificationConfig/set, and /get, /list, /delete | CreateTaskPushNotificationConfig, and Get, List (plural Configs), Delete |
| Extended card | agent/getAuthenticatedExtendedCard, REST GET /v1/card | GetExtendedAgentCard, 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 shape | TextPart, FilePart, DataPart, each with kind | one Part whose text, raw, url, or data member names the type proto Part |
| File fields | file.mimeType, file.name, file.fileWithUri | mediaType, filename, url, flat on the part |
| Stream frames | "kind": "status-update" | a statusUpdate or artifactUpdate member proto StreamResponse |
| End of a stream | final: true on the last status update | the binding closes the stream |
| Card endpoint | url, protocolVersion, preferredTransport, additionalInterfaces | supportedInterfaces[], each with url, protocolBinding, protocolVersion proto AgentInterface |
| Extended card flag | supportsAuthenticatedExtendedCard | capabilities.extendedAgentCard |
| Card security | security | securityRequirements, each with a schemes map proto AgentCard |
| Errors | RFC 9457 problem details | google.rpc.Status with ErrorInfo, plus -32008 and -32009 spec §5.4 |
| Version header | none | A2A-Version: 1.0 on every request spec §3.6.1 |
{
"protocolVersion": "0.3",
"url": "https://agent.example.com/a2a",
"preferredTransport": "JSONRPC",
"supportsAuthenticatedExtendedCard": true,
"additionalInterfaces": [...]
}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=Truetocreate_jsonrpc_routesandcreate_rest_routes, and add anAgentInterfacewithprotocolVersion0.3to the card sdk src/a2a/server/routes/jsonrpc_routes.py sdk src/a2a/server/routes/rest_routes.py. The dispatcher routes by method name, somessage/sendreachesJSONRPC03Adapterbefore the version check sdk src/a2a/server/routes/jsonrpc_dispatcher.py. For gRPC it addsCompatGrpcHandleron the old packagea2a.v1sdk src/a2a/compat/v0_3/grpc_handler.py. The client needs no switch:ClientFactorypicks a compat transport for an interface below1.0sdk src/a2a/client/client_factory.py. - a2a-js 1.3.0. Set
legacyCompat: { enabled: true }on each handler. It routes byA2A-Version, with an absent header as0.3sdk-js src/server/express/json_rpc_handler.ts at v1.3.0. - a2a-go 2.6.0. Mount a second handler from
a2acompat/a2av0under 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-jsonrpcormultiversion-restreference module. ItsVersionRoutersends0.3or 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
MapA2AWithV03Compatinstead ofMapA2A. It routes byA2A-Versionsdk-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
taskStatusUpdateandtaskArtifactUpdate. The proto hasstatusUpdateandartifactUpdate(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 artifactindexthat the proto lacks (conflict D10). The same PR removed all butindex. - Appendix A.2.2 calls the old flag
supportsExtendedAgentCard. The guide and the SDK saysupportsAuthenticatedExtendedCard(conflict D40), and main has no fix. - The guide puts extensions at
agentCard.extensions. The proto puts them atcapabilities.extensionsproto 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
Lookup tables for every name in the manual.
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 method | gRPC rpc | HTTP+JSON route | Request in, response out |
|---|---|---|---|
| SendMessage | SendMessage | POST /message:send | SendMessageRequest, SendMessageResponse |
| SendStreamingMessage | SendStreamingMessage | POST /message:stream | SendMessageRequest, a stream of StreamResponse |
| GetTask | GetTask | GET /tasks/{id=*} | GetTaskRequest, Task |
| ListTasks | ListTasks | GET /tasks | ListTasksRequest, ListTasksResponse |
| CancelTask | CancelTask | POST /tasks/{id=*}:cancel | CancelTaskRequest, Task |
| SubscribeToTask | SubscribeToTask | GET /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.py | SubscribeToTaskRequest, a stream of StreamResponse |
| CreateTaskPushNotificationConfig | CreateTaskPushNotificationConfig | POST /tasks/{task_id=*}/pushNotificationConfigs | TaskPushNotificationConfig in and out, with no request wrapper (conflict D5) |
| GetTaskPushNotificationConfig | GetTaskPushNotificationConfig | GET /tasks/{task_id=*}/pushNotificationConfigs/{id=*} | GetTaskPushNotificationConfigRequest, TaskPushNotificationConfig |
| ListTaskPushNotificationConfigs | ListTaskPushNotificationConfigs | GET /tasks/{task_id=*}/pushNotificationConfigs | ListTaskPushNotificationConfigsRequest, ListTaskPushNotificationConfigsResponse |
| DeleteTaskPushNotificationConfig | DeleteTaskPushNotificationConfig | DELETE /tasks/{task_id=*}/pushNotificationConfigs/{id=*} | DeleteTaskPushNotificationConfigRequest, google.protobuf.Empty |
| GetExtendedAgentCard | GetExtendedAgentCard | GET /extendedAgentCard | GetExtendedAgentCardRequest, 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.
| Method | Other A2A errors | Capability flag |
|---|---|---|
| SendMessage | ContentTypeNotSupportedError, TaskNotFoundError, and UnsupportedOperationError for a terminal task | none |
| SendStreamingMessage | The same three as SendMessage | streaming |
| GetTask | TaskNotFoundError | none |
| ListTasks | None beyond the standard protocol errors | none |
| CancelTask | TaskNotCancelableError, TaskNotFoundError | none |
| SubscribeToTask | TaskNotFoundError, and UnsupportedOperationError for a terminal task | streaming |
| The four push configuration methods | TaskNotFoundError, which GetTaskPushNotificationConfig also returns for a missing configuration | pushNotifications |
| GetExtendedAgentCard | ExtendedAgentCardNotConfiguredError when the flag is on and no extended card is configured | extendedAgentCard |
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
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.
| Field | Type | Req. | Meaning |
|---|---|---|---|
| Task | The unit of work, created by the server | ||
| id | string | yes | Minted by the server |
| contextId | string | The context. Both stream events require it. | |
| status | TaskStatus | yes | The state, status message, and time |
| artifacts | repeated Artifact | The outputs. ListTasks needs includeArtifacts for them. | |
| history | repeated Message | The kept messages, capped by historyLength | |
| TaskStatus | The status of a task | ||
| state | TaskState | yes | A value from Task states |
| message | Message | The agent's note, such as a question | |
| timestamp | google.protobuf.Timestamp | When it was recorded, in ISO 8601 and UTC | |
| Message | One turn of conversation | ||
| messageId | string | yes | Minted by the sender. Agents can use it to detect duplicates. |
| contextId | string | The context. Every server message has it. | |
| taskId | string | The task. A client value names an existing task that matches contextId. | |
| role | Role | yes | ROLE_USER from the client, ROLE_AGENT from the server |
| parts | repeated Part | yes | The content, one part or more |
| referenceTaskIds | repeated string | Other tasks the message refers to | |
| Part | Exactly one of text, raw, url, and data | ||
| text | string | Text content | |
| raw | bytes | File content, base64 in JSON | |
| url | string | A URL to the file content | |
| data | google.protobuf.Value | Any JSON value | |
| mediaType | string | The media type, for every kind of part | |
| Artifact | One output of a task | ||
| artifactId | string | yes | Unique in the task. Chunks match on it. |
| name | string | A readable name, such as test-log.txt | |
| parts | repeated Part | yes | The content, one part or more |
| TaskStatusUpdateEvent | A change of task status | ||
| taskId, contextId | string | yes | The task and its context |
| status | TaskStatus | yes | The new status |
| TaskArtifactUpdateEvent | A new artifact, or one chunk of it | ||
| taskId, contextId | string | yes | The task and its context |
| artifact | Artifact | yes | The artifact or the chunk |
| append | bool | When true, add to the artifact with this artifactId | |
| lastChunk | bool | When true, this is the last chunk | |
| StreamResponse | A frame or push body, one of four spec §3.2.3 spec §4.3.3 | ||
| task | Task | The task now. It opens task streams and subscriptions. | |
| message | Message | A message-only stream holds one, then closes. | |
| statusUpdate | TaskStatusUpdateEvent | A change of task status | |
| artifactUpdate | TaskArtifactUpdateEvent | A new artifact or chunk | |
| SendMessageConfiguration | The configuration of a send | ||
| acceptedOutputModes | repeated string | Media types the client accepts | |
| taskPushNotificationConfig | TaskPushNotificationConfig | A webhook to register, with no taskId | |
| historyLength | optional int32 | Unset means no limit, and 0 means none. | |
| returnImmediately | bool | True returns once the task exists. The default waits. | |
| TaskPushNotificationConfig | A webhook for one task | ||
| tenant | string | Must match the interface tenant, if set | |
| id | string | The configuration id, assigned on create | |
| taskId | string | The task it belongs to | |
| url | string | yes | The webhook URL |
| token | string | A token for the task or session. No transport is defined. | |
| authentication | AuthenticationInfo | How 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.
| Field | Type | Req. | Meaning |
|---|---|---|---|
| AgentCard | What the agent offers, where, and what it demands | ||
| name | string | yes | A readable name, such as test-runner |
| description | string | yes | What the agent does |
| supportedInterfaces | repeated AgentInterface | yes | Where to reach it. The first entry is preferred. |
| provider | AgentProvider | The provider: a required url and organization | |
| version | string | yes | The agent's own version, such as 2.3.0 |
| capabilities | AgentCapabilities | yes | The optional features it supports |
| securitySchemes | map<string, SecurityScheme> | Accepted authentication schemes, by name | |
| securityRequirements | repeated SecurityRequirement | Scheme names mapped to scopes, such as {"schemes": {"bearer": {"list": []}}} | |
| defaultInputModes | repeated string | yes | Media types accepted across all skills |
| defaultOutputModes | repeated string | yes | Media types produced across all skills |
| skills | repeated AgentSkill | yes | What the agent does well |
| signatures | repeated AgentCardSignature | JWS signatures over the canonical card | |
| AgentInterface | One way to reach the agent | ||
| url | string | yes | An absolute HTTPS URL in production |
| protocolBinding | string | yes | JSONRPC, GRPC, HTTP+JSON, or a custom binding URI |
| tenant | string | A routing value. When set, copy it into each request sent to this interface. | |
| protocolVersion | string | yes | The A2A version, such as 1.0 |
| AgentCapabilities | Optional features. An absent flag means false. | ||
| streaming | optional bool | Allows SendStreamingMessage and SubscribeToTask | |
| pushNotifications | optional bool | Allows the four push configuration operations | |
| extensions | repeated AgentExtension | The protocol extensions it supports | |
| extendedAgentCard | optional bool | Allows GetExtendedAgentCard | |
| AgentExtension | One extension the agent supports, in capabilities.extensions | ||
| uri | string | The URI that identifies the extension | |
| description | string | How this agent uses the extension | |
| required | bool | When true, the client must understand and comply with the extension | |
| params | google.protobuf.Struct | Configuration for the extension, as a JSON object | |
| AgentSkill | One thing the agent does well | ||
| id | string | yes | The skill id, such as run-tests |
| name | string | yes | A readable name |
| description | string | yes | What the skill does |
| tags | repeated string | yes | Keywords, one or more |
| inputModes | repeated string | Input media types that override the card's | |
| outputModes | repeated string | Output media types that override the card's | |
| securityRequirements | repeated SecurityRequirement | Requirements 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).
| Name | Use instead |
|---|---|
kind on a part or a stream event | The member name, such as text or statusUpdate |
TextPart, FilePart, DataPart | One Part with text, raw or url, or data |
final on TaskStatusUpdateEvent | The close of the stream |
taskStatusUpdate, taskArtifactUpdate | statusUpdate, artifactUpdate |
Task.createdAt, Task.lastModified | status.timestamp |
TaskArtifactUpdateEvent.index | artifactId, which chunks match on |
PushNotificationConfig, CreateTaskPushNotificationConfigRequest | TaskPushNotificationConfig, which the create operation takes as its request (conflict D4) |
configId on a push configuration | id |
Card url, protocolVersion, preferredTransport, additionalInterfaces | supportedInterfaces, with url and protocolVersion on each entry |
Card supportsAuthenticatedExtendedCard | capabilities.extendedAgentCard |
Card security | securityRequirements (conflict D6) |
Card extensions at the top level | capabilities.extensions (discrepancy N2) |
cursor, limit, nextCursor | pageToken, 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
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.
| State | Class | Set by | The client does next |
|---|---|---|---|
| TASK_STATE_UNSPECIFIED | none | Nobody: the zero value | Treat it, and any unknown name, as an error. |
| TASK_STATE_SUBMITTED | active | The server, on creation | Wait, poll, or subscribe. |
| TASK_STATE_WORKING | active | The agent, while it works | As for a submitted task |
| TASK_STATE_INPUT_REQUIRED | interrupted | The agent, with a question | Answer with a new message that carries the same taskId and contextId. |
| TASK_STATE_AUTH_REQUIRED | interrupted | The agent, for an authorization | Pass the request to whoever approves it, outside A2A, and subscribe, register a webhook, or poll. |
| TASK_STATE_COMPLETED | terminal | The agent, when the work is done | Read the artifacts for the result. Follow up in a new task with referenceTaskIds. |
| TASK_STATE_FAILED | terminal | The agent, unable to do the work | Read the status message, fix the input, and start a new task. |
| TASK_STATE_CANCELED | terminal | The agent, after CancelTask | Start a new task in the same context. |
| TASK_STATE_REJECTED | terminal | The agent, refusing the task | Stop 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
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.
| Error | JSON-RPC | HTTP | gRPC |
|---|---|---|---|
| TaskNotFoundError | -32001 | 404 | NOT_FOUND |
| TaskNotCancelableError | -32002 | 400 | FAILED_PRECONDITION |
| PushNotificationNotSupportedError | -32003 | 400 | FAILED_PRECONDITION |
| UnsupportedOperationError | -32004 | 400 | FAILED_PRECONDITION |
| ContentTypeNotSupportedError | -32005 | 400 | INVALID_ARGUMENT |
| InvalidAgentResponseError | -32006 | 500 | INTERNAL |
| ExtendedAgentCardNotConfiguredError | -32007 | 400 | FAILED_PRECONDITION |
| ExtensionSupportRequiredError | -32008 | 400 | FAILED_PRECONDITION |
| VersionNotSupportedError | -32009 | 400 | FAILED_PRECONDITION |
| JSONParseError | -32700 | none | none |
| InvalidRequestError | -32600 | none | none |
| MethodNotFoundError | -32601 | none | none |
| InvalidParamsError | -32602 | 400 | INVALID_ARGUMENT |
| InternalError | -32603 | 500 or 503 | INTERNAL or UNAVAILABLE |
| Authentication error | custom | 401 | UNAUTHENTICATED |
| Authorization error | custom | 403 | PERMISSION_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
Glossary
Each term below has one meaning across the manual, and its link names the section that defines it.
| Term | Meaning in this manual | Defined in |
|---|---|---|
| A2A error | One of the nine errors the specification defines and maps into every binding, such as TaskNotFoundError | 5.5 |
| Agent card | The JSON document that describes an agent, usually at /.well-known/agent-card.json | 2.1 |
| Artifact | An output of a task, made of parts. A long one arrives in chunks. | 3.3 |
| Blocking call | A send that waits for a terminal or interrupted state, the default | 4.1 |
| Capability | A card flag, such as streaming, that allows a group of operations | 2.1 |
| Card signature | An entry in signatures: a JSON Web Signature over the card's RFC 8785 canonical form | 2.3 |
| Context | A conversation that groups tasks and messages under one contextId | 3.5 |
| Direct reply | A Message that is the whole result of a send, with no task | 4.1 |
| Extended card | A fuller card that GetExtendedAgentCard returns to an authenticated client | 2.3 |
| Extension | A protocol addition named by a URI, declared in capabilities.extensions and activated with A2A-Extensions | 6.2 |
| History | The messages a task keeps, capped by historyLength | 4.3 |
| In-task authorization | An 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 |
| Interface | One entry in supportedInterfaces: a URL, a protocol binding, a protocol version, and an optional tenant | 2.1 |
| Interrupted state | TASK_STATE_INPUT_REQUIRED or TASK_STATE_AUTH_REQUIRED. The task waits. | 4.2 |
| Message | One turn, with a messageId, a role, and parts | 3.1 |
| Opaque | Hidden from the caller. An opaque agent shows only what it sends back. | 1.1 |
| Part | The smallest unit of content: one of text, raw, url, or data | 3.2 |
| Polling | Calling GetTask again and again to follow a task | 4.1 |
| Protocol binding | One concrete form of the operations: JSON-RPC, gRPC, or HTTP+JSON | 5.1 |
| Push notification | An HTTP POST of a StreamResponse to a webhook the client registered | 4.6 |
| Remote agent | The agent behind an A2A endpoint that receives messages and runs tasks | 1.1 |
| Security requirement | One entry in securityRequirements, which maps scheme names to the scopes a request needs | 6.1 |
| Security scheme | A named way to authenticate, declared in a card's securitySchemes | 6.1 |
| Service parameter | A per-request key, such as A2A-Version, sent as an HTTP header or as gRPC metadata | 5.4 |
| Skill | One entry in a card's skills, such as run-tests | 2.1 |
| Status message | The one message a TaskStatus can hold, such as a question or a reason | 3.1 |
| Stream | Server-Sent Events, or a gRPC stream, of StreamResponse frames. SubscribeToTask opens one on a task. | 4.5 |
| Task | The unit of work a server creates for a message, with an id and a TaskState | 3.4 |
| Tenant | An optional routing string on an interface, copied into every request sent to that interface | 5.2 |
| Terminal state | TASK_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
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
| Item | Value |
|---|---|
| Release | A2A 1.0.1, tag v1.0.1 of github.com/a2aproject/A2A, commit 3303592588e388e62e0f69f701af531d2f4e3991, tagged 2026-05-28 |
| Protocol version | 1.0 in every request and card. Facts verified 2026-10-06. |
| Main branch | Commit 679ab3a of 2026-10-05, read for changes after the tag, and quoted only for rules the tag lacks |
| Reference SDK | a2a-sdk 1.2.2, tag v1.2.2 of github.com/a2aproject/a2a-python, commit 2f9e44af243df5c7c7a3da361c8d095324bcd5e3, Apache License 2.0 |
| Capture kit | manuals/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.
| File | Upstream | Cited as |
|---|---|---|
| a2a.proto | specification/a2a.proto, 811 lines, normative | proto and a message or enum name |
| specification.md | docs/specification.md, 3610 lines | spec and a section number |
| specification-main.md | docs/specification.md on main at 679ab3a, 3618 lines, unreleased | spec-main and a section number, only for rules added after the tag |
| docs.md | Fourteen 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.
| Repository | Cited as | Tag | Commit | Date |
|---|---|---|---|---|
| a2a-python | sdk | v1.2.2 | 2f9e44a | 2026-10-05 |
| a2a-go | sdk-go | v2.6.0 | ebf17c5 | 2026-09-25 |
| a2a-java | sdk-java | v1.4.0.Final | 6408194 | 2026-09-28 |
| a2a-js | sdk-js | v1.3.0 | 29417a5 | 2026-09-29 |
| a2a-dotnet | sdk-dotnet | v1.0.0-preview2 | 87fd448 | 2026-04-09 |
| a2a-rs | sdk-rust | a2a-server-lf-v0.5.1 | 32c31f6 | 2026-09-30 |
| a2a-tck | a2a-tck and a file path | 1.0.0.alpha2 | 29063fe | 2026-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.
| ID | The sources say | The manual follows | Discussed in | Upstream |
|---|---|---|---|---|
| D1 | Subscribe verb. Proto GET. §5.3 and §11.3.2 POST. | The proto. Accept both. | front, 4.5, 5.2, R.1 | open PR #2068 |
| D2 | Blocking. §3.1.1 returns at once. §3.2.2 and the proto wait. | §3.2.2 and the proto | front, 4.1, 7.1 | open issue #2135 |
| D3 | Streams 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 ones | front, 1.2, 4.2, 4.5, R.3 | open PR #2270 |
| D4 | Push configuration name. Prose PushNotificationConfig. Proto TaskPushNotificationConfig. | The proto name | 4.6, R.2 | partly fixed on main (PR #1981) |
| D5 | Create request. Prose and Appendix A CreateTaskPushNotificationConfigRequest. Proto TaskPushNotificationConfig. | The proto | 4.6, R.1 | partly fixed on main (PR #1981) |
| D6 | Card security field. §3.1.11, §13.3, and the §8.5 sample security. Proto securityRequirements. | The proto | front, 2.1, 2.3, 6.1, R.2 | partly fixed on main (PR #2046) |
| D7 | Extended card flag number. Appendix A.2.2 field 5. Proto field 4. | The proto | 2.3 | no activity |
| D8 | Protocol version. Appendix A.2.1 protocolVersions on the card. Proto protocolVersion per interface. | One per interface | 2.1 | fixed on main, unreleased (PR #2165) |
| D9 | Stream event names. Migration guide taskStatusUpdate, taskArtifactUpdate. Proto statusUpdate, artifactUpdate. | The proto | 4.5, 7.4 | fixed on main, unreleased (PR #2056) |
| D10 | Fields the proto lacks. Migration guide Task.createdAt, Task.lastModified, configId, TaskArtifactUpdateEvent.index. | Do not send them | R.2, 7.4 | partly fixed on main (PR #2056) |
| D11 | Deprecated OAuth flows. Migration guide: implicit and password removed. Proto: kept, deprecated. | Present, never used | 6.1 | no activity |
| D12 | Pagination names. Migration guide cursor, limit, nextCursor. Proto pageToken, pageSize, nextPageToken. | The proto | 4.3 | fixed on main, unreleased (PR #2165) |
| D13 | Error body. §6.4 and §6.5 samples application/problem+json. Migration guide application/json. §11.6 google.rpc.Status. | §11.6 | 5.5 | open PR #1641 and PR #1689 |
| D14 | JSON-RPC method template. §9.3 category/action. §9.1 and §9.4 PascalCase. | PascalCase | 5.1 | no activity |
| D15 | Proto path. §1.4 spec/a2a.proto. §5.7, §10.1, and the repository specification/a2a.proto. | The repository | 1.1, Vendored files | no activity |
| D16 | Release 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-28 | The pin | open PR #2072 |
| D17 | Header registrations. §14.2.1 and §14.2.2 cite Section 3.2.5. Service parameters are §3.2.6. | §3.2.6 | 5.4 | open issue #2305 |
| D18 | TLS version. Spec 1.3 or later. Enterprise guide 1.2 or later. | The spec | 6.1 | no activity |
| D19 | Empty 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 card | 2.1, R.2 | open issue #2122 |
| D20 | The typ header. §8.4.2 lists typ among the MUST fields and words it as SHOULD. | Send alg, kid, and typ set to JOSE | 2.3 | no activity |
| D21 | Signing steps. §8.4.2 and §8.4.3 remove default values. §8.4.1 keeps REQUIRED and set optional fields. | §8.4.1 | 2.3 | open issue #2249 |
| D22 | JSON naming example. §5.5 push_notification_config. Proto task_push_notification_config. | The proto | R.2 | no activity |
| D23 | Samples that break REQUIRED. Some omit messageId, artifactId, or an event's contextId. | Never copy a sample as it is | 3.1, 6.2 | partly fixed on main (PR #2083) |
| D24 | Invalid JSON. The samples in §4.6.1, §6.7, and §9.4.1. | Never copy a sample as it is | 3.2, 6.2 | open PR #1657 |
| D25 | Layer 2 diagram. §1.3 shows Get Agent Card. §3.1 defines only Get Extended Agent Card. | The diagram is informal | 1.1 | no activity |
| D26 | Mandatory operations. §3.1 requires them all. §3.3.4 lets flags decline streaming, push, and the extended card. | The capability flags | 2.2 | no activity |
| D27 | Transport in a guide. Core Concepts: JSON-RPC 2.0 for everything. Spec: three bindings. | The spec | 5.1 | no activity |
| D28 | Who creates the context id. Core Concepts: the server. Spec: a client can propose one. | The spec | 3.5 | open issue #1317 |
| D29 | States from extensions. The extensions guide lists extensions that add states, and forbids new enum values. | Neither is normative. Add no states. | 3.4, 6.2 | no activity |
| D30 | Extension activation. §4.6.1 JSON-RPC parameters. §9.2 HTTP headers. | The header | 5.4 | no activity |
| D31 | Version as a parameter. §3.6.1 allows a request parameter. §9.2 and §11.2 require headers. | The header | 5.4 | open issue #2184 |
| D32 | Old endpoint names. The what-is-a2a diagram: POST /sendMessage, /.well-known/agent-card. | message:send, agent-card.json | 2.2 | fixed on main, unreleased (PR #2261) |
| D33 | Stream type in a guide. Streaming guide SendStreamingMessageResponse. Spec StreamResponse. | The spec | 4.5 | no activity |
| D34 | gRPC interface URL. Proto: an absolute HTTPS URL. gRPC dials a host and a port. | No rule at the tag | 2.1, 5.3 | fixed on main, unreleased (PR #1997) |
| D35 | Role spelling. §2.2 user and agent. Proto and §5.5 ROLE_USER, ROLE_AGENT. | The enum names | 3.1 | no activity |
| D36 | Another client's task. §3.3.2 calls it an authorization error and forbids revealing that it exists. | TaskNotFoundError | 6.1 | no activity |
| D37 | A 0.3 state name. §3.4.3 input-required. Proto TASK_STATE_INPUT_REQUIRED. | The proto name | 3.4 | open PR #2155 |
| D38 | Cancel transitions. Migration guide: clarified in 1.0. Spec: no transition table. | Transitions are implementation behavior | 3.4 | open issue #1992 |
| D39 | Version in samples. §9.2, §11.2, and §14.2.1 send A2A-Version: 0.3. | Send 1.0 | 5.4 | no activity |
| D40 | Old extended card flag. Appendix A.2.2 supportsExtendedAgentCard. Migration guide and SDK supportsAuthenticatedExtendedCard. | extendedAgentCard in capabilities | 2.3, 7.4 | no 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.
| ID | The sources say | The manual follows | Discussed in | On main |
|---|---|---|---|---|
| N1 | Stream 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.1 | R.6 only | fixed |
| N2 | Extensions path. Migration guide agentCard.extensions. Proto capabilities.extensions. | The proto | 6.2, R.2 | unchanged |
| N3 | Tenant scope. Migration guide: tenant scoping in gRPC requests. Proto: tenant on every request message, and a /{tenant} route for every rpc. | The proto | 5.2 | unchanged |
| N4 | ListTasks 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 schema | R.6 only | no change needed |
| N5 | Upgrade headings. Appendix A.2.1 says pre-0.3.x under the heading about the 1.0 change. | Read as pre-1.0 | R.6 only | unchanged |
| N6 | Removal 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 release | R.6 only | unchanged |
| N7 | Removed 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 proto | R.6 only | unchanged |
| N8 | Version in the §10.2 sample. The gRPC sample sends a2a-version 0.3, which D39 does not list. | Send 1.0 | 5.3 | unchanged |
| N9 | Config id name. Migration guide config_id. Proto id. | The proto | R.2 | fixed |
| N10 | Extension lists as new fields. Migration guide adds Message.extensions and Artifact.extensions in 1.0. Both exist in the 0.3 proto. | Not new | R.6 only | unchanged |
| N11 | Mutual TLS as new. Migration guide adds mutual TLS in 1.0. The 0.3 proto has mtls_security_scheme. | Not new | R.6 only | unchanged |
| N12 | Tenant routes. §5.3, §11.3, and §11.5 list no /{tenant} route. The proto adds one to every rpc. | The proto | 5.2, R.1 | unchanged |
| N13 | Tenant field number. ListTaskPushNotificationConfigsRequest.tenant is field 4. Every other request has field 1. | No conflict. Visible in gRPC only. | R.6 only | unchanged |
| N14 | filename 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.2 | unchanged |
| N15 | Create request in Appendix A. The table names CreateTaskPushNotificationConfigRequest as current. It is the 0.3 type. | The proto, as D5 | 4.6 | unchanged |
| N16 | Operation 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 only | unchanged |
| N17 | The 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 proto | 4.1 | unchanged |
| N18 | Card sample in the extensions guide. A top-level url, the 0.3 shape. | supportedInterfaces | R.6 only | fixed |
| N19 | A gRPC-specific wrapper. §10.5.1 calls TaskPushNotificationConfig a gRPC resource type. The proto uses it in every binding. | The proto | R.6 only | unchanged |
| N20 | Security in the tenancy guide. The multi-tenancy page securitySchemes and security. Proto securityRequirements. | The proto, as D6 | R.6 only | unchanged |
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
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.1 | 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. |
| 1.1 | 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. |
| 1.2 | A2A fixes the messages, states, and events between agents, while trust, discovery, retention, and exactly-once effects stay with the host. |
| 1.3 | 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. |
| 2.1 | 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. |
| 2.2 | Three routes lead to one card, and a client takes the first interface it speaks and checks a capability flag before each optional operation. |
| 2.3 | 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. |
| 2.4 | The signature covers the RFC 8785 bytes of the card without signatures, so an edit to the description changes those bytes and verification fails. |
| 3.1 | A message carries an id its sender mints, while its contextId and taskId come from the server and the client echoes them to continue. |
| 3.2 | 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. |
| 3.3 | 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. |
| 3.4 | 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. |
| 3.5 | The kit's captures show nine transitions between task states, and none of them leaves a double-bordered terminal state. |
| 3.6 | 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. |
| 4.1 | 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. |
| 4.2 | A blocking send holds one request open until this task completes, while returnImmediately answers at TASK_STATE_SUBMITTED and leaves the client to poll. |
| 4.3 | 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. |
| 4.4 | 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. |
| 4.5 | 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. |
| 4.6 | The agent chose three of these endings and the client one, and once a task ends, the server refuses any further message or cancel. |
| 4.7 | A dropped stream loses the frames sent while it was closed, and SubscribeToTask resumes with a Task snapshot that already holds their content. |
| 4.8 | With a webhook registered, test-runner posts eight StreamResponse bodies to the planner's receiver after the SendMessage call has already returned. |
| 5.1 | 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. |
| 5.2 | One rpc sends the request with its metadata, the server streams StreamResponse messages until the task ends, and the rpc status closes the stream. |
| 5.3 | The same TaskNotFoundError arrives as HTTP 200 with code -32001 in JSON-RPC and as HTTP 404 in HTTP+JSON, carrying the same ErrorInfo. |
| 6.1 | 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. |
| 6.2 | 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. |
| 6.3 | 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. |
| 7.1 | 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. |
| 7.2 | 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. |
| 7.3 | 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. |
Sources:the figure blocks in front.md and in every file under sections/; the figure sources in figures/src/ (manuals/a2a-101)
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.