Multi-Agent by Design: Coordinator and Specialist Sub-Agents
Architect scalable multi-agent systems in ADK 2. Build a central Master Coordinator that routes requests to specialized search, cart, and support agents.

Multi-Agent by Design: Coordinator and Specialist Sub-Agents
📦 Companion Code: View the complete, runnable code on GitHub:
part06in the cartpilot-adk2-series repository.
CartPilot is currently one agent accumulating tools: search, cart, checkout... and support is coming. This is the point in every agent project where a monolithic agent starts to degrade — too many tools competing for the model's attention, an instruction block trying to cover every scenario, and no way to give the "which model tier" decision per capability. Today we split CartPilot into a coordinator and three specialists.
Why split at all
A single agent with 12 tools and a 200-line instruction has to reason, on every turn, about which of those 12 tools is relevant and hold all 200 lines of context in mind while doing it. Specialists narrow that: search_agent only ever sees search tools and a focused instruction about search. The coordinator's only job is routing — a much easier task that a cheaper, faster model handles fine.
The specialists
# cartpilot/search_agent.py
from google.adk.agents import Agent
from google.adk.tools import ToolContext
from .catalog import find_products, get_product
def search_products(query: str, category: str, tool_context: ToolContext) -> dict:
"""Searches the CartPilot catalog for products.
Args:
query: Free-text search term. Empty string to browse by category only.
category: Optional category filter. Empty string to search all.
"""
results = find_products(query=query, category=category)
tool_context.state["last_search_results"] = [r["sku"] for r in results]
return {"status": "success", "results": results}
def get_product_details(sku: str) -> dict:
"""Fetches full details for a single product by SKU."""
product = get_product(sku)
return {"status": "success", "product": product} if product else {"status": "not_found", "sku": sku}
search_agent = Agent(
model="gemini-3.5-flash",
name="search_agent",
description=(
"Specialist for finding products in the CartPilot catalog by name, "
"category, or description. Delegate here for any product discovery question."
),
instruction="""
You help shoppers find products. Use `search_products` and
`get_product_details`. If nothing matches, say so and suggest
broadening the search. Keep responses short.
""",
tools=[search_products, get_product_details],
)cart_agent folds in Part 5's checkout pipeline — but wired differently this time:
# cartpilot/cart_agent.py (excerpt)
from google.adk.tools.agent_tool import AgentTool
# ... validation_agent, discount_agent, checkout_step_agent, checkout_pipeline
# defined exactly as in Part 5 ...
checkout_tool = AgentTool(agent=checkout_pipeline)
cart_agent = Agent(
model="gemini-3.5-flash",
name="cart_agent",
description=(
"Specialist for managing the shopper's cart: adding/removing items, "
"viewing the cart, and running checkout. Delegate here for anything "
"cart- or order-related."
),
instruction="""
You manage the shopper's cart. Use `add_to_cart`, `view_cart`.
When the shopper confirms they want to complete the purchase, call the
`checkout_pipeline` tool and relay its order_confirmation to them clearly.
""",
tools=[add_to_cart, view_cart, checkout_tool],
)# cartpilot/support_agent.py
from google.adk.agents import Agent
RETURN_POLICY = (
"Items can be returned within 30 days of delivery if unused and in "
"original packaging. Refunds are issued to the original payment method "
"within 5-7 business days of us receiving the return. Sale items are "
"final unless defective."
)
def get_return_policy() -> dict:
"""Returns CartPilot's current return and refund policy text."""
return {"status": "success", "policy": RETURN_POLICY}
def get_order_status(order_id: str) -> dict:
"""Looks up the status of a previous order.
Args:
order_id: The order reference, e.g. "CP-10234".
"""
return {"status": "success", "order_id": order_id, "state": "shipped",
"estimated_delivery": "2-3 business days"}
support_agent = Agent(
model="gemini-3.5-flash",
name="support_agent",
description=(
"Specialist for order status, returns, and refund policy questions. "
"Delegate here for anything post-purchase or policy-related."
),
instruction="""
You handle CartPilot support questions.
- Use `get_return_policy` for return/refund policy questions.
- Use `get_order_status` for order tracking questions.
- Be empathetic but concise. Don't promise anything the policy doesn't state.
- You cannot issue refunds yourself yet — that's coming in Part 8.
""",
tools=[get_return_policy, get_order_status],
)The coordinator
# cartpilot/agent.py
from google.adk.agents import Agent
from .cart_agent import cart_agent
from .search_agent import search_agent
from .support_agent import support_agent
root_agent = Agent(
model="gemini-3.5-flash",
name="cartpilot_coordinator",
description="CartPilot's main coordinator agent.",
instruction="""
You are CartPilot's coordinator. Delegate to the right specialist:
- Product discovery -> search_agent
- Cart actions and checkout -> cart_agent
- Orders, returns, policy questions -> support_agent
Greet the shopper on their first message, then delegate. Don't try to
answer specialist questions yourself.
""",
sub_agents=[search_agent, cart_agent, support_agent],
)sub_agents vs AgentTool: two different delegation shapes
We used both in this same file, deliberately, because they mean different things:
sub_agents=[...]hands the conversation to the child agent viatransfer_to_agent. The coordinator steps out of the loop; the specialist takes over the conversation directly with the shopper until it (or another handoff) transfers again. This is what the coordinator does withsearch_agent,cart_agent, andsupport_agent— a full context switch.AgentTool(agent=...)wraps an agent so another agent can call it like a function — send it input, get a structured result back, and keep control of the conversation itself. This is whatcart_agentdoes withcheckout_pipeline: it doesn't want to hand the shopper off to the checkout pipeline permanently, it wants to invoke it as one step and then keep talking.
Read description fields as documentation, because that's exactly how the coordinator uses them to decide who handles what — a vague description produces misrouted conversations the same way a vague tool docstring produces bad tool calls.
Model tiering across specialists
Right now every agent uses gemini-3.5-flash — deliberately, as a baseline. But the split we just did is what makes per-agent tiering possible in the first place: a monolithic agent forces one model for everything, while a coordinator + specialists structure lets you match model strength to what each specialist's job actually demands. search_agent and cart_agent do bounded, low-ambiguity work — Flash is the right permanent choice. support_agent is where ambiguity and stakes rise (a shopper upset about a damaged item, a policy edge case) — we're keeping it on Flash for now and will show exactly how and when to escalate specific hard cases to Pro, backed by real token/cost measurements, in Part 10.
Try it
adk webTry a conversation that touches all three: "Do you have hiking socks? ... Add a pack to my cart ... What's your return policy?" — watch the event inspector show transfer_to_agent calls moving the conversation between specialists.
What's next
Right now, cart_agent calling checkout_pipeline via AgentTool runs the whole pipeline in one shot and returns a full conversational result. Part 7 looks at ADK 2's Task API — a more structured way to delegate specific, bounded units of work (like "look up this one order status") without paying for a full conversational round-trip.