Skip to content
intermediate Phase 12 · Agent Loops and Planning

The Core Agent Loop

Implement think-act-observe cycles that drive a model toward a task goal.

55m
0 problems
Topic Progress 0%

The Run That Kept Repeating Itself

An on-call bot receives this at 02:14:

user: Which service caused the checkout incident, and what changed in deploy 4471?

Its first version sends one prompt and hopes the model already knows. Its second version is more ambitious: while not done: calling tools as fast as the model proposes them. By 02:16 it has called search_deploy eleven times with identical arguments, the transcript is 90k tokens, and no answer has arrived. Both versions miss the same thing: a cycle with fixed phases and a hard bound on how many times it may run.

The cycle

goal + user request
      │
      ▼
┌───────────────┐
│  THINK        │  reason: what is known, what is missing
└──────┬────────┘
       ▼
┌───────────────┐
│  ACT          │  commit to exactly one action
└──────┬────────┘
       ▼
┌───────────────┐
│  OBSERVE      │  run it, append the result to the scratchpad
└──────┬────────┘
       ▼
goal met? ── no ──► THINK again (step cap not reached)
       │
      yes / cap hit
       ▼
answer, or bounded failure

Each phase answers a question the others cannot. Think is where the model decides what to do next. Act is where the world gets touched — one tool call, not a wish list. Observe is where reality is written down. Skipping observe is exactly what produced those eleven identical calls: the model never saw its own previous result, so as far as it knew, nothing had happened yet.

The scratchpad — the list of thought, action, observation triples re-sent every iteration — is the loop's only memory. Without it, each turn starts amnesiac. With it, step 6 can cite the file step 2 read, which is the property that makes multi-tool tasks solvable at all, and the property Evaluating Memory Quality exists to measure in a shipped product.

The bound matters as much as the cycle. while not done: has no answer for a confused model, and confusion is the common case here rather than the exception. A step cap plus a goal check means the loop ends either way: with a result, or with a failure a human can inspect.

A Bounded Think-Act-Observe Loop

The engine is a for loop with an explicit done signal. Each iteration hands the full scratchpad back to the model:

import json

MAX_STEPS = 6

def run_agent(goal, ask, act, emit):
    scratchpad = []                    # every (thought, action, observation) triple
    for step in range(MAX_STEPS):      # the cap is structural, not a runtime hope
        thought = ask(goal, scratchpad)
        action = thought.get("action")  # {"tool": ..., "args": ...} or None

        if action is None:             # the model called done(answer) explicitly
            return {"status": "done", "answer": thought["answer"],
                    "steps": step, "trace": scratchpad}

        observation = act(action["tool"], action["args"])
        scratchpad.append({"step": step, "action": action,
                           "observation": observation})
        emit("agent.step", {"step": step, "tool": action["tool"],
                             "result_bytes": len(json.dumps(observation))})

    return {"status": "step_cap", "steps": MAX_STEPS, "trace": scratchpad}

Run against the 02:14 request:

goal: Which service caused the checkout incident, and what changed in deploy 4471?

step 0  search_deploy(incident="checkout")  → {"service": "payments-api", "deploy": 4471}
step 1  get_diff(deploy=4471)               → {"files": 12, "summary": "retry budget 5s → 1s"}
step 2  action=None                         → answer ready

status=done steps=3

Four lines carry the design:

  1. ask(goal, scratchpad) — the model sees every prior observation, so step 1 acts on what step 0 found. The cost is real: each round re-sends the transcript, so scratchpad size is the loop's main cost lever.
  2. action is None — completion is a signal the model emits, not a guess about its prose. Inferring done from fluent text ends runs early with confident wrong answers.
  3. observation = act(...) — state stores the tool's return value, never the model's paraphrase of it. Paraphrase is where facts drift.
  4. for step in range(MAX_STEPS) — a bug in the done check cannot unbound the run the way a while loop would.

When the cap hits, the function returns a structured failure carrying the trace instead of the last thing the model said. The guards themselves — iteration cap, goal check, token budget — are the subject of Stopping Conditions and Budgets.

Loop States, Scratchpad, and Trace

Two structural choices decide whether this loop is debuggable a week later.

States as data, not recursion. Write the transitions as named values and assert the legal ones:

THINK ──act──► ACT ──observe──► OBSERVE ──not done──► THINK
  ▲                                                         │
  └────────────────────── done ─────────────────────────────┘

