aiml
SearchMy learning
Menu

Lesson 11 of 17Module 2

Delegate to sub-agents through a coordinator

Build a documentation review team with a coordinator, two typed specialists and an event check that both delegations ran.

Prerequisites

ADK function tools and event streams, the graph-workflow lessons, Python 3.12 or later and a Gemini API key.

By the end, you can

  • Delegate from a coordinator Agent to sub-agents through ADK's auto-generated delegation tools.
  • Choose a sub-agent mode (chat, task or single_turn) and give a single_turn sub-agent a typed output.
  • Isolate sub-agents from transfer_to_agent and check the delegation trajectory in the event stream.
Runtime
Python 3.12+ and a Gemini API key
Dependencies
google-adk, google-genai

Lab code, tests and fixtures (ZIP, 14 KB)Quiz check your understanding

Progress is saved in this browser.

When you need this. A documentation reviewer needs both a summary of a change and a focused style check. We will give those jobs to two specialist agents and let a coordinator combine their results into a short review.

A sub-agent is an agent attached to another agent. ADK gives the coordinator a delegation tool for each specialist. The coordinator calls those tools, receives the results and writes the final answer.

This differs from a graph: the model decides which delegation calls to make. Our instruction asks for both specialists, so we will check that both actually ran instead of judging collaboration from the final review alone.

data flowreturnfailuredeveloperdocs_reviewercoordinator · chatgemini-3.8-flashdiffreviewsingle_turn sub-agents · transfer disallowedpr_summarizeroutput: PrSummarystyle_checkeroutput: StyleReportfind_long_sentencesfunction toolrequestPrSummaryrequestStyleReportcallresultfailssub-agent errorreturned as thetool responsedata flowreturnfailuredeveloperdocs_reviewercoordinatordiffreviewpr_summarizerPrSummaryrequestPrSummarysub-agenterrorstyle_checkerStyleReportrequestStyleReportfind_long_sentencescallresult
The coordinator calls delegation tools and reads the specialists’ typed results. The style specialist can call its own function tool. Disabling parent and peer transfer keeps each specialist focused on returning its result.

The coordinator calls each single-turn sub-agent as a delegation tool. The specialists return to the coordinator; they do not exchange messages directly. ADK calls this a collaborative workflow, but the model and the coordinator Agent are different components.

Prepare the review-team project

You need Python 3.12 or later and a Gemini API key. Use Download code, extract the ZIP, and open its adk-sub-agents-lesson directory. Keep the Python files and fixtures together. All commands in this lesson run from that directory.

Install the pinned dependenciesTerminal · bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt

On Windows PowerShell, activate with .venv\Scripts\Activate.ps1.

The Python client reads exported environment variables. The bundled .env.example documents them; it is not loaded by the client. Set GOOGLE_API_KEY privately, then select the Gemini API in this terminal:

Select the model backendTerminal · bash
unset GOOGLE_GENAI_USE_VERTEXAI DOCS_MODEL
export GOOGLE_GENAI_USE_ENTERPRISE=0

Sending a request to the model uses your provider account. The offline checks later in the lesson need no key.

The input is a sample docs diff. It removes a troubleshooting section and adds a long sentence, giving the two specialists different details to examine.

docs-pr.diffSource · diffDownload
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -12,13 +12,9 @@
 ## The assistant cites the wrong page
 
-Rebuild the search index with `docs-assistant index --rebuild`.
-
-## The assistant cannot reach Ollama
-
-Check that `OLLAMA_API_BASE` points to a running Ollama server.
+If the assistant cites a page that does not answer the question, which usually happens after pages were renamed or moved and the search index still holds the old URLs, rebuild the search index with `docs-assistant index --rebuild` and then ask the question again so that the new index is used.
diff --git a/docs/cli.md b/docs/cli.md
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -6,3 +6,7 @@
 `docs-assistant ask QUESTION` prints an answer with citation links.
+
+## index
+
+`docs-assistant index --rebuild` drops the search index and builds it again from the docs folder.

Define the team

pr_summarizer returns a title, risk label and changed page names. style_checker uses a deterministic function to find long added sentences and returns a list of problems.

review_team.pySource · pythonDownload
"""A docs-review coordinator that delegates to two single-turn sub-agents."""
import re
from typing import Literal

from google.adk import Agent
from google.genai import types
from pydantic import BaseModel

MODEL = "gemini-3.8-flash"  # or LiteLlm(model="ollama_chat/gemma4:e2b")


class PrSummary(BaseModel):
    title: str
    risk: Literal["low", "medium", "high"]
    pages: list[str]


class StyleReport(BaseModel):
    problems: list[str]


def find_long_sentences(text: str, limit: int = 30) -> dict:
    """Return the added sentences that have more than `limit` words."""
    added = " ".join(line[1:] for line in text.splitlines()
                     if line.startswith("+") and not line.startswith("+++"))
    sentences = re.split(r"(?<=[.!?])\s+", added)
    return {"long_sentences": [s for s in sentences if len(s.split()) > limit]}


