Loop agent¶
A loop agent runs a single ReAct loop repeatedly until the LLM signals
"done" or until max_iterations is hit — each iteration sees the
previous iteration's output and a coaching message telling the LLM
how to terminate. Pick this for iterative refinement: drafts that
need successive polish passes, optimisations that converge, or any
task where you can describe "good enough" but not a fixed step count.
Type identifier¶
YAML form:
What it does¶
runLoop in pkg/agent/adk/run_orchestration.go:
currentMessage := userMessage
for i := 0; i < a.config.MaxIterations; i++ {
emitDebug({type: "loop_iteration", iteration: i})
resp, err := a.runReAct(ctx, currentMessage)
if err != nil { return nil, err }
if containsCompletion(resp.Content) {
return resp, nil
}
currentMessage = fmt.Sprintf(
"Previous iteration result:\n%s\n\nContinue refining. When the task " +
"is fully complete, END your response with the single word DONE on " +
"its own (or trailing punctuation like 'DONE.').",
resp.Content)
}
return nil, fmt.Errorf("loop agent %s: max iterations reached", a.config.ID)
The termination signal is detected by containsCompletion, which
matches the regex (?i)\b(DONE|COMPLETE|FINISHED)\b[\s.!?]*$ —
case-insensitive, word-boundary, anchored to end-of-string. This is a
deliberate tightening: the legacy substring match also flagged
"abandoned" (matches DONE), "complete the form" (matches COMPLETE),
or "I'm done thinking, let me continue" (matches DONE mid-string),
which produced spurious early stops. The current regex only fires
when the LLM ends with one of the three sentinels, matching the
wording in the coaching message.
Per iteration:
- A FULL ReAct sub-loop runs (think → act → observe → … → final text). Each iteration can call tools.
- The iteration's final content becomes the next iteration's input, wrapped in the coaching prompt.
- An iteration error aborts the whole run (not just that iteration).
If the LLM never ends with DONE / COMPLETE / FINISHED, the run
exhausts MaxIterations and errors. There is no "best-effort return"
fallback.
Configuration¶
| Field | Type | Default | Description |
|---|---|---|---|
id, type, model, system_prompt, prompt_vars |
— | — | Standard. |
tools |
list | — | Available inside each iteration's ReAct sub-loop. |
memory |
block | conversation | Shared across iterations; each ReAct turn reads and writes it. |
max_iterations |
int | 10 | Maximum loop iterations. Note this is the OUTER loop budget — each iteration also has its own per-ReAct iteration cap, but the YAML field maps to the outer cap. |
guardrails, budget, governance, approval_*, tool_retry |
— | — | All apply to each iteration's ReAct sub-loop. |
max_iterations plays a dual role in loop agents: the outer loop
uses it as the iteration cap AND the inner ReAct uses the same
value. Set it high enough that an iteration can finish its tool
calls but low enough that runaway loops don't burn budget.
Behavior notes¶
- The completion regex IS case-insensitive (
DONE/done/Complete!/finishedall match) and tolerates trailing whitespace +.!?. It does NOT match prefixes — putting "DONE" at the start of a response will not stop the loop. - Customise the coaching message by giving the system prompt a domain-specific termination criterion (e.g. "When the score exceeds 0.9, end with DONE"). The coaching wrapper itself is fixed in code, but the system prompt is yours.
sub_agents:is NOT used. Loop is a single-agent type.- Loop is the LLM-driven counterpart to
flow.LoopFlowinpkg/agent/flow/, which uses a Go-level condition function instead of an LLM sentinel. See flows. - Streaming is not supported —
RunStreamfalls back to non-streaming and word-chunks the final text.
Example¶
A self-improving draft polisher:
agents:
- id: refiner
type: loop
max_iterations: 4
model:
provider: anthropic
model: claude-sonnet-4-7
system_prompt: |
You are a draft editor. On each iteration, review your
previous draft and produce an improved one. Stop when you
cannot meaningfully improve it further. END your response
with the single word DONE when finished.
tools:
- { name: read-file, type: function, function: fs.read }
budget:
max_tool_calls: 20
max_input_tokens: 200000
Invoke with "Polish this paragraph:
Source¶
pkg/agent/adk/run_orchestration.go—runLoop,containsCompletion,completionPattern.pkg/agent/adk/agent.go—Rundispatch onTypeLoop.pkg/workflow/definition.go—AgentDefinition.MaxIterations.
See also¶
- react — the inner per-iteration executor.
- plan-execute — explicit phases instead of open-ended iteration.
- flows —
flow.LoopFlowwith a Go-level condition function instead of a sentinel string.