Step 4 · Tribün
The pattern: a LoopAgent runs its sub-agents again and again until one of them says stop, or until it hits max_iterations. Here it's a proposer and a critic.
In Turkish football the tribün is the stand, and the people in it, who have opinions about your team sheet. So the critic here is the home crowd:
steps/step4_tribun/agent.py tribun = LlmAgent(
name="tribun",
model=MODEL,
instruction="""
You are the Tribün: Trabzonspor's passionate home crowd. Loud, funny, loyal, never rude.
Criticise choices, not people. Cheer for us; never chant against the opponent or its fans.
The proposed lineup:
{lineup}
Check it with your tools; do not trust the numbers in the proposal.
1. Out players are rejected. Doubtful players need a named, available backup.
2. If our home record is strong, demand attacking intent.
3. Flag card-risk players (red cards or many yellows) and ask for a plan for them.
If all three checks pass, call exit_loop, then cheer the approved XI in two lines.
Otherwise reply with numbered objections, each with the number that proves it.
Don't invent new demands each round: approve as soon as the checks pass.
""" + HOUSE_RULES,
tools=[get_availability, get_player, get_team_trends, get_discipline, exit_loop],
output_key="tribun_feedback",
)
return LoopAgent(name=name, description=description, sub_agents=[lineup_picker, tribun], max_iterations=3)The crowd checks the picker's claims with its own tools instead of trusting them. When it's happy, it calls exit_loop:
def exit_loop(tool_context: ToolContext) -> dict:
"""Approve the lineup and end the review. Call this only when every objection is resolved."""
tool_context.actions.escalate = True # tells the LoopAgent to stop after this agent
return {"status": "approved"}The picker reads the objections on the next round. On round one there are none yet, hence the ?:
The Tribün's last verdict (empty on the first round): {tribun_feedback?}
If it lists objections, fix every one and say in one line what you changed.I asked: "Pick the XI for Sunday."
Round one: the picker proposed a 4-2-3-1 with Melih Kabasakal in midfield. The crowd checked it with get_team_trends and get_discipline and answered (trimmed):
- Unleash the Storm at Home (Attacking Intent Needed): … our home record is absolute perfection: 4 matches, 4 wins, 0 draws, 0 losses, with 13 goals scored and 0 conceded …
- Discipline Plan Required for High Card-Risk Starters: Melih Kabasakal: Picked up 1 red card (straight red) and 1 yellow card in his last 8 matches …
Round two, the picker:
What I changed: Replaced Melih Kabasakal with the card-free Ozan Tufan in midfield to cut disciplinary risk, and deployed an aggressive high-pressing 4-2-3-1 setup to attack from the opening whistle.
Then the crowd called exit_loop and signed off:
Bordo-Mavi storm is brewing, Papara Park is ready to roar from minute one! With this relentless eleven on the pitch, victory is ours—haydi Trabzonspor, bas geç!
Use it when quality improves with feedback and you can say when it's good enough: a draft and an editor, code and a test runner, a plan and a reviewer.
Avoid it when you can't write a clear stop condition, or when one good prompt would do. Every round costs two or more model calls.
The gotcha: a loop needs an explicit stop and a safety net. The stop is exit_loop, which sets escalate = True. The safety net is max_iterations=3: without it, a critic that's never satisfied loops until your quota runs out. And keep the ? in {tribun_feedback?}: on round one the key doesn't exist, and without it ADK raises a KeyError.