Production-ready agents, Part 6.5: Multi-agent topologies beyond a router
A router is only one way to connect agents. This part maps eight multi-agent topologies on two simple axes, shows each one on the Acme system with its ADK building block and its main risk, and explains how to combine them without losing control of cost, loops and ownership.
In Part 5 we built the router for Acme Consumer Brands, and in Part 6 we connected it to the marketing and supply specialists over A2A. That gives us one working shape: code routes the question, remote specialists answer, and a writer model turns their findings into one checked answer.
Once a system like this is running, the requests start. The pricing team wants its own agent. Someone asks whether a second model could review the writer’s draft. A partner asks whether their agent could talk to ours. Each request is really a question about topology, the shape of how agents are connected, and each shape has a different cost, a different failure mode and a different owner.
This in-between part steps back from the build. I map the common topologies on two axes, explain eight of them on the Acme example, show how they combine, and finish with a decision guide.
If you prefer to watch first, here is the heart of this part as a short animated explainer.
Video: eight ways to connect agents, and how to combine them without losing control, in three minutes. The narration is a synthetic voice; the script and animation are mine.
Where are we in the architecture?
This part steps back and compares multi-agent topologies, using the router and its two specialists as the example. Part 1.25 walks through the whole picture, layer by layer.
where Part 6.5 sits in the Acme architecture: highlighted boxes are the subject of this part, solid boxes come from earlier parts, faded boxes arrive later
What do I need to know first?
These are the terms the rest of the post uses.
- Multi-agent system. An application in which more than one agent (a model with instructions, tools and a loop, as defined in Part 1) works on the same request.
- Orchestration. Deciding which agent or step runs next and passing work between them. The part of the system that does this is the orchestrator.
- Delegation. One agent handing part of a task to another agent. Transfer (also called a handoff) is delegation where control moves to the other agent, so it answers instead of the caller.
- Control. Which agent currently holds the turn, and so whose reply the user will see.
- State. The data a request carries between steps. In ADK this is session state, a dictionary of named keys stored with the conversation.
- In-process or remote. In-process agents run inside one application and call each other in memory. Remote agents run as separate services and are called over the network, in this series with A2A (the Agent2Agent protocol from Part 6).
As a reminder, this is the router we built, in five lines:
question
-> extract (model, Flash) rewrite the question as one self-contained line
-> route (code) marketing | supply | both | decline
-> specialist(s) (remote, A2A) both = run in parallel, then a JoinNode
-> writer (model, Pro) one answer from the findings
-> number check (code) after-model callback; replace the answer if a figure is unmatched
Part 5 explains every node and Part 6 every A2A call.
What is a multi-agent topology?
The short answer: a topology is who calls whom, who holds the data, and who decides the next step. The third of those, code or a model, matters far more than how many agents there are.
In simple words, a topology is the shape of a multi-agent system: who calls whom. The word is used the same way in networking, where a star or a mesh topology describes how machines are wired together.
A more precise definition, and the one I use, is that a multi-agent topology is the arrangement of agents together with three rules:
- Who calls whom. This is the control flow: which agent starts, which agents it can reach, and whether control comes back afterwards.
- Who holds the state. This is the data flow: whether agents share one session state, pass messages, or keep their own private state.
- Who decides the next step. This is the decision: a piece of code, or a model reading the conversation.
The third rule matters most, because it decides how predictable the system is. Anthropic’s widely read guide Building effective agents draws the same line. It calls systems where “LLMs and tools are orchestrated through predefined code paths” workflows, and systems where models direct their own process agents. It also recommends “finding the simplest solution possible, and only increasing complexity when needed”.
That gives two axes that place almost every topology:
- Vertical: who decides the next step? Code (a fixed order, a route function, a loop counter) or a model (a coordinator reading the request and choosing).
- Horizontal: where do the agents run? In-process, sharing memory and session state, or remote, as separate services over A2A.
eight multi-agent topologies on two axes: whether code or a model decides the next step, and whether the agents run in one process or as remote services over A2A; the Acme router sits where code decides and the specialists are remote
The Acme router sits in the bottom right: every decision is code, and only the work is distributed. That is a deliberate choice from Part 5, where the series rule was set: Models read and write language. Code makes the decisions.
Before Acme, here are the same three rules on something everyone has seen: a table ordering a starter and a main in a small restaurant.
Restaurant A (a fixed rule decides)
1. Who calls whom the waiter passes the ticket to the grill and the pastry station; dishes come
back to the pass, and the waiter carries them out
2. Who holds state the order ticket on the rail; each station keeps its own prep notes
3. Who decides the till: anything marked "grill" prints at the grill, every time
Restaurant B (a person decides)
1. Who calls whom the head chef reads each ticket and calls out who cooks what
2. Who holds state one shared whiteboard the whole kitchen writes on
3. Who decides the head chef, judging each ticket, so a busy night can go differently
The stations, the waiter and the ticket are the agents and the state. The till printing by rule is code deciding; the head chef judging is a model deciding. Same kitchen, same cooks, a very different amount of predictability.
How does this apply to Acme? Here are the three rules applied to one question, Maria’s “Compare the Acme Pure Refill Launch spend with what the stockouts cost us”:
Acme router (code decides, remote specialists)
1. Who calls whom the router calls marketing and supply over A2A; control comes back to the router
2. Who holds state the router: marketing_findings and supply_findings in its session state;
each specialist keeps its own private session
3. Who decides route(), a Python function, returns "both"
The same question under a coordinator (model decides, in-process)
1. Who calls whom the coordinator model calls transfer_to_agent(agent_name="supply"), and maybe
marketing too, if it thinks of it
2. Who holds state one shared session state for all of them
3. Who decides the coordinator model, reading the question and the agents' descriptions
Only rule 3 moves the system on the map: the same agents with a model deciding sit in the top half instead of the bottom. That is the move Part 5 decided against. The common misconception is that adding agents is what makes a system “multi-agent” and harder to run; in practice, handing the decision to a model is what changes how you test it, as Restaurant B shows.
Different frameworks use different names for the same shapes. ADK’s
workflows overview lists four ways to build them: graph-based workflows
and dynamic workflows (new in ADK 2.0), collaborative workflows (a coordinator agent with
sub-agents) and template workflows (SequentialAgent, ParallelAgent and LoopAgent). LangChain’s
multi-agent guide uses subagents, handoffs,
skills, router and custom workflow. I use the common industry names below, with the ADK building block
for each. One honest note: as of October 2026, ADK’s documentation says that for Python and Go the
template workflow agents “have been superseded” by graph-based and dynamic workflows. They still work
and explain each pattern clearly, so I name both.
Where do subagents, graphs, hierarchies and crews fit?
Those four words come up in every framework comparison, and they are not four rival architectures. They are framework vocabulary, not new shapes, in the way “lift” and “elevator” name the same machine. Each is a framework’s name for one or more of the eight topologies below. Here is the map, with an Acme example of each:
Term you hear What it is Topology below Acme example
------------------- ----------------------------------------- ------------------ ------------------------------
Subagent an agent another agent hands work to; 3 (hierarchical) a pricing desk agent that hands
either it takes over the conversation or 4 (agent as "margin" questions to a margin
(ADK sub_agents, transfer_to_agent) or a tool) agent; or the supply agent
it is called like a tool and returns calling contract_reader as a
(ADK AgentTool) tool
Graph steps and arrows that code defines; 1, 2 and 5, the router: front -> extract
ADK Workflow, LangGraph StateGraph joined by code -> route -> specialists ->
writer (Part 5)
Hierarchy a supervisor model that picks which 3 (hierarchical) rejected for the main path:
agent below it acts, and agents below with 6 (handoff) a model would choose between
can have their own agents when control marketing and supply
moves sideways
Crew CrewAI's team of agents with roles and 1 (sequential "analyst writes, reviewer
tasks, run by a Process: sequentially, process) or 3 checks" as a sequential crew;
or hierarchically with a manager (hierarchical a manager agent delegating
model or agent that delegates process) tasks is the hierarchical one
So when someone asks “should we use a crew or a graph?”, the real question underneath is rule 3
from above: who decides what runs next, code or a model? A sequential crew and a graph both let
code decide the order. A hierarchical crew, a supervisor and an ADK coordinator with sub_agents all
let a model decide. Acme’s router is a graph because the decision about which team answers is a
business rule (Part 5); models work only
inside the steps. Part 1.75 shows the
same step written in ADK, LangGraph, CrewAI and three other frameworks.
What are the eight topologies?
Each topology below is explained the same way: what it is, a simple everyday example, how it applies to Acme, when to choose it, the ADK building block, and its main risk. The diagrams use the same colours throughout: blue boxes call a model, white boxes are code, and dashed boxes are remote agents reached over A2A.
1. Sequential pipeline
What it is. A sequential pipeline runs steps in a fixed order, and each step’s output becomes the next step’s input. Anthropic calls the model-only version prompt chaining.
A simple example. A car wash: soap, then rinse, then dry. Rinsing before the soap is pointless and drying before the rinse is worse, so each stage waits for the one before and takes the car exactly as that stage left it. The stages are the steps, the car is the data passed along, and the fixed order is written into the building, not chosen per car.
How does this apply to Acme? The router’s main line is a sequential pipeline: extract, then route, then a specialist, then the writer. No step can start before the one before it has finished, because each needs the previous result.
a sequential pipeline: extract, then route, then the supply agent, then the writer, each step passing its output to the next
When to choose it. When the steps are known in advance and each depends on the one before. It is the easiest shape to test, because every run takes the same path.
The ADK building block. The template version is
SequentialAgent, which runs its
sub_agents in order. In ADK 2.0 the same thing is a graph workflow whose edge
lists the nodes in order, such as ("START", extract, route). Steps pass data either directly (each node
receives the previous output as node_input) or through session state.
The main risk. Waiting time adds up, and one slow or failing step holds up everything after it.
2. Parallel fan-out and gather
What it is. A fan-out sends independent work to several agents at the same time; a gather (or fan-in) waits for all of them and combines the results. Anthropic calls this parallelization and describes two kinds: sectioning (different subtasks in parallel) and voting (the same task several times, to compare answers).
A simple example. Cooking rice (20 minutes) and a curry (15 minutes) for dinner. On two hobs at once, dinner is ready at 20 minutes, the slower dish, not 35. Plating waits until both pans are done. If both cooks scribble their notes on the same line of one recipe card, the second note rubs out the first. The hobs are the branches, plating is the gather, and the recipe card is session state.
How does this apply to Acme? The router’s both branch already does this. When Maria asks “Compare the Acme
Pure Refill Launch spend with what the stockouts cost us”, marketing reads BigQuery and supply reads
Databricks at the same time, and a JoinNode waits for both.
parallel fan-out and gather: the route sends one question to the marketing and supply agents at the same time, a JoinNode waits for both, and the writer combines them
When to choose it. When the subtasks are independent and the user is waiting: they wait for the slowest branch instead of the sum of all of them.
The ADK building block.
ParallelAgent runs its sub-agents
concurrently, usually followed by an agent that combines their outputs. In a graph workflow, a tuple of
nodes in an edge fans out and a JoinNode fans in.
The main risk. ADK’s documentation is explicit that each branch runs independently, with “no
automatic sharing of conversation history or state between these branches during execution”. So two
branches must write to distinct state keys (marketing_findings and supply_findings), or one result
silently overwrites the other.
Here is that failure on Maria’s compare question (toy figures, the same ones as the Try it below):
Both branches write "findings"
marketing state["findings"] = "Acme Pure spend: EUR 283,590"
supply state["findings"] = "Service penalties: EUR 35,600" (finishes last, wins)
writer sees only the penalties; the spend is gone, and no error is raised
Each branch writes its own key
marketing state["marketing_findings"] = "Acme Pure spend: EUR 283,590"
supply state["supply_findings"] = "Service penalties: EUR 35,600"
writer sees both and can compare them
Which branch “wins” depends on which finishes last, so the bug can come and go between runs. That is why the key names are fixed in code, not left to each agent.
3. Hierarchical supervisor
What it is. A supervisor (ADK calls it a coordinator) is an LLM agent that reads each request and delegates it to one of its sub-agents. Sub-agents can have their own sub-agents, which makes a tree, so the pattern is also called hierarchical task decomposition. Anthropic’s name for the flexible version is orchestrator-workers: the orchestrator decides the subtasks at run time instead of following a fixed list.
A simple example. A family doctor (a GP) hears your symptoms and decides, there and then, which
specialist you need: a cardiologist one day, a dermatologist the next. Either the specialist takes over
your care and you deal with them from now on, or they send a report back and you return to the GP. The
GP is the supervisor, the specialists are sub-agents, and “takes over” versus “reports back” is exactly
the "chat" versus "task" mode below. The GP’s choice is a judgement, so two GPs may refer the same
patient differently.
How does this apply to Acme? At Acme, this is how it would work with a pricing desk agent. Pricing questions vary a lot: one needs promotion history, another competitor prices, another the margin model. A supervisor model reads the question and passes it to the right sub-agent.
a hierarchical supervisor: the pricing desk agent reads the request, transfers it to one of its sub-agents, and control comes back to the parent when the sub-agent finishes
When to choose it. When you cannot list the subtasks in advance and the choice depends on understanding the request. If a rule can make the choice, a route function is cheaper and more predictable, which is the argument Part 5 made for the top of the router. The usual mistake is to reach for a supervisor simply because there are several agents; a hospital receptionist sending every “broken arm” to fracture clinic needs a rule, not a GP.
The ADK building block. An Agent with sub_agents=[...]. The model delegates by calling
transfer_to_agent(agent_name=...), and ADK switches control to that agent, as the
multi-agent patterns guide shows. ADK 2.0
collaboration modes add a mode setting on each sub-agent:
"chat" (the default) keeps control until it transfers back, "task" returns automatically when the task
is finished, and "single_turn" returns at once with its result and can run in parallel. One honest gap:
as of October 2026 the documentation says task mode is disabled inside graph-based workflows in ADK
Python 2.0.0, and is expected to come back in a later release.
Where does mode go: on the child or the parent? On the child. ADK has no separate sub-agent class
and no is_sub_agent=True flag; every agent is an ordinary Agent(...). An agent only becomes a
sub-agent when a parent lists it in sub_agents=[...]. Settings about how the child behaves once it has
control, mode, disallow_transfer_to_parent and disallow_transfer_to_peers, are part of the child’s
definition. The parent owns only the list:
from google.adk.agents import Agent
# Step 1: an ordinary agent. Mode and transfer limits live HERE, on the child.
margin_model = Agent(
name="margin_model",
model="gemini-2.5-flash",
description="Computes promotion margin for one brand and month.",
instruction="Compute the margin, then finish the task.",
mode="task", # hand control back to the parent when done
disallow_transfer_to_peers=True, # never hop sideways to promo_history
)
promo_history = Agent(
name="promo_history",
model="gemini-2.5-flash",
description="Past promotions and their sales uplift.",
)
# Step 2: listing them in the PARENT's sub_agents is what makes them sub-agents.
pricing_desk = Agent(
name="pricing_desk",
model="gemini-2.5-flash",
instruction="Send each pricing question to the right specialist.",
sub_agents=[margin_model, promo_history],
)
When you wire agents together, ADK enforces a few tree rules. I checked each one on ADK Python 2.13.0 by building this tree and printing what each agent can reach:
- One parent only. Listing
margin_modelin a second parent’ssub_agentsraises aValueErrorwhen the agent is built: “Agentmargin_modelalready has a parent agent, current parent:pricing_desk”. - The child’s mode decides how the parent reaches it. A
"chat"sub-agent is reached withtransfer_to_agent. A"task"or"single_turn"sub-agent is not offered totransfer_to_agentat all. Instead, ADK adds a tool named after the child to the parent, sopricing_deskcallsmargin_model(request=...)like a function and gets control back. In the test,pricing_desk’s transfer targets were['promo_history']and its tools were['margin_model']. - Transfers are scoped to the tree around the agent.
transfer_to_agentoffers only three kinds of target: the agent’s own chat-mode children, its parent (unlessdisallow_transfer_to_parent=True) and its chat-mode siblings (unlessdisallow_transfer_to_peers=True). ADK lists exactly those names in the instruction and as anenum(a fixed list of allowed values) in the tool’s schema, so the model is never shown any other agent. An agent defined in the same file but never put in the tree is out of scope. A name that is not in the tree at all fails at run time with “Agent … not found in the agent tree”. One honest nuance: the narrowing happens in what the model is offered. The run-time lookup itself only checks that the name exists somewhere in the tree. - Nesting is just more of the same. If
margin_modelneeds a helper, give itsub_agents=[tax_calculator]. The test printed each agent’s transfer targets:
pricing_desk -> promo_history (margin_model is a tool, because it is in task mode)
margin_model -> tax_calculator, pricing_desk (no peers: disallow_transfer_to_peers=True)
promo_history -> pricing_desk (margin_model is task mode, so not a peer target)
tax_calculator -> margin_model (its parent; it has no siblings)
pricing_desk can never transfer straight to tax_calculator: a grandchild is not in its scope.
The main risk. Every request pays for an extra model call before any work starts, and the supervisor can choose differently on different days.
Here is the pricing desk on one question, “What margin did Harvest Row make on last month’s promotion?”:
1. pricing_desk (model) reads the question, calls transfer_to_agent(agent_name="margin_model")
2. margin_model answers with the margin
3a. mode "chat" margin_model keeps the turn: Maria's next message goes straight to it
3b. mode "task" when margin_model finishes, control returns to pricing_desk, which
takes Maria's next message
4. Another day the same question, and the model transfers to promo_history instead
Step 1 is the extra model call, and step 4 is the risk: the choice was made by a model, so a unit test cannot pin it down. Only an eval over many runs can tell you how often it picks right.
4. Agent as a tool
What it is. In agent as a tool, one agent calls another exactly as it would call a function. The inner agent does its job and returns a result, and the caller keeps control: it reads the result, decides what to do and answers the user itself.
A simple example. A sales manager gets a letter in German, asks a translator colleague for an English version, reads it, and writes and signs the reply herself. The translator never speaks to the customer. If the manager instead put the translator on the phone with the customer, the translator would own that conversation. The first is agent as a tool; the second is a sub-agent transfer.
How does this apply to Acme? At Acme, this is how it would work: the supply agent needs to explain why a penalty applies, which means reading the service-level clause in a retailer contract. Reading contract text is a language task, so the supply agent calls a small contract reader agent as a tool. The supply agent still owns the answer it sends back to the router, as the manager owns her reply.
agent as a tool: the supply agent calls a contract reader agent wrapped in AgentTool, gets its answer back as a tool response, and keeps control of the conversation
When to choose it. When the inner job is bounded (one question in, one result out) and the caller must stay responsible for the final answer. LangChain now recommends this tool-calling style for most supervisors; the README of its langgraph-supervisor library says it recommends “using the supervisor pattern directly via tools” because it gives more control over context.
The ADK building block. Wrap the inner agent in
AgentTool and list it in the caller’s tools. ADK’s
documentation spells out the difference from a sub-agent: with a tool, the inner agent’s “answer is
passed back to Agent A”, which “retains control”; with a sub-agent, “the responsibility of answering
the user is completely transferred to Agent B”. AgentTool also accepts skip_summarization=True, which
returns the inner agent’s output without the caller’s model rewording it.
The main risk. The inner agent’s model calls are hidden behind one tool call, so its cost and time are
easy to overlook. Count each AgentTool as a full agent.
Here is one call through the contract reader, with the clause text kept generic:
1. Router -> supply "Why did Acme Pure pay service penalties in Western Europe?"
2. Supply (model) finds the penalty figure with its own query, then calls
contract_reader(request="Which clause sets the OTIF penalty for this retailer?")
3. contract_reader returns, as a tool response: a short summary of the service-level clause
4. Supply (model) combines the clause with its own EUR figure into one finding
5. Supply -> router one finding: the figure and the reason, owned by supply
As a sub-agent instead, step 3 would transfer control: the contract reader would answer the router itself,
with the clause and no penalty figure. With skip_summarization=True, step 3’s text would come back word
for word, without the supply model rewording it.
5. Loop and critic
What it is. A loop repeats steps until a condition is met. The most common version pairs a generator with a critic (a second model that reviews the draft and gives feedback), and repeats until the critic is satisfied. Anthropic calls this evaluator-optimizer; ADK’s guide calls it iterative refinement.
A simple example. A reporter files a story, the editor sends back one note (“lead with the result”), the reporter revises, and at the print deadline the story runs whether or not the editor is delighted. The reporter is the generator, the editor is the critic, and the deadline is the cap. Note who stops the loop: not the editor’s mood, but the clock.
How does this apply to Acme? The writer’s after-model callback already checks every figure, and that stays in code. A critic could check the things code cannot: does the draft answer the question that was asked, and does it lead with the answer? If not, the writer gets one more try with the critique, and then the loop stops whatever happens.
loop and critic: the writer drafts, a critic reviews, and a code gate either sends the draft back with the critique or delivers it, with a hard cap of two rounds
When to choose it. When there are clear review criteria and a second attempt measurably improves the result. If code can check the property (a number, a format, a schema), use code, as Part 5 argued.
The ADK building block. LoopAgent repeats its
sub_agents and takes max_iterations. A sub-agent stops the loop early by escalating: a tool sets
tool_context.actions.escalate = True, or a custom agent yields an event with escalate=True. In a graph
workflow a loop is a back-edge, an edge from a later node back to an earlier one, chosen by a route
value.
The main risk. A loop that never ends. ADK’s documentation is direct about it: “the LoopAgent itself
does not inherently decide when to stop looping”, so you must set the stopping rule. Always set a hard
cap in code. ADK’s run configuration has a backstop,
max_llm_calls, which defaults to 500 model calls per run, far too many for one business question.
Two quick cases show both stopping rules:
LoopAgent(sub_agents=[writer, critic], max_iterations=2)
Round 1 writer drafts; the critic is satisfied, so its tool sets
tool_context.actions.escalate = True
Result the loop stops after one round (2 model calls)
The same loop with no max_iterations and a critic that is never satisfied
Each round 2 model calls (writer + critic)
Stops at about round 250, when the run reaches the default max_llm_calls of 500
The first loop was stopped by code reacting to the critic; the second was stopped only by a backstop that was never meant for one question. Set the cap yourself.
6. Handoff and swarm
What it is. In a handoff, one agent transfers control to another, and the other agent then talks to the user directly. When several peer agents can all hand off to each other, the result is often called a swarm. LangChain’s langgraph-swarm library describes it as agents that “dynamically hand off control to one another based on their specializations”, where the system “remembers which agent was last active”.
A simple example. You ring a call centre about a parcel, and halfway through mention a wrong charge. “Let me put you through to billing.” From then on you talk to billing. If you then ask where the parcel is, billing cannot see the courier system, and nobody is checking the whole call. The transfer is the handoff, billing is the new active agent, and “nobody owns the whole call” is the risk below.
How does this apply to Acme? Maria asks the marketing agent about a campaign, then asks about penalties. In a swarm, the marketing agent would hand her over to the supply agent, and her next message would go straight to supply. Acme does not do this, and the reason is ownership: the writer and its number check would no longer sit on every answer.
handoff or swarm: Maria starts with the marketing agent, which transfers control to the supply agent, and her next message goes straight to supply
When to choose it. When a conversation genuinely changes owner and the new owner needs a long, multi-turn exchange with the user, as in customer service, where a triage agent passes a caller to a billing agent for the rest of the call.
The ADK building block. Peer sub-agents of the same parent, in the default "chat" mode, which ADK
describes as “agent controls until manual handoff” and returns “via transfer”. Two settings on an
Agent restrict it: disallow_transfer_to_parent and disallow_transfer_to_peers.
The main risk. Nobody owns the whole answer, and the path differs from one conversation to the next, which makes evaluation and audit hard.
Here is why Acme rejects it, as a three-turn swarm:
Turn 1 Maria: "What was Verde's CTR in Western Europe last month?"
marketing answers
Turn 2 Maria: "And what did the stockouts cost?"
marketing calls transfer_to_agent(agent_name="supply")
supply answers Maria directly: no writer, no number check
Turn 3 Maria: "And Verde's spend?"
goes to supply, the last active agent, which has no BigQuery tool
Setting disallow_transfer_to_peers=True on the marketing agent removes the turn 2 transfer. Acme goes
further and has no peer transfers at all: every turn goes through the router, the writer and the check.
7. Shared state, or blackboard
What it is. In a shared state design, sometimes called a blackboard, agents do not call each other directly. Each one writes its result to a named key in a shared store, and later agents read the keys they need. This is less a separate shape than the way in-process agents pass data, which is why it spans the whole left half of the map.
A simple example. A team planning an event around a meeting-room whiteboard: the venue person writes
under “Venue”, the caterer under “Food”, and the organiser reads both headings to write the invitation
without asking who wrote what. Scrap paper is thrown away after each task, the whiteboard is wiped after
the meeting, a personal notebook lasts for good, and the office notice board is read by everyone. The
headings are state keys, and the four kinds of paper are the scopes described below (temp:, no prefix,
user: and app:).
How does this apply to Acme? The router already works this way. route writes question, route_label and
route_reason; the specialists’ findings land in marketing_findings and supply_findings; and the
writer’s instruction reads them with placeholders such as {marketing_findings?}.
shared state or blackboard: the route, the specialists and the critic write named keys into session state, and the writer reads them through placeholders in its instruction
When to choose it. When several in-process agents each contribute a piece of the answer. The writer does not need to know which agent produced a key.
The ADK building block. Session state. An Agent with
output_key="marketing_findings" saves its final text to that key; a function node can return
Event(state={...}); and an instruction reads a key with {key} or {key?}. Key prefixes set the
scope: no prefix means this session, user: means every session for this user, app: means every user,
and temp: means this one invocation only. ADK notes that sub-agents called by a parent share the same
invocation, and therefore the same temp: state. Part 8 draws how such a write travels
from the node, on an event, to the saved session.
Here is how the prefixes play out across two of Maria’s sessions, using keys from this series:
Session 1, question 1
user:home_team = "marketing" (user scope: saved for every session of Maria's)
question = "Compare the Acme Pure Refill Launch spend with ..." (this session)
temp:rounds = 2 (the critic gate's counter: this question only)
Session 1, question 2
temp:rounds starts again at 0; question is overwritten
Session 2, a new chat
user:home_team = "marketing" (still there)
question = not set until Maria asks something
Picking the prefix is picking how long a value lives: a team for as long as Maria exists, a question for one chat, a loop counter for one question.
The main risk. Any agent can read any key, and a key written for one question is still there for the next one unless something clears or scopes it. The Try it section below shows exactly this. Across A2A there is no shared state at all: a remote agent keeps its own session, and data travels only in messages and metadata, as Part 6 showed.
8. Mesh over A2A
What it is. In a mesh, agents run as independent services, often owned by different teams or companies, and call each other over the network. Each agent publishes an agent card describing what it does and how to reach it. The A2A specification (latest released version 1.0.0 as of October 2026) is designed for exactly this: communication between “independent, potentially opaque AI agent systems”, which collaborate “without needing to share their internal thoughts, plans, or tool implementations”.
A simple example. Separate companies phoning each other: a shop rings its supplier, who rings a haulier. Each company has a published number and a list of what it can do (the agent card), and none sees inside the others’ offices. If the supplier’s receptionist says “let me check with the haulier” and the haulier’s says “let me check with the supplier”, the caller waits on hold until someone gives up.
How does this apply to Acme? Today Acme is a hub and spoke (a star): the router calls each specialist, and the specialists call nobody. It would become a mesh if, say, the supply agent called a retailer’s replenishment agent or a carrier’s delivery-slot agent. That is a possible future, not part of this series.
a mesh over A2A: the router calls Acme's marketing and supply agents, and the supply agent could in turn call agents run by a retailer and a carrier, each found through its agent card
When to choose it. ADK’s introduction to A2A lists the cases: a separate service, a different team or organisation, a different language or framework, or the need for a formal contract. It advises against A2A for internal code organisation, low-latency internal steps and sub-agents that need shared state.
The ADK building block. RemoteA2aAgent, given a name and the URL of the remote agent_card, used
as a sub-agent or a workflow node, as in Part 6. It lives in google.adk.agents.remote_a2a_agent and
needs the a2a extra (pip install "google-adk[a2a]"); I checked the import and constructor on ADK
Python 2.12.0, which also accepts a description, a timeout and your own httpx_client. ADK still
marks A2A support in Python as experimental as of October 2026, and constructing one prints a
warning saying so.
The main risk. Every hop adds network latency and a contract that both sides must keep stable. A slow partner agent slows every answer that needs it, and nothing central stops agent A calling B calling A. Identity must also travel across every hop, which is the subject of Part 7.
Here is an illustrative cycle, using the possible future above:
1. Router -> Acme supply timeout=300, as in Part 6
2. Acme supply -> retailer's agent "What are next week's replenishment orders?"
3. Retailer's agent -> Acme supply "What is our OTIF this month?" (to plan the orders)
4. Acme supply -> retailer's agent ... and round again
Nothing in A2A breaks this cycle. Maria waits until the router’s 300 seconds run out and then gets no supply finding, while the calls inside the cycle can keep running and costing money. Each hop must also carry who is asking, and decide again whether to trust it (Part 7). A mesh needs its own hop limits and timeouts on every agent, not just at the top.
How do topologies combine?
Real systems nest topologies: a graph contains a fan-out, a branch contains a supervisor, a loop wraps the writer. The skill is keeping each decision in the right place. A restaurant does the same: the till routes tickets by rule, the starter and main cook in parallel, the head chef improvises only on the specials board, and the pass tastes each plate once before it goes out. Rules at the top, judgement in one small corner, a check on everything. How does this apply to Acme? Here is a worked extension of the Acme router with the same layout:
- Keeps: code routes at the top.
routegains a fourth value,pricing. - Adds a supervisor inside one branch: the pricing desk, which calls two agents as tools, promotion history and margin.
- Adds a critic loop on the writer: a critic model reviews each draft, and a code gate sends it back at most once.
- Keeps the number check on every draft, because it is the writer’s after-model callback, so it runs every time the writer runs.
the Acme router extended: code still routes at the top, a new pricing desk supervisor calls two agent tools inside its branch, and a critic loop with a code gate reviews the writer's draft at most twice before the answer goes out
In code, the extension is a few new nodes and edges. This is an illustration I wrote for this post, not
code from the series repository; front, the decline branch and the model settings are left out. I did
run it on ADK Python 2.12.0 with a stand-in model that returns fixed text (no API key, no network): the
graph builds, output_key works on Agent nodes inside a Workflow (the writer’s text lands in
draft, the critic’s in critique), and the gate ends the loop as soon as the critique starts with
“OK”:
# Illustrative only: the Acme router plus a pricing desk and a critic loop.
pricing_desk = Agent(
name="pricing_desk",
model=Gemini(model=PRO_MODEL),
instruction="Answer the pricing question with your tools. Never invent a price.",
tools=[AgentTool(agent=promo_history), AgentTool(agent=margin_model)], # caller keeps control
output_key="pricing_findings",
)
critic = Agent(
name="critic",
model=Gemini(model=FLASH_MODEL),
instruction=(
"Reply OK if the draft answers {question?} and leads with the answer. "
"Otherwise reply with one line of feedback. Draft: {draft?}"
),
output_key="critique",
)
def gate(ctx, node_input):
"""Code decides when the loop stops: the critic is happy, or two rounds are done."""
rounds = ctx.state.get("temp:rounds", 0) + 1
done = ctx.state.get("critique", "").startswith("OK") or rounds >= 2
return Event(output=node_input, route="done" if done else "revise", state={"temp:rounds": rounds})
root_agent = Workflow(
name="router",
edges=[
("START", extract, route),
(route, {"marketing": marketing, "supply": supply, "both": both, "pricing": pricing_desk}),
(marketing, writer), (supply, writer), (both, writer), (pricing_desk, writer),
(writer, critic, gate), # writer keeps output_key="draft"
(gate, {"revise": writer, "done": deliver}), # back-edge with a hard cap
],
)
routeis still code. Adding a specialist means adding a route value and an edge, and a unit test for the new value.- The pricing desk is the only place a model chooses, and its choice is limited to two tools inside one branch. If it picks badly, only pricing answers are affected.
- The gate is code, and its round counter uses a
temp:key, so it starts again at zero for every question. - Cost is visible. A pricing question now makes at least six model calls at the router level (extract, the pricing desk twice, a tool agent, the writer and the critic), and up to nine with both tools and a second draft. Today’s router makes two. That should be a decision, not a surprise.
What goes wrong as topologies grow?
Each topology is reasonable on its own. Problems appear when they stack up:
-
Cost multiplies. In How we built our multi-agent research system, Anthropic reports that agents use about 4 times more tokens than chat, and multi-agent systems about 15 times more. Measure cost per question by route, as Part 11 does.
-
Loops never end. A critic that is never satisfied, or two agents transferring back and forth, runs until something stops it. Cap every loop in code, lower
max_llm_calls, and keep the kill switch and rate limit from Part 9. -
Context leaks between agents. Every agent can read every state key, and history can carry one agent’s data into another’s prompt. Use distinct keys,
temp:for anything that belongs to one question, andinclude_contents="none"for anAgentthat should work only from its instruction and the current turn. Across A2A, send only what the remote agent needs.For example, take the critic from the worked extension above. By default it sees the conversation history, including the previous question’s penalty findings, and may judge today’s draft against them. With
include_contents="none"it sees only its instruction, filled in with{question?}and{draft?}, so it can judge only the draft it was given. -
Ownership blurs. With handoffs and supervisors, it is unclear which agent, and so which team, owns an answer. Keep one owner per answer; in Acme that is the writer, checked by code.
-
Tracing gets harder. One question can touch eight agents in three services. Give each node a clear name; ADK records a path such as
router@1/writer@2for the second pass through the writer. Part 11 covers traces, and Part 10 the evals every new route needs.
Which topology should I choose?
The bottom line: pick the shape with the fewest model-made decisions that still does the job. The trap is splitting into agents because it looks modular; every split adds model calls, hops and a new place for answers to go wrong.
Start with the simplest shape that does the job, and move down this list only when the one above it cannot:
- One agent with good tools. Anthropic notes that for many applications, optimising single model calls with retrieval is enough.
- Sequential pipeline. Choose it when the steps are known and ordered.
- Parallel fan-out and gather. Choose it when the steps are independent and the user is waiting.
- Loop with a critic. Choose it when a review measurably improves the answer and code cannot do the review. Always cap it.
- Agent as a tool. Choose it when a bounded language task deserves its own agent but the caller must keep ownership.
- Hierarchical supervisor. Choose it when the subtasks really cannot be predicted. Keep it inside a branch, not at the top.
- Handoff or swarm. Choose it only when a conversation genuinely changes owner for many turns.
- Mesh over A2A. Choose it when the other agent belongs to another team, language or company. Ownership and permissions are good reasons to split; looking modular is not.
Here are the three requests from the start of this post, put through the list:
- “The pricing team wants its own agent.” The questions are pricing questions, which a rule can spot,
so step 2 or 3 is enough at the top: a new
pricingroute value and one more branch, as in the worked extension. Step 6 applies only inside that branch, if pricing questions really cannot be predicted. - “A second model should review the writer’s draft.” Code already checks the numbers, and only the “does it answer the question” part needs a model, so this is step 4: a critic loop capped in code.
- “A partner’s agent wants to talk to ours.” The other agent belongs to another company, so this is step 8: A2A, with the identity checks from Part 7.
The series rule settles most arguments: Models read and write language. Code makes the decisions. If a choice can be written as a rule (who owns a metric, when to stop, which branch to take), write it in code. Use a model where the task is understanding or writing language: rewriting a question, reading a contract, reviewing a draft for clarity.
My view: in enterprise systems, supervisors and swarms are best kept small and deep inside a graph that code controls, and users should still see one agent. For Acme, that is the router we publish to Gemini Enterprise in Part 12, whatever shape sits behind it.
To lock it in, here are the eight shapes with the everyday picture used above:
Topology What it is Everyday picture
------------------- ------------------------------------------- ------------------------------------
Sequential pipeline fixed steps, each feeds the next a car wash: soap, rinse, dry
Fan-out and gather independent steps at once, then combine rice and curry on two hobs
Supervisor a model picks a sub-agent at run time a GP referring to a specialist
Agent as a tool call an agent, get a result, keep control a manager using a translator
Loop and critic draft, review, revise, capped in code a reporter, an editor, a deadline
Handoff or swarm control moves to another agent for good "putting you through to billing"
Shared state agents write and read named keys headings on a meeting whiteboard
Mesh over A2A independent services calling each other companies phoning each other
What does each step of the ladder look like in a real company?
Acme only needs a few of these shapes, so here is one real-world job for each step, outside Acme. For each one I give the shape, why it fits, and why the step above it on the ladder would not be enough. The companies and timings are illustrative; the reasoning is the point.
eight topologies, eight real jobs: an IT helpdesk agent with three tools; a medical scribe pipeline from notes to billing codes to a patient letter; loan underwriting checks run in parallel and gathered into one memo; a bank's marketing copy reviewed by a compliance critic at most twice; a portfolio agent calling a tax agent as a tool; an analytics supervisor delegating to churn and support sub-agents; an airline triage agent handing the traveller to rebooking and then baggage; and a travel agent talking to other companies' agents over A2A
1. One agent with good tools: an internal IT helpdesk.
The agent resets passwords, checks VPN status and searches the IT documentation, with three narrow
tools: reset_password, check_vpn_status and search_it_docs. One model in one loop handles most
requests, such as “My VPN keeps dropping and I am locked out”: it checks the VPN, then offers a
reset. Splitting this into three agents would add a routing call and more tokens to every request
without making any answer more accurate. There is no simpler step, and nothing here needs a harder
one.
2. Sequential pipeline: a medical scribe and billing pipeline.
Step one extracts structured notes (symptoms, diagnosis, medicines) from a consultation transcript.
Step two maps the diagnosis to ICD-10 billing codes, the WHO’s standard classification of diseases.
Step three writes a plain-English letter for the patient. In ADK this is a SequentialAgent or a
Workflow with edges ("START", extract, code, letter), and each step reads the one before through
state (output_key and a {notes?} placeholder). It fits because every step depends on the previous
output and the order never changes: you cannot code a diagnosis you have not extracted yet. One agent
with tools would work, but the order would then depend on the model; here code fixes it. A clinician
still reviews the notes and the codes before anything is billed or sent.
3. Parallel fan-out and gather: commercial loan underwriting.
When a business applies for a loan, three checks run at the same time: a credit agent reads credit
bureau scores and debt ratios, a legal agent searches court records and sanctions lists, and a
cash-flow agent analyses twelve months of bank statements. A gather step (ParallelAgent, or a
JoinNode in a Workflow) waits for all three, and a writer turns them into one risk memo. A
pipeline would give the same memo, but slower, because none of the checks needs another’s result:
why the underwriting checks run in parallel: three independent checks of about 15, 12 and 10 seconds take about 37 seconds one after another, but only as long as the slowest, about 15 seconds, when they run side by side and are gathered into one memo (illustrative timings)
The loan officer waits for the slowest check, not for all three in turn. The credit decision itself stays with a person.
4. Loop and critic: a bank’s marketing copy. A writer agent drafts a promotional email for a new credit card. A critic agent reviews it against financial promotion rules; in the UK, the FCA requires promotions to be “fair, clear and not misleading”. Is the interest rate as prominent as the offer? Does it imply guaranteed approval? A code gate sends the critique back for a rewrite, at most twice, then stops either way. It fits because “misleading” is a judgement about language that no regular expression can make, and one targeted rewrite fixes most slips. A pipeline cannot express “try again”, and the cap keeps cost and time bounded. A compliance officer still signs off before anything is sent.
5. Agent as a tool: a portfolio agent asking a tax agent.
A client asks a wealth-management agent “Should I sell my tech shares to buy bonds now?”. The
portfolio agent calls a tax agent through AgentTool, as a function: “What is the capital gains
impact of selling 500 shares?”. The tax agent runs its own instruction and tax-band lookups and
returns a number. The portfolio agent weighs that against the client’s goals and writes the reply.
It fits because tax needs its own prompt and tools, but the caller must keep the conversation:
the tax figure is one input, not the answer. A loop or pipeline would hand the answer to whichever
step ran last.
6. Hierarchical supervisor: an “ask data” analytics desk.
An executive asks “Why did third-quarter churn spike in Europe compared with North America, and did
support tickets rise beforehand?”. A supervisor model breaks the question down and delegates to a
churn sub-agent (CRM data) and a support sub-agent (the ticket system), then combines their answers.
In ADK the sub-agents sit in sub_agents=[...] with mode="task" or "single_turn", so the
supervisor calls them and gets control back. It fits because the space of questions is open: no
if/else can anticipate every combination. Where questions can be classified by a rule, as at the
top of Acme’s router, use code instead (step 2 or 3).
7. Handoff and swarm: an airline’s customer service.
A traveller writes to a triage agent: “My flight was cancelled and my bag is stuck.”. Triage hands
the conversation to a rebooking agent, which needs several turns with the traveller (“The 4 pm via
another hub, or tomorrow’s direct flight? Window or aisle?”). When the traveller asks “Now what
about my bag?”, rebooking hands over to a baggage agent for its own multi-turn conversation. In ADK
these are sub_agents in mode="chat", which keep the conversation until they transfer it. It fits
because the conversation genuinely changes owner for many turns; routing every seat preference back
through a supervisor and a writer would be slow and awkward. A supervisor would keep pulling control
back after every reply.
8. Mesh over A2A: corporate travel across companies. A company’s travel agent, built with ADK on Google Cloud, books a trip by calling an airline’s booking agent and a hotel group’s reservation agent, each built by a different company, in a different language, on a different cloud. It reads their Agent Cards and calls them over A2A, authenticating with whichever scheme each card declares, for example OAuth 2.0. It fits because the agents share no code, memory or database, and never will; a network contract is the only option. Agent as a tool would need the other company’s agent in your process, which is impossible here.
The pattern across all eight: each step up the ladder buys one specific capability (fixed order, speed, a second opinion, a specialist the caller controls, open-ended delegation, a change of owner, a company boundary) and costs latency, tokens and testability. Take the step only when you need that capability.
Try it
This small program builds four topologies at once with no model at all, so it runs without any API key:
a code route, a parallel fan-out with a JoinNode, a critic loop with a hard cap, and shared state
between all of them. Plain Python functions stand in for the agents, and their figures are toy data. It runs on ADK Python 2.12; install
it with pip install "google-adk>=2.0".
import asyncio
from google.adk import Event, Workflow
from google.adk.apps import App
from google.adk.runners import InMemoryRunner
from google.adk.workflow import JoinNode
MAX_ROUNDS = 2
def route(node_input: str):
"""Code picks the path: no model, no tokens, same answer every time."""
q = node_input.lower()
if "spend" in q and "penalt" in q:
target = "both"
elif "penalt" in q:
target = "supply"
else:
target = "marketing"
return Event(output=node_input, route=target, state={"question": node_input})
def marketing(node_input: str):
text = "Acme Pure spend: EUR 283,590"
return Event(output=text, state={"marketing_findings": text})
def supply(node_input: str):
text = "Service penalties: EUR 35,600"
return Event(output=text, state={"supply_findings": text})
def writer(ctx, node_input):
"""Stands in for the writer model: it drafts from whatever is in state."""
rounds = ctx.state.get("temp:rounds", 0) + 1
parts = [ctx.state.get("marketing_findings"), ctx.state.get("supply_findings")]
draft = "; ".join(p for p in parts if p)
if rounds == 1:
draft = "Here is what I found. " + draft # wordy first draft
return Event(output=draft, state={"draft": draft, "temp:rounds": rounds})
def critic(ctx, node_input: str):
"""Stands in for the critic model: it asks for the answer to lead."""
ok = not node_input.startswith("Here is what I found")
return Event(output=node_input, state={"critique": "OK" if ok else "Lead with the figures."})
def gate(ctx, node_input: str):
"""Code decides when the loop stops: the critic is happy, or the cap is reached."""
done = ctx.state["critique"] == "OK" or ctx.state["temp:rounds"] >= MAX_ROUNDS
return Event(output=node_input, route="done" if done else "revise")
def deliver(ctx, node_input: str):
return f"{node_input} (after {ctx.state['temp:rounds']} round(s))"
both = Workflow(name="both", edges=[("START", (marketing, supply), JoinNode(name="merge"))])
root_agent = Workflow(
name="topologies",
edges=[
("START", route),
(route, {"marketing": marketing, "supply": supply, "both": both}), # code routes
(marketing, writer),
(supply, writer),
(both, writer), # fan-out, then gather
(writer, critic, gate),
(gate, {"revise": writer, "done": deliver}), # loop with a cap
],
)
app = App(name="topologies", root_agent=root_agent)
async def main():
runner = InMemoryRunner(app=app)
events = await runner.run_debug(
[
"Compare our Acme Pure spend with the service penalties",
"What were the service penalties?",
],
quiet=True,
)
for e in events:
if e.output is not None:
print(f"{e.node_info.path:42} {e.output}")
asyncio.run(main())
Run it and look at the output:
topologies@1/route@1 Compare our Acme Pure spend with the service penalties
topologies@1/both@1/marketing@1 Acme Pure spend: EUR 283,590
topologies@1/both@1/supply@1 Service penalties: EUR 35,600
topologies@1/both@1/merge@1 {'marketing': 'Acme Pure spend: EUR 283,590', 'supply': 'Service penalties: EUR 35,600'}
topologies@1/writer@1 Here is what I found. Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/critic@1 Here is what I found. Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/gate@1 Here is what I found. Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/writer@2 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/critic@2 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/gate@2 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/deliver@1 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600 (after 2 round(s))
topologies@1/route@1 What were the service penalties?
topologies@1/supply@1 Service penalties: EUR 35,600
topologies@1/writer@1 Here is what I found. Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/critic@1 Here is what I found. Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/gate@1 Here is what I found. Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/writer@2 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/critic@2 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/gate@2 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600
topologies@1/deliver@1 Acme Pure spend: EUR 283,590; Service penalties: EUR 35,600 (after 2 round(s))
- The route and the fan-out. The first question mentions spend and penalties, so
routesends it downboth, and marketing and supply run as two branches beforemergegathers them. - The loop. The first draft fails the critic, so
gatesends it back;writer@2andcritic@2are the second round, anddeliverruns after it. ChangeMAX_ROUNDSto1and the first draft goes out. - The leak. The second question only asks about penalties, and only
supplyruns, yet the answer still contains the Acme Pure spend. The marketing finding from the first question is still in session state. Rename both findings keys totemp:marketing_findingsandtemp:supply_findings(in the specialists and inwriter) and run it again: the second answer now contains only the penalties. Acme’s real router uses the other fix: its route step writesmarketing_findingsandsupply_findingsback to empty strings at the start of every question (Part 5), so the writer never sees the previous question’s findings. Either works; what matters is that something clears them on purpose.
What can go wrong
- A supervisor at the top. A model choosing the route on every question adds cost and removes the guarantee that only supply computes penalties. Keep the top-level route in code.
- A critic prompt as the stopping rule. “Stop when it is good” is not a cap. Count rounds in code.
- An agent tool that grows into a hidden tree. If an
AgentToolgains its own agent tools, one tool call hides a whole topology. Draw it and count its calls. - The same agent as a sub-agent and a tool. The model then has two ways to reach it, with different ownership. Pick one.
- Splitting into remote agents too early. Every A2A hop is latency, a contract and a deployment.
What code did we write in this part?
Every part ends with the same map of Acme’s build: which files exist so far, and the code this part added, explained line by line. The map grows with each part.
This part adds no code to Acme’s build. It compares eight multi-agent topologies and places the router among them; the snippets are illustrations, including a worked extension with a pricing desk supervisor and a critic loop that the post says is not part of the series repository, and a toy program with no model that shows the topologies running.
Where does it live?
router/ the router agent: extract, route, write
app/agent.py P5 the graph: front, extract, route, both, decline, edges
app/remote.py P6 A2A handles for marketing and supply, metadata, capture
app/writer.py P5 writer agent and its after-model number check
marketing/ the marketing specialist, on BigQuery
app/agent.py P1,2,3,4 marketing agent with its skill in a SkillToolset
app/app_utils/a2a.py P6 A2A endpoint and agent card (1.0 and 0.3) for marketing
app/skills/marketing-kpis/SKILL.md P4 marketing know-how: which tool, definitions, story checks
app/skills/marketing-kpis/references/tables.md P4 tables, grain, measures and the join rule, read on demand
app/tools.py P1,2 three narrow tools: measures, unserved demand, SQL fallback
tests/unit/test_marketing.py P2 unit tests for the tools, with a fake context and database
supply/ the supply specialist, on Databricks
app/agent.py P3 the supply Agent, Databricks specialist, behind sql_guard
app/app_utils/a2a.py P6 A2A endpoint and agent card (1.0 and 0.3) for supply
app/skills/supply-catalog/SKILL.md P4 supply know-how, given only to the supply agent
app/tools.py P3 supply tools such as query_supply_measures, run as the user
shared/ code all three services share
adk_common.py P3,6 shared ADK helpers, now with the A2A request converter
engines.py P3 run_bigquery and run_databricks: guard again, caps, as the user
factcheck.py P5 ungrounded_numbers: finds figures not in the findings
registry.py P2 reads semantic.yaml: measure names, computable measures
routing.py P1 classify: picks the specialist in code, no model
semantic.yaml P1,2 registry of measures, owners and published formulas
skill_source.py P4 loads a skill from disk or a pinned registry revision
sqlguard.py P3 guard_sql read-only allow-list; redact for log lines
+ added in this part ~ changed in this part Pn = the parts that touched the file
The Try it shows why findings must be cleared between questions; Acme’s router already does this in its route step, from Part 5.
What did we learn?
- A multi-agent topology is the shape of a system: who calls whom, who holds the state and, most importantly, who decides the next step.
- Two axes place almost every topology: whether code or a model decides the next step, and whether the agents run in one process or as remote services over A2A.
- The eight common topologies are sequential, parallel fan-out and gather, hierarchical supervisor, agent as a tool, loop and critic, handoff or swarm, shared state, and a mesh over A2A, and ADK has a building block for each.
- Real systems combine topologies, and the safe way to do it is to keep the top-level route and every stopping rule in code, and let models choose only inside a bounded branch.
- Growing topologies multiply cost, risk endless loops, leak context between agents, blur ownership and make tracing harder, so each one needs a cap, scoped state, one owner per answer and a clear trace.
- The simplest topology that works is the right one, and the series rule still decides: models read and write language, code makes the decisions.
What comes next?
Every topology in this post passes work from one agent to another, and in a mesh that work crosses services and even companies. That raises the question of whose permissions each agent uses. Part 7: Who is asking? Identity for agents answers it. Every call an agent makes carries two identities, the agent’s own and the user’s, and Part 7 explains both: Agent Identity on Google Cloud, how OAuth 2.0 works, and how the supply agent queries Databricks as the signed-in user while keeping tokens away from the model.
Sources
- ADK: Workflows, multi-agent and multi-node applications
- ADK: Multi-agent workflow patterns
- ADK: Build collaborative agent teams (collaboration modes)
- ADK: Template agent workflows
- ADK: Sequential template workflow agent
- ADK: Parallel template workflow agent
- ADK: Loop template workflow agent
- ADK: Graph-based agent workflows
- ADK: Build graph routes for workflow agents (JoinNode, loops)
- ADK: Dynamic agent workflows
- ADK: Custom agents (LLM delegation and agent transfer)
- ADK: LLM agents (include_contents, transfer settings)
- ADK: Function tools and Agent-as-a-Tool
- ADK: State (output_key and prefixes)
- ADK: Runtime configuration (max_llm_calls)
- ADK: Introduction to A2A
- ADK: Quickstart, consuming a remote agent via A2A
- Welcome to ADK 2.0
- A2A protocol specification
- Anthropic: Building effective agents
- Anthropic: How we built our multi-agent research system
- LangChain: Multi-agent
- LangGraph Multi-Agent Supervisor (GitHub)
- LangGraph Multi-Agent Swarm (GitHub)
- FCA: regulating financial promotions and adverts
- WHO: International Classification of Diseases (ICD)
Opinions are my own. Every fact in this post comes from the public documentation linked above. Acme Consumer Brands and its data are fictional.