Multi-Agent Systems in ADK: Coordinators, sub_agents, and LLM-Driven Delegation

How one agent routes work to specialists — and why the description field is the most important string you write.

Module 02 of this series covered workflow agentsSequentialAgent, ParallelAgent, LoopAgent — where you hard-code the orchestration. The order is fixed; the LLM never decides who runs next. This post is about the opposite: dynamic composition, where the model chooses how work flows between agents at runtime. In Google’s Agent Development Kit (ADK), the simplest and most common form of this is delegation — a coordinator agent that transfers control to the right specialist based on what the user asked.

The mechanism: sub_agents + description

You give an LlmAgent a list of sub_agents. ADK exposes those children to the coordinator’s model and lets it call an implicit transfer_to_agent(...) function. The model decides which child to transfer to by reading each child’s description — a one-line summary of what that agent is for. That single string is the routing table. Write it vaguely and you get bad routing; there is no separate config that overrides it.

Here is a two-specialist coordinator in Python:

from google.adk.agents import LlmAgent

MODEL = "gemini-flash-latest"

weather_specialist = LlmAgent(
    name="weather_specialist",
    model=MODEL,
    description="Answers questions about the weather in a city.",
    instruction="You answer weather questions. If asked anything else, say you only do weather.",
)

joke_specialist = LlmAgent(
    name="joke_specialist",
    model=MODEL,
    description="Tells one short, family-friendly joke on request.",
    instruction="Tell exactly one short, family-friendly joke.",
)

coordinator = LlmAgent(
    name="coordinator",
    model=MODEL,
    description="Routes the user to the correct specialist.",
    instruction=(
        "You are a router. If the user asks about weather, transfer to weather_specialist. "
        "If the user wants a joke, transfer to joke_specialist. Do not answer yourself."
    ),
    sub_agents=[weather_specialist, joke_specialist],
)

Ask "what's the weather in Paris?" and the coordinator transfers to weather_specialist; ask "tell me a joke" and it transfers to joke_specialist. The instruction nudges the coordinator’s behaviour (“don’t answer yourself”), but the actual matching is driven by the sub-agents’ description fields. This is why Module 01 insisted on writing a crisp one-liner for every agent — in a multi-agent system that line is load-bearing.

The Go form

ADK is dual-language, and the Go SDK mirrors the shape exactly — a config struct with a SubAgents slice instead of a keyword argument:

import (
    "google.golang.org/adk/v2/agent"
    "google.golang.org/adk/v2/agent/llmagent"
)

weather, _ := llmagent.New(llmagent.Config{
    Name: "weather_specialist", Model: m,
    Description: "Answers questions about the weather in a city.",
    Instruction: "You answer weather questions only.",
})
joke, _ := llmagent.New(llmagent.Config{
    Name: "joke_specialist", Model: m,
    Description: "Tells one short, family-friendly joke.",
    Instruction: "Tell exactly one short, family-friendly joke.",
})

coordinator, _ := llmagent.New(llmagent.Config{
    Name: "coordinator", Model: m,
    Description: "Routes the user to the correct specialist.",
    Instruction: "Transfer weather questions to weather_specialist and joke requests " +
        "to joke_specialist. Do not answer yourself.",
    SubAgents: []agent.Agent{weather, joke},
})

Same three ingredients — name, description, sub-agents — same routing behaviour. In both languages, attaching a child as a sub-agent sets its parent link automatically (sub.parent_agent in Python; the equivalent internal parent in Go).

Delegation is a transfer, not a call

The key thing to internalise: when the coordinator delegates, it hands over the turn. The specialist responds to the user directly and owns the rest of the conversation. Control does not automatically bounce back to the coordinator after each reply.

That behaviour is tunable. By default a sub-agent can transfer back up to its parent or sideways to a peer. You can lock those doors:

weather_specialist = LlmAgent(
    name="weather_specialist",
    model=MODEL,
    description="Answers questions about the weather in a city.",
    disallow_transfer_to_parent=True,   # can't hand control back up
    disallow_transfer_to_peers=True,    # can't jump to a sibling
)

The Go equivalents are Config{DisallowTransferToParent: true} and DisallowTransferToPeers: true. Use these when you want a specialist to fully own a task once it’s engaged, rather than ping-ponging control around the hierarchy.

Mental model: delegation vs. agent-as-tool

Delegation is one of two ways to compose agents, and it’s worth contrasting with the other — agent-as-tool — because they feel similar but differ on one axis: who keeps control.

Agent-as-tool wraps the child so the parent can invoke it:

from google.adk.tools import agent_tool

summarizer = LlmAgent(
    name="summarizer", model=MODEL,
    description="Summarizes provided text into exactly one sentence.",
    instruction="Summarize the user's text into exactly one concise sentence.",
)

assistant = LlmAgent(
    name="assistant", model=MODEL,
    instruction="When the user gives long text and asks for a summary, call the "
                "summarizer tool, then present the result.",
    tools=[agent_tool.AgentTool(agent=summarizer)],
)

In Go that’s agenttool.New(summarizer, nil) passed in Tools: []tool.Tool{...}.

One reuse rule for both languages: an agent can have only one parent. If you want to both delegate to an agent and wrap it as a tool, create two distinct instances — reusing a single object makes the second parent assignment conflict. That’s why the summarizer above is a separate agent, not the weather or joke specialist reused.

When to reach for delegation

Use a coordinator/dispatcher when you have several specialist agents with clearly separable jobs — a support triage that routes to billing vs. technical vs. account agents, or an assistant that fans out to domain experts. Keep each specialist’s description sharp and non-overlapping, because overlap is exactly where the model’s routing goes wrong. And remember the contrast with Module 02: reach for a workflow agent when the sequence is known and fixed, and a coordinator with sub_agents when the right next step depends on the input and you want the LLM to decide.

Next in the series: Module 04 — Tools, the full ecosystem of built-in, OpenAPI, MCP, and third-party tools, plus tool-level human-in-the-loop.

Sources & References

Multi-agent systems and delegation
The Go SDK (google.golang.org/adk/v2)