Demo of the A2A (Agent2Agent) protocol with the official a2a-sdk (v1.1.0).
It shows the two situations side by side:
- You calling an agent (a plain client → agent).
hello/translator/math. - An agent intelligently routing to another agent — the real Agent2Agent — where the Orchestrator uses an LLM (OpenAI) to pick the right worker agent for your request and delegates to it over A2A.
A2A is an open protocol (created by Google, donated to the Linux Foundation) that
lets AI agents discover (via an AgentCard) and communicate with each
other, regardless of the framework each one is built with.
Independent A2A agents, each its own HTTP server with its own AgentCard.
The Orchestrator is a smart router: at routing time it discovers each
worker's real AgentCard over A2A, builds the capability catalog from those
cards (not from any hardcoded text), asks an LLM which worker fits the user's
message, then delegates to it over A2A. config.py only stores where each
worker is (URL) — what it does always comes from its live card.
graph TD
subgraph clients["clients/"]
CLI["a2a-client (CLI)"]
end
subgraph orchestrator["orchestrator/ :8883"]
OA["OrchestratorAgent"]
RT["router (OpenAI)<br/>discovers cards + picks worker + normalizes input"]
OA --> RT
end
subgraph workers["worker agents (each owns its AgentCard)"]
TA["Translator :8882<br/>card: ES → EN"]
MA["Math :8884<br/>card: arithmetic"]
end
subgraph shared["shared/"]
BE["BaseTextExecutor"]
SA["server_app"]
CALL["call_agent() / fetch_card()"]
CFG["config (WORKERS = key+URL only)"]
ENV[".env → OPENAI_API_KEY"]
end
CLI -->|A2A| OA
RT -.reads URLs.-> CFG
RT -.auth.-> ENV
RT -->|"GET /.well-known card"| TA
RT -->|"GET /.well-known card"| MA
OA -->|"A2A (chosen worker)"| TA
OA -->|"A2A (chosen worker)"| MA
TA -.uses.-> BE
MA -.uses.-> BE
OA -.uses.-> CALL
BE -.served by.-> SA
| Folder | What it is |
|---|---|
shared/base_executor.py |
The A2A task lifecycle (WORKING → artifact → COMPLETED), reused by all agents. |
shared/server_app.py |
Builds the AgentCard + Starlette routes + uvicorn. |
shared/a2a_call.py |
fetch_card(url) (discover an agent's real card) + call_agent(url, text) (send it a message). |
shared/router.py |
LLM router (OpenAI): discovers worker cards, builds the catalog from them, picks the worker and normalizes the input. Reads OPENAI_API_KEY from .env. |
shared/config.py |
Ports/URLs + WORKERS (just key + url — capabilities come from each card). |
agents/*/agent.py |
The brain of each agent (swap for a real LLM). |
agents/*/main.py |
Wires brain + card + executor and serves it. |
clients/cli.py |
Generic CLI to talk to any agent. |
When you send the Orchestrator a request, it first discovers each worker's real card, builds the catalog from those cards, asks the LLM which worker fits (and how to phrase the input), then delegates over A2A.
sequenceDiagram
actor User
participant O as Orchestrator :8883
participant M as Math :8884
participant T as Translator :8882
participant LLM as OpenAI (router)
User->>O: "cuánto es 3 * (4 + 5)?"
rect rgb(255, 245, 235)
Note over O,T: Discover capabilities from REAL cards
O->>M: GET /.well-known/agent-card.json
M-->>O: card (skills: arithmetic)
O->>T: GET /.well-known/agent-card.json
T-->>O: card (skills: ES→EN)
end
O->>LLM: message + catalog built from those cards
LLM-->>O: {agent: "math", input: "3 * (4 + 5)"}
rect rgb(235, 245, 255)
Note over O,M: Agent → Agent (A2A delegation)
O->>M: A2A message: "3 * (4 + 5)"
M-->>O: artifact: "3 * (4 + 5) = 27"
end
O-->>User: chose math → 27
The capability catalog is never hardcoded — it is read from each agent's live
AgentCard. Change a worker's skill description and the router sees it on the
next request, no orchestrator changes needed. The LLM only decides and
normalizes; the work happens in the chosen worker, reached over A2A.
uv sync cp .env.example .env # then put your real key in .env # .env contains: OPENAI_API_KEY=sk-...
.env is gitignored — your key never gets committed.
The Orchestrator needs its worker agents running. Open three terminals for the workers + orchestrator:
uv run translator-agent # terminal 1 (:8882) uv run math-agent # terminal 2 (:8884) uv run orchestrator-agent # terminal 3 (:8883)
Then talk to the orchestrator — it routes automatically:
uv run a2a-client orchestrator "cuánto es 3 * (4 + 5)?" # → math → 27 uv run a2a-client orchestrator "sumame 2 + 2 por favor" # → math → 4 uv run a2a-client orchestrator "el sol es una estrella" # → translator → the sun is a star
You can also hit each worker directly (no routing):
uv run a2a-client math "3 * (4 + 5)" uv run a2a-client translator "el sol es una estrella"
Or the simplest possible standalone echo agent:
uv run hello-agent # terminal 1 uv run a2a-client hello "hi there" # terminal 2
The AgentCard is published at the A2A well-known path:
curl http://127.0.0.1:8884/.well-known/agent-card.json # math agent's card- Create
agents/<name>/agent.py(the brain) +main.py(copy an existing one). Describe what it does in itsAgentSkill— that text is what the router reads. - Add a port + URL in
shared/config.pyand append aWorkerInfo(key, url)toWORKERS(just where it lives — no description, the card provides that). - Register its entry point in
pyproject.toml.
The router discovers its card and starts considering it automatically — no orchestrator or router changes.
In any agents/<name>/agent.py, replace the body of invoke with a call to your
LLM, a tool, or a LangGraph/CrewAI/ADK graph. The whole A2A layer (discovery,
tasks, artifacts, routing, delegation) stays exactly the same.