Skip to content

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:

agents:
  - id: refiner
    type: loop
    max_iterations: 5
    model: { ... }
    tools: [ ... ]

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! / finished all 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.LoopFlow in pkg/agent/flow/, which uses a Go-level condition function instead of an LLM sentinel. See flows.
  • Streaming is not supported — RunStream falls 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: " — the agent iterates until either it emits DONE (early stop) or four iterations elapse.

Source

  • pkg/agent/adk/run_orchestration.go — runLoop, containsCompletion, completionPattern.
  • pkg/agent/adk/agent.go — Run dispatch on TypeLoop.
  • 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.LoopFlow with a Go-level condition function instead of a sentinel string.