Multi-agent with A2A: a worked example
A runnable three-agent system — a researcher, a critic, a writer — orchestrated over A2A, all thinking through one relay.
The whole example lives in
docs/examples/multiagent/,
with a justfile that starts everything. Its
README
is the runnable quickstart; this page is the argument for why it is built the
way it is.
Why A2A, and not three function calls
The honest question about any multi-agent demo is: why is this a protocol and not three function calls? Most of the time it should be function calls — if every agent is yours, in one language, in one process, a protocol is pure overhead.
So this example is deliberately built where a function call cannot reach.
Two of its agents are shell scripts served by the Go a2a CLI; the third is a
Python program on the Python a2a-sdk. And there are two orchestrators — one
that is the Go CLI, one that is the Python SDK — either of which drives all
three agents without knowing which language any of them is written in.
A Go binary cannot call a Python function. A Python process cannot call into a
Go binary's shell script. Something has to carry the task across that boundary,
and that something is A2A. Swap orchestrate.sh for orchestrate.py and
nothing changes — that is the whole point, and it is what makes this A2A rather
than three function calls in a trench coat.
flowchart LR
orchgo(["orchestrate.sh<br/>(Go a2a CLI)"])
orchpy(["orchestrate.py<br/>(Python SDK)"])
subgraph agents["agents — one A2A server each, its own card"]
direction TB
res["researcher<br/>:9101 · shell / Go CLI"]
crit["critic<br/>:9102 · PYTHON"]
wri["writer<br/>:9103 · shell / Go CLI"]
end
relay["agent-relay"]
sub["Anthropic subscription"]
ollama["local Ollama<br/>(free, offline)"]
orchgo -- "A2A" --> agents
orchpy -- "A2A" --> agents
res --> relay
crit --> relay
wri --> relay
relay -- "sonnet, haiku…" --> sub
relay -- "granite4.1:3b" --> ollama
One relay, several agents
The instinct to run several relays — one per agent — is the wrong one, and it is worth saying why.
A multi-agent system is interesting because its agents differ by role: a researcher, a critic, a writer. They do not become interesting by differing in which model they run on. Several relays would give you several agents with the same role and different brains — the least useful axis to vary.
So the specialisation belongs in the agents, and the relay stays what it is: the thing that thinks, underneath all of them. An agent's whole identity here is a system prompt plus a model.
Several relays do have legitimate uses — but they are about deployment, not composition: a relay with agentic execution enabled and one without are two different privilege boundaries, and that separation is a process-level thing.
What the relay buys you: a brain per agent, priced per job
Each agent picks its model from the same relay, with the same token. The critic
(judgement) and the writer (prose) are worth the subscription. The researcher is
not — gathering is not where the money should go, so RESEARCHER_MODEL can point
at a local Ollama model (granite4.1:3b, via just run-hybrid): free,
offline, no data leaves the machine.
| agent | model | cost |
|---|---|---|
| researcher | granite4.1:3b (local) |
$0 |
| critic | sonnet |
earns the subscription |
| writer | sonnet |
earns the subscription |
One word of config apart. That is a real orchestration decision — spend where it matters — and it is the relay that makes it a one-word change.
The agents are real A2A servers
Each agent publishes its own Agent Card and serves the JSON-RPC binding. The orchestrator never imports them: it is handed three base URLs, reads the cards to learn what each agent is, and delegates over the protocol — exactly as it would to an agent someone else wrote, on another machine, in another language. Here, two of them genuinely are in another language.
Two details bite anyone wiring heterogeneous agents together, both handled in the
example and written up in
upstream-bugs.md:
the Go CLI's auto-generated Agent Card omits fields the proto marks REQUIRED
(so the example serves hand-written --card files), and the two SDKs default to
different transports (so the example pins everything to JSONRPC). An A2A reply is
also a oneof — a Task with artifacts or a Message with parts; the
Go-served agents return the first, the Python agent the second. Both
orchestrators read both shapes, because a client that reads only one breaks
against half the ecosystem.
Running it
Start a relay in the repo root (just run, or just run-hybrid if the
researcher points at a local model), then from docs/examples/multiagent/:
export RELAY_URL=http://127.0.0.1:18082
export RELAY_TOKEN=$(just print-token) # from the repo root
just setup # install the Go a2a CLI and the Python SDKs into .venv
just agents # start all three agents
just both "Why keep a personal inference relay on loopback?"
just both runs both orchestrators over the same three agents — the Go
CLI first, then the Python SDK. Neither knows which agent is written in which
language; both produce the same three-stage result. just run-go and
just run-py run them one at a time; just clean stops the agents.
What it does
[1] researcher — shell agent, served by the Go CLI
**Privacy engineer**: Keep sensitive data on-machine without transmission …
**Cost manager**: Eliminate inference API charges during development …
[2] critic — PYTHON agent, on the Python SDK
**Cost manager** — "eliminate charges" is true only for locally-hosted
models; if the relay proxies to a remote paid API, costs aren't eliminated,
just intercepted. *Falsified if* the relay's backend is a metered endpoint.
[3] writer — shell agent again
**Cost manager**: Avoids inference API charges during dev/test — genuinely
true for a locally-hosted model, but not if the relay just proxies to a
metered cloud backend.
The critic earns its keep: it catches the researcher's silent assumption that a loopback relay is always paired with a local model, and the writer folds that correction into the final answer. Three roles, two languages, one relay.
Where to go next
- Give an agent tools. These three only talk. An agent that acts sends
tools[]and runs the client-tool loop — the relay serves it on both wires. - Let an agent run code on the host. Present the agentic credential and the relay executes in a workspace, returning the files as artifacts. That is what the relay's own A2A adapter exposes — useful when your relay is the peer in someone else's network, which is a different job from the one on this page.
- Route by difficulty, not by hand. Here the model choice is fixed per agent. A triage agent (local, free) could decide which brain each task deserves.
This is an example, not a framework
The relay is not an agent framework, and this page does not make it one. If you want persistence, retries, planning and human-in-the-loop, reach for a real orchestration framework (Google ADK has first-class A2A support, LangGraph and CrewAI are options) and point its agents at the relay exactly as these three do.