| TLDR |
|---|
| We built a multi-agent assistant at Eon. When three of its specialists had to move out of the process, we put them behind A2A rather than writing our own RPC contract for each. It doesn't carry the identity of the person asking, and it doesn't carry enough provenance for a second person to check the answer. Then production found three more problems. We'd choose A2A again. |
ORIGIN
Buzz is our multi-agent assistant we built at Eon, a cloud data platform. It answers questions in plain language against a company's cloud inventory, CRM, support desk and call transcripts, and it's a fleet of agents rather than one chatbot because the questions people ask cross four of those systems in a single sentence.
It started as one agent with a pile of tools and grew into a router: a root agent that picks a specialist, hands the question over, and assembles the reply. About twenty of those specialists live in the same service, and a function call is enough of a boundary for them.
Then three had to leave the process.
Our triage agent holds production credentials Buzz has no business holding and ships on the backend's release train. The Eon agent owns the semantic layer. Eon models a company's data into one layer of tables, metrics and the definitions a company actually argues about, and that layer is what the questions get answered against. It ships when the data model changes rather than when the assistant does. And a tenant admin may want to plug in an agent their own team built, on a framework we don't run, whose source we will never see.
CHOICE
Why we chose A2A
For the first two, a bespoke HTTP contract each would have worked. We'd have written auth, retries, streaming and a task lifecycle three times and owned them forever, but it would have worked.
The third case made that impossible. You can't import a stranger's agent. You can only call it, and to call it you need a contract neither side wrote for the other: capabilities you can read without asking a human, an auth scheme you can both name, a task lifecycle you both already agree on. That's what A2A exists to solve.

