A2A and the multi-vendor future
The last protocol chapter looks at the newest and least settled layer: agents talking to other agents, across organizational and vendor boundaries. MCP connects an agent to tools; A2A, the Agent2Agent protocol, connects an agent to counterparties, other agents that plan, act, and report back. Chapter 27 already placed it as the third and rarest communication pattern, reserved for genuine cross-boundary cases. This chapter decodes the protocol properly and then does something the hype cycle rarely does: an honest accounting of what is load-bearing in 2026 versus what is still aspiration.
What A2A standardizes
Inside one platform, agents coordinate through the ledger and scheduler you built; there is no need for a protocol when everyone shares a control plane. A2A is for the case where they do not share one: your agent engaging another team's agent, another company's, another vendor's, where there is no common scheduler, no shared trust, no agreed data model. It standardizes the handshake for that:
- Agent cards. An A2A agent publishes a card, a signed description of
what it does, how to reach it, and how to authenticate. This is the
discovery layer, the A2A analog of MCP's
tools/list: a way to learn a counterparty's capabilities without bespoke integration. The signature matters, because a counterparty you did not write is untrusted, and a signed card is at least a verifiable identity. - Tasks and their lifecycle. A2A models an interaction as a task with a lifecycle (submitted, working, input-required, completed, failed), messages and artifacts flowing inside it. This is the key difference from MCP: a tool call is a request and an immediate result, but a task is long-lived, a counterparty may work for minutes, ask for clarification, and return artifacts over time, which is what "hiring an agent" rather than "calling a tool" actually requires.
The governance is part of the story and part of why it is worth taking seriously: A2A was donated to the Linux Foundation, its spec reached 1.0, and the major agent platforms (AgentCore Runtime and Strands on the AWS side) ship support. It sits in the same structural slot for cross-vendor agent interop that MCP occupies for tools, an open, governed standard rather than one vendor's API.
Lab 10.2: A2A from scratch
Lab 3.0 built MCP from scratch and the protocol stopped being magic. Same treatment here, because the difference between the two is easiest to see running. The lab is a real HTTP server and client, no SDK: the server publishes an agent card and manages tasks, the client discovers it, checks its skills, hires it, and reads the artifact.
python3 a2a_mini.py
--- 1. discovery: fetch the peer's agent card ---
GET /.well-known/agent-card.json -> Incident Analyst v1.0.0
auth: ['bearer'] | streaming: False
skill: root-cause Given metrics, deploys, and logs, name the cause.
skill: blast-radius Estimate which services an incident affected.
--- 2. capability match: can this peer do the job? ---
need 'root-cause' -> match, hiring it
--- 3. hire it: create a task (underspecified on purpose) ---
POST /a2a/tasks -> task task-0a2a0001 state=input-required
the peer asked: 'Which deploys landed in the 2 hours before the spike?'
(input-required is what makes this a PEER, not a tool: it can question you)
--- 4. answer it: continue the same task ---
POST /a2a/tasks/task-0a2a0001/send -> state=completed
--- 5. read the artifact (the deliverable) ---
root cause : deploy v841 shrank the payments-db pool 50->10
confidence : 0.91
evidence : p99 210ms -> 2930ms at 02:00
evidence : 02:03 pool exhausted 10/10 (payments-db)
evidence : v841 deployed 01:55
recommends : roll back v841 or restore pool size 50
--- 6. the task is durable and inspectable ---
GET /a2a/tasks/task-0a2a0001 -> state=completed, 3 messages, 1 artifact(s)
Four things in that run are the whole protocol, and three of them have no MCP equivalent:
- Discovery is a document, not a registry. The card lives at a
well-known path, so knowing a domain is enough to learn what an agent
does and how to authenticate. Compare MCP's
tools/list: same job, but a card describes an agent with skills, not a set of callable functions. - The unit is a task with an id, not a call.
POST /a2a/tasksreturned a task object, and step 6 fetched it again later. Tasks are addressable and durable precisely because a counterparty may take minutes or hours, which a request/response tool call cannot express. input-requiredis the state that proves peerhood. The lab sends a deliberately underspecified request, and the specialist asks a clarifying question back rather than guessing or failing. A tool cannot do this: it has no standing to interrogate its caller. This single state is the clearest reason A2A is not "MCP with more steps."- The deliverable is an artifact with evidence. The completed task carries structured output and its message history, so the counterparty's answer arrives auditable, which matters enormously because you did not write it.
The simplifications are in the file's docstring (no streaming, no push notifications, no signed cards, no auth enforcement). None changes the shape; all of them would matter in production, and the missing signature is the one to notice, since the trust section below turns on it.
The honest 2026 accounting
Here the design review earns its keep by refusing the hype. A protocol existing, reaching 1.0, and gaining platform support does not make it load-bearing in production, and the candid picture is:
- MCP is load-bearing today. It solves a problem everyone has right now (connect an agent to tools), the ecosystem is large and real, and you built it because it is genuinely in use. Adopting MCP is a present-tense decision.
- A2A is real but early. It solves a problem fewer teams have yet, because most agent systems in 2026 are still single-organization: one company's agents doing one company's work, coordinating through a shared control plane where A2A adds nothing. The compelling cross-vendor cases, your procurement agent negotiating with a supplier's sales agent, exist and are growing, but they are the frontier, not the mainstream, and a platform built today is right to treat A2A as a capability to adopt when a real counterparty appears, not a foundation to build on speculatively.
This is not skepticism about the direction; the multi-vendor agent future is plausible and A2A is a credible standard for it. It is discipline about sequencing: build on what solves a problem you have, adopt standards for problems you are about to have, and do not architect around a future that has not arrived. The book has applied that rule throughout, and A2A is where it applies most sharply, because the temptation to build the multi-agent-internet is strongest exactly where the present need is weakest.
What to standardize, in order
Assembling the protocol advice from this part and Chapter 27 into a sequence a real platform can follow:
- Your own schemas first. The result and event schemas of your ledger are the platform's real internal protocol, entirely yours, and getting them right matters more than any external standard. Standardize these before looking outward.
- MCP for tools, now. It is mature, in use, and you understand it completely. Adopt it for shared, third-party, or separately-deployed tools; keep in-process tools in-process.
- A2A when a counterparty is real. When a genuine cross-boundary agent interaction appears, a supplier's agent, a partner's, a vendor's, A2A is the standard to reach for, and it will be more mature then than now. Until then, it is a capability you know exists, not a foundation you build on.
Part 10 closed: the platform meets the ecosystem
Part 10 read three pieces of the ecosystem against the platform the book built: Strands turned out to be thin conveniences over the Part 1 loop, giving you the loop's ergonomics while leaving the control plane yours; MCP was the tool protocol you already built, with genuine discovery economics and a "not for in-process tools" verdict; and A2A is the credible-but-early standard for the multi-vendor future. The through-line is the book's recurring posture, one last time: adopt the ecosystem's conveniences and standards where they solve a real problem, and keep as your own architecture the things, budgets, verification, isolation, scheduling, that no framework or protocol supplies. The frameworks structure a loop; the standards connect across boundaries; the platform is still yours to build, which is what the capstones put to work.
Don't be confused: a protocol vs a solution. A2A (or MCP, or any standard) is a protocol: an agreed way for two parties to talk. It is not a solution to the hard part, which is having two parties worth connecting and a reason to connect them. A standard makes the connection possible and portable; it does not make the cross-vendor agent interaction valuable, that comes from a real task that spans a real boundary. Adopting a protocol before you have the task is building a telephone network with no one to call, which is the exact failure mode the 2026 accounting warns against.
Full source
"""Lab 10.2: A2A from scratch. Agent cards and the task lifecycle.
MCP (Lab 3.0) connects an agent to TOOLS. A2A connects an agent to
another AGENT: a counterparty with its own judgment, its own tools, and
its own trust domain. That difference shows up in the wire protocol, and
this lab implements the shape end to end over real HTTP, no SDK:
1. DISCOVERY the server publishes an "agent card" at a well-known
URL: who it is, what skills it has, how to authenticate.
2. TASK the client creates a task (not a function call): a unit
of work with an id and a lifecycle.
3. LIFECYCLE submitted -> working -> completed | failed |
input-required (the counterparty can ask YOU a question)
4. ARTIFACT the finished task carries artifacts, the deliverable,
plus a message history.
The demo runs a real server in a thread and a real client over HTTP:
a coordinator discovers a specialist "incident analyst" agent, checks
its skills, hires it, polls the task to completion, and reads the
artifact. It also shows the input-required pause, which is the state
that makes A2A a conversation between peers rather than a call.
Deliberate simplifications vs the full spec (noted in the chapter):
no streaming/push notifications, no signed cards, no auth enforcement.
Standard library only. Deterministic.
"""
from __future__ import annotations
import json
import threading
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, HTTPServer
HOST, PORT = "127.0.0.1", 8931
BASE = f"http://{HOST}:{PORT}"
# --- the agent card: identity, skills, and how to reach it ----------------
# In the real spec this is served at /.well-known/agent-card.json so any
# client can discover a peer knowing only its domain.
AGENT_CARD = {
"protocolVersion": "0.3",
"name": "Incident Analyst",
"description": "Analyzes production incidents and names a root cause.",
"url": f"{BASE}/a2a",
"version": "1.0.0",
"capabilities": {"streaming": False, "pushNotifications": False},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json"],
"securitySchemes": {"bearer": {"type": "http", "scheme": "bearer"}},
"skills": [
{"id": "root-cause",
"name": "Root cause analysis",
"description": "Given metrics, deploys, and logs, name the cause.",
"tags": ["incident", "sre", "diagnosis"],
"examples": ["checkout p99 spiked at 02:00, what caused it?"]},
{"id": "blast-radius",
"name": "Blast radius estimate",
"description": "Estimate which services an incident affected.",
"tags": ["incident", "impact"]},
],
}
TASKS: dict[str, dict] = {} # the server's task store
def _analyze(text: str) -> dict:
"""The specialist's actual judgment (a real agent would call a model)."""
if "deploy" not in text:
return {"need": "Which deploys landed in the 2 hours before the spike?"}
return {"root_cause": "deploy v841 shrank the payments-db pool 50->10",
"evidence": ["p99 210ms -> 2930ms at 02:00",
"02:03 pool exhausted 10/10 (payments-db)",
"v841 deployed 01:55"],
"confidence": 0.91,
"recommendation": "roll back v841 or restore pool size 50"}
class A2AHandler(BaseHTTPRequestHandler):
def log_message(self, *_args): # keep the demo output clean
pass
def _send(self, obj: dict, code: int = 200):
body = json.dumps(obj).encode()
self.send_response(code)
self.send_header("content-type", "application/json")
self.send_header("content-length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self): # discovery
if self.path == "/.well-known/agent-card.json":
self._send(AGENT_CARD)
elif self.path.startswith("/a2a/tasks/"):
self._send(TASKS.get(self.path.rsplit("/", 1)[-1], {}))
else:
self._send({"error": "not found"}, 404)
def do_POST(self): # task creation and continuation
n = int(self.headers.get("content-length", 0))
req = json.loads(self.rfile.read(n) or b"{}")
if self.path == "/a2a/tasks": # message/send: create a task
task_id = f"task-{uuid.UUID(int=0xA2A0000 + len(TASKS) + 1).hex[-8:]}"
text = req["message"]["parts"][0]["text"]
task = {"id": task_id, "contextId": req.get("contextId", "ctx-1"),
"status": {"state": "submitted"},
"history": [req["message"]], "artifacts": []}
TASKS[task_id] = task
self._advance(task, text) # do the work
self._send(task)
elif self.path.startswith("/a2a/tasks/") and self.path.endswith("/send"):
task = TASKS[self.path.split("/")[-2]]
text = req["message"]["parts"][0]["text"]
task["history"].append(req["message"])
self._advance(task, text) # resume from input-required
self._send(task)
else:
self._send({"error": "not found"}, 404)
@staticmethod
def _advance(task: dict, text: str) -> None:
task["status"] = {"state": "working"}
joined = " ".join(p["text"] for m in task["history"]
for p in m["parts"])
result = _analyze(joined)
if "need" in result: # the peer asks US a question
task["status"] = {"state": "input-required"}
task["history"].append({"role": "agent",
"parts": [{"text": result["need"]}]})
else:
task["status"] = {"state": "completed"}
task["artifacts"] = [{"artifactId": "art-1", "name": "analysis",
"parts": [{"data": result}]}]
# --- the client side: discover, hire, poll, read -------------------------
def get(url: str) -> dict:
with urllib.request.urlopen(url) as r:
return json.load(r)
def post(url: str, payload: dict) -> dict:
req = urllib.request.Request(url, data=json.dumps(payload).encode(),
headers={"content-type": "application/json",
"authorization": "Bearer demo"})
with urllib.request.urlopen(req) as r:
return json.load(r)
def message(text: str) -> dict:
return {"role": "user", "messageId": uuid.UUID(int=7).hex[:8],
"parts": [{"text": text}]}
def main() -> None:
server = HTTPServer((HOST, PORT), A2AHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
print("--- 1. discovery: fetch the peer's agent card ---")
card = get(f"{BASE}/.well-known/agent-card.json")
print(f" GET /.well-known/agent-card.json -> {card['name']} "
f"v{card['version']}")
print(f" auth: {list(card['securitySchemes'])} | "
f"streaming: {card['capabilities']['streaming']}")
for s in card["skills"]:
print(f" skill: {s['id']:<13} {s['description']}")
print("\n--- 2. capability match: can this peer do the job? ---")
want = "root-cause"
ok = any(s["id"] == want for s in card["skills"])
print(f" need {want!r} -> {'match, hiring it' if ok else 'no match'}")
print("\n--- 3. hire it: create a task (underspecified on purpose) ---")
task = post(f"{BASE}/a2a/tasks",
{"message": message("checkout p99 spiked at 02:00, why?")})
print(f" POST /a2a/tasks -> task {task['id']} "
f"state={task['status']['state']}")
print(f" the peer asked: "
f"{task['history'][-1]['parts'][0]['text']!r}")
print(" (input-required is what makes this a PEER, not a tool: "
"it can question you)")
print("\n--- 4. answer it: continue the same task ---")
task = post(f"{BASE}/a2a/tasks/{task['id']}/send",
{"message": message("deploy v841 at 01:55 changed db pool "
"config 50->10; logs show pool exhausted")})
print(f" POST /a2a/tasks/{task['id']}/send -> "
f"state={task['status']['state']}")
print("\n--- 5. read the artifact (the deliverable) ---")
art = task["artifacts"][0]["parts"][0]["data"]
print(f" root cause : {art['root_cause']}")
print(f" confidence : {art['confidence']}")
for e in art["evidence"]:
print(f" evidence : {e}")
print(f" recommends : {art['recommendation']}")
print("\n--- 6. the task is durable and inspectable ---")
fetched = get(f"{BASE}/a2a/tasks/{task['id']}")
print(f" GET /a2a/tasks/{task['id']} -> state="
f"{fetched['status']['state']}, "
f"{len(fetched['history'])} messages, "
f"{len(fetched['artifacts'])} artifact(s)")
print("\nMCP called a tool and got a value back. A2A opened a TASK with "
"a peer that has its own judgment, asked us a clarifying question, "
"and returned an artifact. That lifecycle is the whole difference.")
server.shutdown()
if __name__ == "__main__":
main()
👉 Next, per the map: Part 11's capstones put the whole platform to work on four real jobs, a repository-audit fleet, an incident responder, a document-intake swarm, and a research service, each stating its production gates and ending in a costed, torn-down build.