def build_team(model=None, isolate=True) -> Agent:
    """isolate=True removes transfer_to_agent from the sub-agents (see the failure section)."""
    model = model or MODEL
    no_transfer = {"disallow_transfer_to_parent": isolate, "disallow_transfer_to_peers": isolate}
    pr_summarizer = Agent(
        name="pr_summarizer",
        model=model,
        generate_content_config=types.GenerateContentConfig(max_output_tokens=1024),
        mode="single_turn",
        description="Summarizes a docs diff: title, risk and changed pages.",
        instruction=("Summarize the diff. risk is high if the diff removes a section "
                     "that readers rely on, medium if it changes commands, otherwise low."),
        output_schema=PrSummary,
        **no_transfer,
    )
    style_checker = Agent(
        name="style_checker",
        model=model,
        generate_content_config=types.GenerateContentConfig(max_output_tokens=1024),
        mode="single_turn",
        description="Finds prose problems in the added lines of a docs diff.",
        instruction="Call find_long_sentences on the diff, then list each problem in one line.",
        tools=[find_long_sentences],
        output_schema=StyleReport,
        **no_transfer,
    )
    return Agent(
        name="docs_reviewer",
        model=model,
        generate_content_config=types.GenerateContentConfig(max_output_tokens=1024),
        instruction=("You review docs pull requests for the docs-assistant project. "
                     "Send the diff to pr_summarizer and to style_checker, then reply with "
                     "a review of at most five lines that uses both results."),
        sub_agents=[pr_summarizer, style_checker],
    )


def delegations(events: list[dict]) -> list[str]:
    """The sub-agents the coordinator called, in order, from the event stream."""
    names = {"pr_summarizer", "style_checker"}
    return [call["name"] for event in events if event["author"] == "docs_reviewer"
            for call in event.get("calls", []) if call["name"] in names]

The coordinator lists both in sub_agents. ADK exposes delegation tools named pr_summarizer and style_checker; a call’s request carries the task text for that specialist. Here the coordinator should send the diff to both.

The specialists use mode="single_turn": each performs its task and returns a result without a user conversation. The coordinator remains a chat agent that can receive the original request and produce the final review.

Pydantic output schemas define the returned shapes. PrSummary.risk permits low, medium or high; it does not independently validate the model’s risk judgment. The style tool counts words, while the specialist decides how to explain its findings.

The two disallow_transfer_* flags remove transfer_to_agent from the specialists. This keeps their role focused on returning a result to the coordinator.

Sub-agents keep transfer_to_agent. In the bundled comparison without isolation, the summarizer transfers control instead of returning its typed result. The coordinator receives a sub-agent error and delegates again. The tests preserve that failure and check that the isolated specialists are not offered the transfer tool.

A sub-agent with two parents. Reusing one specialist instance in a second team raises ValidationError because it already has a parent. Construct a new instance for each team.

A single_turn root agent. A root agent cannot use single_turn; ADK raises ValueError: LlmAgent as root agent must have mode='chat' or 'task'. Keep that mode on the specialists.

Run the review

Review the bundled docs diffTerminal · bash
python review_client.py

Find the output’s delegations list. It should include both specialist names. Follow their function calls and responses, then compare the final review with the diff. The style specialist should call find_long_sentences on the added text.

The client sends one review request containing the entire diff. It prints a proposal, without editing a file or posting a review to a Git provider. A team turn can make several model requests across the coordinator and specialists.

To try a separate input, save it as my-docs.diff and use python review_client.py --diff my-docs.diff.

Read the review client
review_client.pySource · pythonDownload
"""Review one selected diff through the original isolated collaborative team."""
import argparse
import asyncio
import json
import os
from pathlib import Path
from google.adk.runners import InMemoryRunner
from review_team import build_team, delegations
from harness import run

HERE = Path(__file__).parent

def parse_options(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--diff", type=Path, default=HERE / "docs-pr.diff")
    return parser.parse_args(argv)

async def run_one(options, model=None):
    diff = options.diff.read_text(encoding="utf-8")
    if not diff.strip(): raise ValueError("diff must contain text")
    team = build_team(model=model or os.environ.get("DOCS_MODEL") or None)
    runner = InMemoryRunner(agent=team, app_name="docs_assistant")
    try:
        events, session_id = await run(team, f"Review this docs pull request:\n\n{diff}", runner=runner)
        result = {"session_id": session_id, "delegations": delegations(events), "events": events}
        print(json.dumps(result, ensure_ascii=False, indent=2)); return result
    finally:
        await runner.close()

if __name__ == "__main__": asyncio.run(run_one(parse_options()))

Test the team without a model

Check delegation and specialist boundaries offlineTerminal · bash
python -m unittest -v test_review_team.py test_client.py

The tests replay saved replies through the coordinator, specialists and real style tool. They also construct invalid teams and check the resulting errors.

The coordinator skips delegation. A fluent final answer can still omit both specialist calls. delegations(events) extracts the coordinator’s actual calls, and the test detects an answer without them.

Try changing the coordinator instruction to ask for only the summary in a separate copy. Before running it, decide which trajectory expectation must change. If both reviews are mandatory for every input, use graph edges to enforce them rather than relying on that instruction.

The final review looks sensible. How do you check that both specialists ran?

Choose one answer, then check your reasoning.

Read the explanation

The event stream records actual delegation calls; final text alone cannot demonstrate the required trajectory.

Return specialist results to the coordinator

This excerpt keeps the coordinator’s name and the delegation tool names from the first event:

A coordinator delegation eventRecorded output (excerpt) · jsonFull output
{
  "author": "docs_reviewer",
  "calls": [
    {
      "name": "pr_summarizer"
    },
    {
      "name": "style_checker"
    }
  ]
}

ADK runs each specialist and delivers its result as a function response. The coordinator then reads those results and generates the user-facing review. A specialist with tools can make its own tool calls before returning its structured answer.

The input to a specialist is the text supplied in the delegation request. Review exact arguments if a specialist appears to have missed part of a change; having a full diff in the user turn does not guarantee the coordinator forwarded it intact.

Use specialists for useful separations of task, instructions or tools. More agents also add model calls and handoffs to inspect. Use a deterministic function directly when a simple rule is all the job needs.

Next, turn expectations about tool use and answers into an evaluation set.

References

The code uses google-adk 2.12.0, as pinned in requirements.txt.

Written by Arjun Prabhulal.

Module 2

Workflows and Evals