Recursive run() calls hide the transition history inside the call stack, so OBSERVE → ACT — acting twice without looking — is invisible. With an explicit state variable that sequence fails an assertion at the exact line it happened. Keep the scratchpad attached to OBSERVE, because a state you can name is a state you can log, and a state you can log is a state you can compare between a passing run and a failing one.

Failure modes, in the order they show up in real runs:

  • Runaway loop. while not done: with no cap: a confused model re-issues calls until the token budget drains. The cap must be structural — see Stopping Conditions and Budgets.
  • Echo step. The model repeats its previous action because its observation never reached the prompt. The scratchpad was built but not re-sent, which looks from the model's side exactly like nothing happened.
  • Silent finish. The model returns prose with no done signal and no goal check. The loop either keeps working on a finished task or accepts a fluent answer the goal check would have rejected.
  • Blind run. No trace events. The run failed at 02:14 and nothing can explain why. Emit one structured event per iteration — step index, action, arguments, observation size, latency — into the same pipeline you use for Tracing LLM Calls, and the run replays offline step by step with the exact inputs.

This gives you a bounded machine that can touch the world. What it does first, in what order, and whether it reorganizes after a surprise is Planning Strategies for Agents, the topic that follows this one.

Quiz

1. What must the loop re-send to the model on every iteration?

Question 1 options

2. Why require an explicit done signal instead of inferring completion from the model's prose?

Question 2 options

3. What does the OBSERVE phase provide that THINK and ACT cannot?

Question 3 options

4. The step cap is reached with no answer. What should run_agent return?

Question 4 options

Flashcards

Question

What are the four phases of the core agent loop?

Answer

Think (reason about known and missing), Act (commit to one tool call), Observe (record the real result), then repeat until the goal is met or the step cap hits.

Question

Why must the scratchpad be re-sent every iteration?

Answer

It is the loop's only memory. Omit it and each turn starts amnesiac, so the model re-runs actions whose results it already produced.

Question

Why model the loop as explicit states instead of recursion?

Answer

Named states with asserted transitions make illegal sequences like acting twice without observing fail at a specific line, rather than hiding in the call stack.

Question

What goes into one trace event?

Answer

Step index, action name, arguments, observation size, and latency — enough to replay a failed run offline with its exact inputs.

Question

What does the loop return when the step cap is hit?

Answer

A structured failure status with the full trace, not the last partial answer, so a human can see exactly where progress stopped.

Revision Notes

Key Takeaways

  • 1. The loop is think, act, observe — each phase produces what the others cannot, and observe is what stops repeat actions
  • 2. The scratchpad is the only memory; it must be re-sent every iteration and is the loop's main cost lever
  • 3. Bound the run structurally with a step cap, and end it on an explicit done signal verified against the goal
  • 4. One trace event per iteration is what makes a failed run replayable instead of merely unfortunate

Interview Tips

  • • Draw the cycle and name what each phase contributes before writing any code
  • • Explain why you record the tool return rather than the model's summary of it
  • • Distinguish the step cap (guarantees termination) from the goal check (allows early exit) — they solve different failures
  • • Say what you log per iteration so you can answer 'how would you debug this run last night?'

Cheat Sheet

The Core Agent Loop — Cheat Sheet

Cycle

THINK → ACT → OBSERVE → (goal met? → answer : THINK again)

Skeleton

for step in range(MAX_STEPS):          # cap is structural
    thought = ask(goal, scratchpad)    # full history every time
    action = thought.get('action')
    if action is None:                 # explicit done signal
        return {'status': 'done', 'answer': thought['answer'], 'trace': scratchpad}
    observation = act(action['tool'], action['args'])
    scratchpad.append({"step": step, "action": action, "observation": observation})
    emit('agent.step', {...})          # one event per iteration
return {'status': 'step_cap', 'trace': scratchpad}

Rules

  • Record the tool's return value, never the model's paraphrase of it
  • Completion = emitted signal checked by a goal verifier, not fluent prose
  • States as named values with asserted transitions; recursion hides history
  • Emit step index, args, observation size, latency → replay failures offline

Failure modes

Symptom Cause Fix
identical calls until tokens drain while not done, no cap structural for cap + goal check
same action re-issued observation never re-sent append and re-send scratchpad
answer arrives with goal unmet done inferred from prose explicit done tool + verifier
cannot explain a 02:14 failure no trace events per-iteration structured events

Continue Learning