Agents talking to agents: A2A explained
Companion code: 17-a2a
TL;DR
- A2A (Agent2Agent) is an open protocol for one agent to delegate work to another, typically one built by a different team or company, without either seeing the other’s internals. Its stable 1.0 release came in early 2026, and it’s hosted by the Linux Foundation.
- Its core ideas: an Agent Card for discovery, a Task with a lifecycle for work that takes time, and messages, parts and artifacts for what flows in and out. Crucially, a task can stop and ask for input instead of guessing.
- Use MCP when the other side is a tool. Use A2A when the other side is an agent. Then design for it: expose skills, not tools; treat replies as untrusted; authenticate both ways; make tasks durable.
When the other side is an agent
Post #5 covered MCP, which standardizes how an agent reaches its tools: a call goes in, a result comes back. That model starts to strain when the thing on the other end is itself an agent:
- It takes time. A research or approval task can run for minutes or hours. A request/response call is the wrong shape.
- It may need to ask. “Which Bluebird do you mean?” is a legitimate response, not an error.
- It’s opaque, on purpose. Another company’s procurement agent won’t expose its tools, prompts or data to you. You get the outcome, not the internals.
- It needs discovery and trust. You have to find it, learn what it can do, and authenticate with it, often across organizational boundaries.
Google introduced A2A in April 2025 for exactly this, and the project now lives at the Linux Foundation. Its 1.0 specification brought support for several transports (JSON-RPC, gRPC and plain HTTP+JSON), multi-tenancy, updated security flows, and signed Agent Cards so a caller can verify who published one. By the project’s first anniversary in April 2026, more than 150 organizations supported it, and it had been integrated into Microsoft’s Azure AI Foundry and Copilot Studio and Amazon Bedrock AgentCore Runtime.
The core ideas
1. The Agent Card: discovery
An A2A agent publishes a JSON document describing itself, usually at a well-known address: /.well-known/agent-card.json. The card says who the agent is, where to reach it and with which protocol binding, what input and output types it handles, how to authenticate, and, most importantly, its skills:
{
"name": "Larkspur CRM specialist",
"supportedInterfaces": [{"url": "http://127.0.0.1:9001/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"}],
"capabilities": {"streaming": false, "pushNotifications": false},
"skills": [{
"id": "customer-lookup",
"name": "Customer lookup",
"description": "Look up a customer by name or email and report plan, city, spend and open tickets.",
"examples": ["How much has Riverside Legal spent?"]
}]
}
A skill is a capability described for another agent, much like a good tool description from post #4, but at the level of a job (“look up a customer”) rather than a function signature.
2. Tasks and their lifecycle
When a client sends a message, the remote agent creates a Task, a unit of work with an ID, a context ID that groups related tasks, a status, and eventually artifacts, the outputs. A task moves through a small set of states:
Two of these states are what make A2A more than remote function calls. INPUT_REQUIRED means the agent has stopped to ask something. The client answers by sending another message in the same task, and work resumes. AUTH_REQUIRED means the agent needs additional authorization to continue. Both are normal outcomes, not errors.
3. Messages, parts and artifacts
Everything that flows is made of parts: text, files (inline or by reference) or structured data. A message has a role (ROLE_USER for the caller, ROLE_AGENT for the remote agent) and some parts. An artifact is a named output made of parts. That’s it: a small vocabulary that covers chat, documents and structured results.
4. Waiting, polling and streaming
SendMessage waits by default until the task finishes or needs input. For long work, the client sets returnImmediately and polls with GetTask, or uses streaming (SendStreamingMessage) or push notifications to a webhook, if the agent’s card says it supports them. Errors have defined codes, such as task not found (-32001), task not cancelable (-32002) and version not supported (-32009), and every request names its protocol version in an A2A-Version header.
On the wire, with the JSON-RPC binding, a delegation looks like this:
{"jsonrpc": "2.0", "id": "1", "method": "SendMessage",
"params": {"message": {"role": "ROLE_USER", "messageId": "m-1",
"parts": [{"text": "How much has Bluebird spent?"}]}}}
and the reply is a task:
{"jsonrpc": "2.0", "id": "1",
"result": {"task": {"id": "t-7", "contextId": "c-3",
"status": {"state": "TASK_STATE_INPUT_REQUIRED",
"message": {"role": "ROLE_AGENT", "parts": [{"text": "There are several Bluebird customers. Which one do you mean?"}]}}}}}
Seven rules for using A2A well
1. Choose the protocol by what’s on the other side. If it’s deterministic, fast and you control it, it’s a tool: use MCP or a plain function. If it reasons, takes time, may ask questions or belongs to someone else, it’s an agent: use A2A. Don’t wrap every function as an “agent”. The protocol overhead buys nothing there.
2. Expose skills, not tools. A remote agent’s card should describe jobs it can do, with examples, the way you’d brief a colleague. If callers need to know your internal tools to use you well, the boundary is in the wrong place.
3. Ask instead of guessing. Build remote agents to return INPUT_REQUIRED when a request is ambiguous. Build clients to handle it. A delegated task that guesses wrong is worse than one that asks, especially across an organizational boundary where nobody can look inside.
4. Treat every reply as untrusted input. An artifact from another organization’s agent is content you didn’t write, which is exactly the “untrusted content” leg of the lethal trifecta (post #10). Don’t let it trigger consequential actions without your own checks.
5. Authenticate both ways, and scope per task. Verify the card’s publisher (1.0 supports signed cards), authenticate your requests with the schemes the card declares, and grant the remote agent only what the task needs. Agent identity gets its own post later in this series.
6. Make tasks durable. A task that can run for hours must survive restarts on both sides: store task state, not just connections, as in the harness from post #11. The demo keeps tasks in memory, so it can’t.
7. Version explicitly, and trace across the boundary. Send A2A-Version on every request, handle the version error, and propagate trace context so a single trace follows the work into the other agent (post #15).
The experiment
The companion code (17-a2a) implements the core of A2A 1.0 from scratch: a remote CRM specialist agent (the tool-using agent from post #4, now hidden behind the protocol) and a client that does what a calling agent would.
python remote_agent.py --port 9001
python client.py
The client walks through discovery, a blocking task, a non-blocking task with polling, and protocol errors. The step that matters most is the ambiguous one:
4. An ambiguous request: the agent asks a question instead of guessing
state: TASK_STATE_INPUT_REQUIRED
agent asks: There are at least 15 customers called "Bluebird" (for example Bluebird Books in Denver,
Bluebird Analytics in Dublin and Bluebird Dental in Lisbon). Which one do you mean?
-> answering in the same task
state: TASK_STATE_COMPLETED
artifact: Bluebird Dental (C-1058, Lisbon, Starter plan) has spent $1,120 in total, across 8 orders.
They also have 2 open tickets: "CSV export fails" and "Webhook not firing."
In three live runs on Claude Opus 5.5, every answer matched the CRM’s records, and the agent asked which Bluebird was meant every time rather than picking one. There are exactly 15.
The remote agent’s system prompt tells it to stop and ask rather than guess, and the server turns that into an INPUT_REQUIRED status. The client answers with a message carrying the same taskId, and the agent continues where it left off, with its own context intact on its side. The caller never sees the CRM tools, the prompt or the conversation behind the task.
The demo is written to run on Amazon Bedrock too (LLM_PROVIDER=bedrock), though the runs above used the Claude API.
Trade-offs
- Another protocol to operate. A2A adds discovery, task storage, auth and versioning. Inside one team and one codebase, an in-process sub-agent (post #7) is simpler. A2A earns its keep at boundaries: between teams, vendors or organizations.
- Opacity cuts both ways. You can’t see why a remote agent did what it did. Contracts (clear skills, expected artifacts) and your own verification of results matter more than with code you own.
- Interoperability is only as good as adoption. A2A has broad backing, but agents in the wild will implement different subsets: streaming, push, extensions. Read the card, and degrade gracefully when a capability is missing.
Next in the series: What it really costs to run an agent.
Sources & further reading
- A2A Project, Agent2Agent (A2A) Protocol Specification, version 1.0.
- Linux Foundation, A2A Protocol surpasses 150 organizations, lands in major cloud platforms… (Apr 2026).
- Google Open Source Blog, A year of open collaboration: celebrating the anniversary of A2A (Apr 2026).
- A Survey of AI Agent Protocols (2025).