Would we choose it again? Yes. An agent's capabilities reach us as data we read at runtime, so a tenant can add an agent to Buzz without us shipping code. Everything else in the protocol we could have built ourselves, and in a few places we effectively did.
DISCOVERY
What the agent card gave us
{
"name": "Eon Agent",
"description": "Answers a question in plain language against the semantic layer over the tenant's data.",
"supportedInterfaces": [
{ "url": "https://agent.example.com/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "0.3" }
],
"skills": [
{
"id": "semantic-query",
"name": "Semantic query",
"description": "Plan SQL against the semantic layer, execute it, and return a table or narrative.",
"examples": [
"How many tasks ran successfully in the last 7 days?",
"Show failures by account this month"
]
}
],
"security": [{ "oauth2": ["https://agent.example.com/scopes/query"] }],
"capabilities": { "streaming": true }
}The agent card did more for us than anything else in the protocol. An agent publishes one, and it's enough to route to it. We're still on 0.3; v1.0 replaces the old transport fields with the supportedInterfaces array shown above, and the extension mechanism this post depends on is the same in both.
Those skills, with their examples, are what a routing model needs in a tool description. We generate the ask_<agent> tool from the card, so an agent's own words about itself decide when the orchestrator reaches for it. A tenant admin pastes a URL and credentials, we fetch the card, import the skills, and the next question routes through that agent.
We added an auto_route switch, because "the orchestrator may pick this agent" and "this agent is reachable when a user names it" turned out to be different products. And an SSRF guard on every URL we're handed, the agent's and its token endpoint both.
DIVISION
Where MCP ends and A2A begins
We use both, heavily, and we sort them by who owns the meaning of the answer. An MCP server hands back rows and lets the caller decide what they mean; asking the Eon agent a question hands over which tables to join and what the metric even is.
| DIMENSION | MCP | A2A |
|---|---|---|
| Connects | an agent to a system | an agent to another agent |
| Returns | a tool result | a turn: reasoning, its own tool calls, progress |
| Owns the semantics | the caller | the callee |
IDENTITY
Carrying the asker's identity
Our definitions are role-aware. "Active customer" can legitimately differ by team and by region, so who is asking is part of the query. Send a question across an agent boundary without the asker and the far side answers as a service identity.
So we put identity in the context_id, structured as ADK/{app}/{user}/{session}, and the remote parses it back into a real user and session. That identity is load bearing downstream: the Eon agent reads the catalog with the calling user's own token, so answers respect that person's role.
This works because we own both ends. Worth knowing before you copy it: v1.0 says clients shouldn't hand a server a client-generated context_id unless they understand how that server will process it. The spec has in-task authorization for when the callee needs the user to grant something, but no first class place to say who is asking. We'd like one.
PROVENANCE
Carrying provenance
This was the biggest gap we hit, and the one that made us write an extension.
"Tasks succeeded 41,203 times last week" is a weak answer for a shared assistant, because the person reading it has no way to check it. Which tables, how fresh, which definition of "succeeded," is it a sample. The text crosses the boundary and the provenance stays behind, so the orchestrator states the number as fact.
So we defined a DataContext envelope: the executed query, the semantics, lineage with per-source freshness, confidence with caveats, governance flags. The producing agent declares the extension in capabilities.extensions, the consumer opts in with an A2A-Extensions header, and the payload rides in message metadata alongside the text.
The UI turns it into a "Data source" affordance, so whoever reads the answer can see the SQL and the lineage without going to ask an analyst. And the orchestrator caveats its own answer from the freshness block, taking the worst case across sources.
Two rules came out of that:
- Validate types, allow unknown fields, never raise. A malformed or newer envelope drops silently and the turn still renders its text answer, because two independently deployed agents disagreeing about a schema happens all the time.
- Strip the bulk on the way in. We remove the result rows before the envelope reaches the orchestrator, which needs the provenance while the user needs the data.
FIELD NOTES
Three things production broke
An SDK gets you talking in an afternoon, but getting from a working delegation to a production one took a wrapper of about four hundred lines.
01 A streaming default cost us every long request.
A client can default to non-streaming, which makes each delegation one blocking message/send POST held open for the whole remote run. Our triage agent sits behind a gateway that gives up after sixty seconds. A triage would work when an engineer ran it locally and failed at exactly sixty seconds through the gateway. Every time, including on every retry, until the turn budget was gone and the user had nothing.
message/stream fixed it, because incremental task-status events keep the connection producing bytes. Clients negotiate this from capabilities.streaming, so if you publish a card, advertise streaming honestly.
02 A failure the model can't see becomes a fabrication.
A remote error often arrives as an event carrying an error message and no content, and content-free events are easy for a framework to drop when it assembles the model's context. The orchestrator delegates, receives nothing, notices nothing, and answers from whatever it already had.
We now synthesize text content for every error event. Our first version of that fix was overcorrected: the orchestrator started citing one 502 on every later turn, so we had to tell it to retry the subagent fresh rather than remember the error.
03 Someone else's context window is your problem too.
The remote accumulates history under that context_id and eventually exceeds its own window. What we saw was a conversation that worked for twenty minutes and then started failing in a way that looked like our bug. We fold an epoch counter into the session segment of the context_id, which starts a fresh conversation on the far side while keeping the same user. It's a workaround for something the protocol doesn't cover.
UPSTREAM
What we'd take to the spec
We built the provenance envelope because we needed it, not because we thought we should own it. If the A2A community wants a shared way to carry lineage, freshness and confidence across a hop, we'd rather bring ours to that conversation than keep a private one, and we will write it up as an extension proposal.
Smaller things we'd work on with anyone interested:
- Session lifecycle across a hop. A standard way for a peer to say "my context is full, start a new one" would delete our epoch counter.
- A retryable signal in the error. v1.0 has a real error model, and HTTP bindings can carry retry guidance. What a caller still can't read off the error itself, portably, is whether the callee thinks the failure is transient.
- Somewhere to look up an extension. A card can advertise an extension URI, and there's nowhere to read what that URI means. A registry would let a consumer support an envelope it hasn't hard-coded.
VERDICT
What we'd tell you if you're deciding
Adopting A2A added failure modes rather than removing them. The three above are the ones that reached users first.
What we got for it is that the boundary between our agents became cheap enough to keep: the triage agent ships with the backend, the Eon agent gets rebuilt without a Buzz deploy, and a tenant can bring an agent we have never seen.
If you're federating agents and only the text survives the hop, identity and provenance are what you'll end up adding yourself, so decide early where you put them.
Share
Author

Rachel Neriya Schifter



