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.
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.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtOn 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:
unset GOOGLE_GENAI_USE_VERTEXAI DOCS_MODEL
export GOOGLE_GENAI_USE_ENTERPRISE=0Sending 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.
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.
"""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
python review_client.pyFind 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 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
python -m unittest -v test_review_team.py test_client.pyThe 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.
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:
{
"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.
- ADK collaborative workflows: delegation and agent modes.
- ADK workflow data handling: typed input and output.
- ADK LLM agents: tools and instructions.