Step 1 · The Scout
The pattern: a single LlmAgent is an instruction (who it is, how it works) plus tools (plain Python functions it can call).
Here's the whole agent:
steps/step1_scout/agent.pydef build_scout(
name: str = "scout",
description: str = "Looks up players and team form: stats, availability, top performers, recent results.",
tools: list | None = None,
) -> LlmAgent:
return LlmAgent(
name=name,
model=MODEL,
description=description,
instruction="""
You are the scout on Trabzonspor's analysis staff. "We" and "us" always mean Trabzonspor.
Answer questions about players and teams.
How you work:
1. Look everything up with your tools before you answer. Never answer from memory.
2. "In form" means recent output and ratings: check top players by goals, assists and avg_rating.
3. "Who can't play" means the availability list: out and doubtful players with the reason.
4. Name the player, then the numbers that back up your point.
""" + HOUSE_RULES,
tools=tools or SCOUT_TOOLS,
)
root_agent = build_scout()HOUSE_RULES comes from config.py and goes into every agent: answer in the user's language, and only use numbers from a tool. Type in Turkish and you get Turkish back. The agent is built by a function, build_scout(), which pays off in step 5.
ADK turns each tool's type hints and docstring into the schema the model sees, so the docstring is written for the model: it's all it knows about the tool. Inside, the tools do the counting, like the share of goals per 15-minute window:
matchday/tools.py t["goals_by_15min"] = {
window: {
**g,
"pct_of_goals_scored": round(100 * g["scored"] / total_for) if total_for else NA,
"pct_of_goals_conceded": round(100 * g["conceded"] / total_against) if total_against else NA,
}
for window, g in t["goals_by_15min"].items()
}Name matching ignores Turkish and Polish letters (simsir finds Aral Şimşir), and bad input returns an error with suggestions instead of raising.
I asked: "Who's in form for us, and who can't play Sunday?"
The scout called get_availability once and get_top_players three times (goals, assists, avg_rating), then answered in 12 seconds. Trimmed:
In Form (Last 10 Matches)
- Mohamed Salah (Available): 8 goals, 5 assists, and an 8.03 average rating in 10 matches (802 minutes).
- Ernest Muçi (Available): 2 goals, 5 assists, and a 7.14 average rating in 9 matches (549 minutes).
- (Note: Franculino has also been in top form with 7 goals, 2 assists, and a 7.75 average rating in 8 matches / 545 minutes, but he is currently doubtful). …
Use it when one job needs a handful of tools and some judgement: a support bot, a data assistant, a code helper.
Avoid it when the instruction grows into a long list of "first do this, then that". That's a workflow, and step 2 is for that.
The gotcha: tools do the maths. Every number an agent states should come out of a tool. Give the model computed answers (pct_of_goals_conceded: 33), not raw rows to add up. My smoke script flags any number in an answer that appears in no tool result; step 5 shows what happens when you skip this.