How does a Google ADK LoopAgent decide to stop iterating?
answer
- exactly two ways out
- a number, or a signal from inside
- the loop itself judges nothing
- the checker must act, not just say so
- always set the backstop bound
basics
~20 sTwo ways only: it reaches max_iterations, or a sub-agent escalates — by setting escalate on the tool context's actions, or emitting an event with escalate set. With neither configured, the loop keeps re-running its sub_agents indefinitely.
solid answer
~50 s`LoopAgent` runs its `sub_agents` list in order, then starts the list again from the top, and repeats. It stops on exactly two conditions. The first is the numeric bound `max_iterations`, which you pass at construction. The second is **escalation**: a sub-agent signals that the loop's goal is met, typically by a tool setting `escalate` to true on its tool context's actions, or by a custom agent emitting an event whose actions carry escalate. The loop itself calls no model and evaluates no condition — the exit judgment lives inside one of the sub-agents, which is why the standard shape is a worker agent followed by a checker agent that escalates when quality is acceptable. Always set `max_iterations` anyway: it is the backstop that turns "the checker never escalates" from an unbounded token burn into a bounded, observable failure.
code
python · 23 linesfrom google.adk.agents import LlmAgent, LoopAgent
from google.adk.tools import ToolContext
def mark_done(tool_context: ToolContext) -> dict:
"""Call this only when the draft meets every requirement; it ends the review loop."""
tool_context.actions.escalate = True
return {"status": "approved"}
writer = LlmAgent(
name="writer", model="gemini-2.0-flash",
instruction="Improve the draft using the critique.\n\nDRAFT:\n{draft?}\n\nCRITIQUE:\n{critique?}",
output_key="draft",
)
critic = LlmAgent(
name="critic", model="gemini-2.0-flash",
instruction=("Review the draft below. If it meets every requirement, call mark_done. "
"Otherwise list the specific problems.\n\nDRAFT:\n{draft?}"),
tools=[mark_done],
output_key="critique",
)
refine = LoopAgent(name="refine", sub_agents=[writer, critic], max_iterations=5)go deeper
Know that LoopAgent repeats its sub_agents and that max_iterations caps how many passes it makes. Be able to say a sub-agent can also signal the loop to stop early.
Explain both exits precisely — the numeric bound and escalation raised from inside a sub-agent — and describe the worker-plus-checker shape where the checker owns the exit criterion.
Diagnose from symptoms: uniform maximum iteration counts mean escalation never fires, uniform single passes mean the checker is too lenient, and drafts that change without improving mean the critique is not wired back into the worker's prompt.
Own the loop as a budget: worst-case cost and latency scale with sub-agent count times the bound, so set the bound from spend and SLO, monitor the iteration-count distribution as a health signal, and require every loop to declare a backstop.
## What the loop actually does `LoopAgent(name=..., sub_agents=[...], max_iterations=N)` is a workflow agent, so like `SequentialAgent` and `ParallelAgent` it invokes no model of its own. Its behaviour is: run sub-agent 1, then 2, then 3; that is one iteration; go back to sub-agent 1; repeat. It carries the same invocation context and the same session state across every iteration, which is what makes progress possible — each pass reads what the previous pass left in state and improves on it. ## Termination condition one: max_iterations The simplest bound. `max_iterations=5` means at most five passes over the sub-agent list, after which the loop finishes regardless of whether anything was achieved. If you leave it unset, nothing in the framework will stop the loop for you. `max_iterations` is not primarily a quality mechanism; it is a **cost and liveness** mechanism. Iterative agent loops fail in a characteristic way — the refiner and the critic settle into a stable disagreement, each pass producing an almost identical draft and an almost identical critique — and without a bound that failure is an unbounded spend that ends when someone notices the bill or the request times out. Set it on every loop, including ones you are confident will escalate. ## Termination condition two: escalation The interesting exit is semantic: stop because the work is *done*. ADK expresses this as escalation. A tool running inside a sub-agent sets `escalate` to true on its tool context's actions, and the loop treats that as a signal to end after the current step. A custom agent built on the base agent class can emit an event whose actions carry escalate for the same effect. The key structural point is that the loop delegates the exit judgment to its sub-agents. There is no `condition=` parameter on `LoopAgent` and no LLM inside it asking "are we done?". That produces the standard two-member loop: 1. a **worker** agent that improves the artifact in state (`output_key="draft"`), 2. a **checker** agent whose only job is to evaluate the draft and, when it passes, call a tool that escalates. The checker is where the exit criterion is written, in that agent's instruction, in natural language you can iterate on independently of the worker. ## The failure modes **No exit at all.** The most common bug is a checker whose instruction says "decide whether the draft is good enough" without ever telling it to *call the escalation tool*, or a checker with no such tool registered. The model dutifully replies "yes, this is good enough" as text — text which the loop does not read, because the loop reads actions, not prose. The loop then runs to `max_iterations` every single time. Symptom: every run costs exactly the maximum. If you see uniform maximum iteration counts in your traces, look here first. **Escalating on the first pass.** A checker with a lenient instruction escalates immediately, and the loop degenerates into a single pass — an expensive way to write a `SequentialAgent`. Uniform iteration counts of one are the mirror-image symptom. **No progress carried between passes.** Because iterations share state, the worker must actually overwrite the artifact key each pass. If the worker's `output_key` differs from the key the checker reads, or the worker regenerates from the original input each time without seeing the critique, the loop spins producing new drafts that are not better drafts. Wire the critique into the worker's instruction so pass N+1 sees pass N's feedback. **Oscillation.** Two passes alternating between two acceptable-but-different drafts consume the full budget without converging. Bounding iterations covers the cost; converging usually means making the checker's criterion concrete and checkable rather than aesthetic. ## Operating a loop in production Instrument the iteration count and treat its distribution as a health metric. A healthy refinement loop shows a spread — mostly two or three passes, occasionally the maximum. A distribution pinned at either end means the escalation path is broken in one of the two directions above, and it will not surface as an error because both outcomes are, to the framework, a successful run. Also budget the loop as a whole rather than per agent. A three-member loop with `max_iterations=5` is up to fifteen model calls with a context that grows as state accumulates, so worst-case cost and worst-case latency are both multiplied by the bound. Choose `max_iterations` from what you are willing to spend and wait for on the worst request, not from what feels generous. ## The summary "Iterations run out, or a sub-agent escalates." The senior half of the answer is the follow-on: the loop has no opinion about doneness, so somebody inside it must actively signal it, and `max_iterations` exists to bound what happens when nobody does.
- Your loop always runs exactly max_iterations. What is the likely bug?The escalation path is never firing. Usually the checker agent has no tool that sets escalate, or its instruction asks it to *say* whether the draft is acceptable rather than to *call* the tool that ends the loop. The framework reads the action, not the prose, so a confident "this is good enough" in the reply text has no effect. Confirm by checking whether the escalating tool is invoked at all in the trace.
- Why set max_iterations even when the loop reliably escalates?Because it is the backstop against the failure you have not seen yet — a model change, a prompt edit or an odd input that makes the checker never satisfied. Without it the loop is unbounded, and the failure presents as runaway spend and a hanging request rather than a clean result. It costs nothing when escalation works and converts an outage into a bounded, degraded answer when it does not.
- How does the worker in a refinement loop see the previous pass's critique?Through session state, since iterations share it. The checker writes its critique under an `output_key`, and the worker's instruction templates that key in — with the optional form so the first pass, which has no critique yet, still assembles. If you skip that wiring, each pass regenerates blind and you get different drafts rather than better ones.
- How do you size max_iterations for a three-agent loop?Multiply out the worst case: three sub-agents at five iterations is up to fifteen model calls, with context growing as state accumulates, so both worst-case cost and worst-case latency scale with the bound. Pick the number from the spend and latency you will tolerate on your slowest request, then watch the iteration-count distribution — if runs cluster at the maximum, fix the exit criterion rather than raising the bound.
saying these in an interview costs you the question
- Thinking LoopAgent evaluates a stopping condition itself
- Expecting the model saying 'done' in its reply to end the loop
- Leaving max_iterations unset because the checker 'always' escalates
- Assuming each iteration starts from fresh state
- Treating a run that hits the maximum every time as normal