Skip to content
advanced

The Anatomy of an Agent Loop: State, Action, Observation, Feedback, and Termination

The demo works. The agent books the flight, summarizes the repo, files the ticket. Then you point it at a task with one extra branch, and it calls the same…

Published 2026-09-11Updated 2026-09-1215 min read
Stunning close-up portrait of a black horse against a dark backdrop, showcasing its elegance.
Stunning close-up portrait of a black horse against a dark backdrop, showcasing its elegance. Photo by Missi Köpf on Pexels.

The demo works. The agent books the flight, summarizes the repo, files the ticket. Then you point it at a task with one extra branch, and it calls the same failing tool eleven times, forgets the constraint you gave it in turn two, and exits with a confident summary of work it never did.

That failure is rarely a model failure. It is a contract failure. The loop had no contract.

Most agent code starts as while not done: response = llm(messages); messages.append(response). That is not an architecture. It is a hope with a token budget. The moment you write it down as a state machine — with named state, a validated action interface, structured observations, an explicit progress judgment, and enumerated exit conditions — the failure modes stop being mysterious and start being debuggable.

This article specifies the five contract elements of an agent loop and the invariant that holds them together. By the end, you should be able to write the transition contract for your own harness: what state it carries, how actions and observations flow, how feedback is judged, and every condition under which the loop stops.

The Loop Is a State Machine, Not a While Loop

A left-to-right agent-loop flowchart shows durable state feeding context into a model proposal, then validation and action execution producing an observation. Feedback returns to updated state, while invalid actions and failed progress follow a bounded retry path and success or budget or stall conditions lead to named termination.
The harness owns the transition: every iteration updates state or consumes a bounded resource, and every exit has a named reason.

The canonical cycle is: assemble context, invoke the model, select an action, validate it, execute it, observe the result, record it, judge progress, decide whether to continue. Every production agent loop implements some version of this, whether or not its authors named the stages.

The "LLM in a while loop" framing hides the actual design surface. It implies the model owns the loop. It does not. The harness owns the loop. The model proposes; the harness decides what is legal, what gets executed, what gets remembered, and when the run ends. Frameworks differ in naming, SDK shape, and where they put the seams, but the underlying mechanism is the same: a state transition function with defined inputs, outputs, and exit conditions.

Five elements carry the contract:

  • State — what the loop remembers across iterations.
  • Action — the interface through which the model requests a change in the world.
  • Observation — what the environment returns after an action executes.
  • Feedback — the verdict on whether that action moved the task forward.
  • Termination — the named, budgeted conditions under which the loop stops.

The invariant that makes the whole thing coherent: every iteration must either advance state toward the goal or consume a bounded resource. An iteration that does neither is a bug, not a step. If you cannot point to what changed between iteration N and iteration N+1, you do not have a loop. You have a spin.

Knowledge check

Check your understanding

Answer this question before you continue.

Which condition must hold for every iteration to avoid a spin?
Single Choice

Focus: Identify the invariant that distinguishes a valid agent-loop iteration from a spin.

The Transition Contract

Before the five elements, the relation that connects them. An agent loop is a transition function:

(state, proposal) -> validated action | invalid event
validated action -> observation -> feedback -> next state | exit

Each element in the contract is an input, output, or invariant on that relation. State is the input. The proposal is the model's raw output. Validation produces either a legal action or an invalid event that still consumes a turn. Execution produces an observation. Feedback produces a verdict. The next state is a function of the previous state, the action, the observation, and the verdict — or the loop exits with a named reason.

Keep this relation in view. Every section below maps to one of its edges, and every failure mode is a broken edge.

Knowledge check

Check your understanding

Answer this question before you continue.

A model emits a proposal that fails action validation. According to the transition contract, what should the harness do?
Scenario Interpretation

Focus: Trace how an invalid model proposal affects the transition contract.

State: What the Loop Must Remember

State is a selection problem, not a transcript dump. Two distinct things get called "state," and conflating them causes most memory bugs:

  • Working context — the material actually sent to the model this iteration: system prompt, task description, relevant history, current observations.
  • Durable execution record — the harness-side log of actions taken, observations received, feedback verdicts, and any derived progress signal.

The working context is a projection of the execution record. It is lossy on purpose. The execution record is the source of truth; the context is what the model needs right now to make the next decision.

A minimal state schema, with each field marked by its role:

FieldRolePurpose
Task contractDurableGoal, constraints, verification criteria
Event logDurableActions, observations, verdicts, in order
Progress signalDerivedSteps completed, tests passing, distance to goal
Resource countersDurableTurns used, tokens spent, wall-clock elapsed
Last action fingerprintDerivedDetects repetition when paired with the next observation
Exit statusDurableNamed reason the loop stopped, or running

What belongs in the context is a subset of the record, chosen per iteration. Append-only memory degrades predictably. Stale observations crowd out current ones. A tool error from iteration three sits in the window while the model tries to reason about iteration nine. Token cost climbs linearly with iteration count while decision quality falls. The model re-reads irrelevant history and pays attention to it.

Compaction and summarization are state transformations, and they are dangerous. A summarizer that drops "the API returned 403 because the token expired" while keeping "the API call was attempted" has silently deleted the evidence the next decision needs. My rule: compact the narrative, never the evidence. Keep structured records of actions, observations, and verdicts; summarize only the reasoning prose around them. Archive the full transcript before compaction so you can reconstruct what was lost.

A concrete divergence: a task that requires reading three files and editing one. With append-only memory, the model sees every file's full contents in every subsequent turn, and by iteration six the edit decision is buried under forty thousand tokens of irrelevant source. With a structured record — files read, files edited, current diff, test status — the context stays small and the next decision is obvious. Same model, same task, different state design, different outcome.

Action and Observation: The Interface Contract

Most loop failures are interface failures. The model proposes an action; the harness validates and executes it. The model never touches the environment directly. That separation is the entire safety and debuggability story.

Prefer structured action schemas over free-text parsing. String matching on model output — looking for ACTION: or FINAL ANSWER: — is fragile dispatch and fragile termination. A model that paraphrases, adds whitespace, or emits a near-miss token breaks your loop in ways that look like model failures but are parser failures. Schema-constrained tool calls, or at minimum a strict grammar with validation and a repair path, remove that class of bug.

Observation design is where most teams underinvest. The observation should return the signal the next decision needs, not the raw dump of the tool's output. If a search returns two hundred results, the observation is the top few with enough metadata to choose, not the full payload. If a test run fails, the observation is the failing assertion and the relevant stack frame, not the entire log.

Truncation, error payloads, and partial results are first-class observation types, not exceptions. A truncated result should say it was truncated and how. An error should be structured — error class, message, whether retry is sensible — so the model can reason about recovery instead of guessing. A partial result should say what completed and what did not. When these arrive as unstructured strings, the model treats them as noise; when they arrive as typed fields, the model can act on them.

One boundary deserves its own line: an action can fail deterministically, or it can fail ambiguously. A 400 response is deterministic — the action did not happen. A timeout on a write is ambiguous — the action may have happened, and the observation is missing. For side-effecting actions, the recovery contract needs an idempotency key, a reconciliation read, or an explicit escalation path. Retrying a write whose outcome is unknown is how a loop double-charges a customer or double-files a ticket. The observation contract must distinguish "failed" from "unknown," because the recovery is different.

The action space is a designed surface. A bad surface produces bad trajectories even with a strong model: ambiguous tool names, overlapping capabilities, missing arguments, no way to express "I need more information before acting." Treat the action schema the way you would treat a public API. Every ambiguity in the surface becomes a wrong turn in the trajectory.

Knowledge check

Check your understanding

Answer this question before you continue.

A side-effecting write times out, so the harness cannot tell whether it happened. Which recovery contract best matches the article?
Comparison Reasoning

Focus: Distinguish recovery requirements for deterministic action failure and ambiguous side-effect outcomes.

Feedback: Turning Observations Into Progress Signals

Observation is what the environment returned. Feedback is the verdict on whether the action moved the task forward. These are different, and the difference is what makes revision possible.

A loop with observations but no feedback cannot revise. It can only repeat. The model sees the same result, reaches the same conclusion, and issues the same action. This is the mechanism behind the eleven-identical-tool-calls failure: nothing in the loop ever told the model that the last attempt failed to make progress.

Feedback sources, roughly in order of reliability:

  • Deterministic checks — tests, schema validation, exit codes, type checks, diff inspection. Cheap, fast, unambiguous. Use these wherever the task admits them.
  • Model-based critique — a separate judgment call, ideally with a rubric, on whether the observation satisfies the current subgoal. Useful when no deterministic check exists, but it inherits the model's blind spots.
  • Human input — expensive, slow, and often the only honest signal for subjective tasks.

Binary success/fail is sufficient when the task decomposes into verifiable steps. Graded progress signals — tests passing out of total, files remaining, distance to a target — are worth the extra machinery when the loop needs to decide whether to keep pushing or change strategy. A binary signal tells you that you failed. A graded signal tells you whether you are getting warmer.

The failure mode to watch for: feedback that is always "success" because the check is too weak. A test suite that passes on an empty implementation, a schema validator that accepts any JSON, a critique prompt that is too polite to say no. These produce confident loops that never converge, because the loop believes it is making progress while the task sits untouched. If your agent reports success and the task is not done, the feedback layer is the first place to look.

Termination: Every Exit Needs a Name

"The model says it is finished" is not a termination condition. It is a claim that needs verification. Enumerate your exits explicitly, and give each one a name you can log.

Success termination. The model declares completion, and the harness verifies it against the task contract. Verification is the load-bearing part. For code, that means tests pass. For a data task, that means the output validates against the schema. For a research task, that means the required fields are populated. If you cannot verify, you have a weaker termination condition, and you should say so.

Budget termination. Iteration caps, token or cost budgets, wall-clock limits. These are different failure modes with different recovery paths. Hitting an iteration cap means the loop was still working; resuming with a higher cap is reasonable. Hitting a cost budget means the loop was expensive; resuming requires a strategy change, not just more money. Hitting a wall-clock limit usually means an external dependency is slow. Log the specific subtype so the operator knows which recovery applies.

Stall and repetition detection. Identical action plus identical observation, or a progress signal that has not moved in N iterations. This catches the loop that is technically within budget but is not advancing. It is the cheapest termination condition to add and the one that saves the most money.

Resumability. When the loop exits on a budget, what state must survive so the run can continue rather than restart? The execution record, the current working context, and the session identity. If a budget-exhausted exit throws away the trajectory, the next run pays for the same work twice.

A loop with one termination condition has zero termination conditions. The model will find the case where that condition does not fire.

A Failure Trace: Watching the Contract Catch a Spin

Here is the eleven-identical-tool-calls failure, traced through the contract. The task: read three config files and update one. The model keeps calling read_file on a path that does not exist.

IterActionObservationFeedbackProgress deltaNext
1read_file("config.yaml")ENOENT: no such filefail (deterministic)0continue
2read_file("config.yaml")ENOENT: no such filefail (deterministic)0continue
3read_file("config.yaml")ENOENT: no such filefail (deterministic)0continue
4read_file("config.yaml")ENOENT: no such filefail (deterministic)0stall detected

The stall detector fires on iteration four: identical action fingerprint plus identical observation, progress delta zero for three consecutive iterations. The loop exits with exit_reason = "stall", not "success". The operator sees the trace and knows the model never found the file — the fix is a directory listing tool or a corrected path, not a bigger budget.

Without the progress delta column, this trace looks like a working loop that is simply "still trying." The delta is the field that turns a silent spin into a named exit.

Knowledge check

Check your understanding

Answer this question before you continue.

In the failure trace, what exit reason should be recorded after the fourth identical missing-file read?
Output Prediction

Focus: Predict the named termination reason produced by repeated identical actions with no progress.

The action and observation are identical on iterations 1–4, and the progress delta is zero on each iteration.

Hooks and Observability: Instrumenting the Transitions

Hooks are callbacks at defined points in the loop: before a tool executes, after it returns, when the agent finishes, before context compaction. They are where you enforce the contract and where you measure it.

The useful hook points and what each is for:

  • Before action execution — validate arguments, block dangerous operations, enforce rate limits, log the intended action. This is your last chance to stop a bad call before it touches the world.
  • After observation — audit outputs, trigger side effects, normalize the observation into the structured form the next iteration expects.
  • On stop — validate the result, persist session state, emit the final verdict with its subtype.
  • Before compaction — archive the full transcript before summarizing, so the evidence survives.

Per-iteration telemetry is what makes the loop debuggable: tokens consumed, cost, latency, action type, feedback verdict, and progress delta. The progress delta is the one most teams skip and the one that matters most. If you log nothing else, log whether each iteration advanced state.

A loop you cannot replay is a loop you cannot debug. The minimum event log to reconstruct a trajectory: for each iteration, the context hash, the model output, the validated action, the observation, the feedback verdict, and the progress delta. With that, you can replay any run and find the exact iteration where the loop went wrong. Without it, you are reading tea leaves in a summary.

Framework hook abstractions are conveniences over this mechanism. The names differ; the transition points do not. Learn the mechanism, and any framework's hook system becomes a mapping exercise rather than a new mental model.

A Minimal Loop Contract You Can Implement

Here is the skeleton I would write before touching a framework. Note that every branch — including invalid proposals and failed verification — passes through the same budget check before continuing:

state = init_state(task)                 # task, event log, progress, counters
while True:
    if exit_condition(state):            # budget, stall, wall-clock
        return terminated(state.exit_reason, state)

    context = assemble_context(state)    # projection, not the full record
    proposal = model(context)            # model proposes
    action = validate(proposal)          # harness validates

    if action is None:
        state = record_invalid(state, proposal)   # consumes a turn
        continue

    if action.kind == "finish":
        if verify(action.result, task.contract):
            return success(action.result, state)
        state = record_failed_verification(state, action)
        continue

    observation = execute(action)        # harness executes
    verdict = judge(observation, state)  # feedback, not observation
    state = update(state, action, observation, verdict)

The exit_condition check at the top of the loop is the structural fix. Every path — invalid proposal, failed verification, executed action — returns to the top and re-checks the budget. No branch can spin without consuming a turn.

The contract table:

ElementOwnsMust guaranteeBreaks when missing
StateWhat persists across iterationsEvery iteration changes it or consumes budgetLoops repeat; context degrades
ActionThe interface to the worldValidated before execution; schema-constrainedParser failures look like model failures
ObservationWhat the environment returnedStructured, truncated explicitly, errors typedModel guesses at recovery
FeedbackThe progress verdictIndependent of the model's self-reportLoop repeats without revising
TerminationEvery exit pathNamed, budgeted, resumableInfinite loops; silent budget burn

When not to build a loop: single-shot tasks, deterministic pipelines, and any case where a fixed workflow beats open-ended control. If you can enumerate the steps in advance, write the workflow. A loop earns its complexity only when the path is genuinely unknown.

The smallest useful drill: take one real task, give it a hard iteration cap of five, and attach a deterministic feedback check — a test, a schema validation, an exit code. Run it. Then inspect the per-iteration progress deltas and find the first iteration that did not advance state. That iteration is your bug, and it is almost never where you expected it.

If you cannot name the state, the action schema, the feedback source, and every termination condition, you have a demo, not a loop. Instrument one existing agent with per-iteration progress deltas this week. Find the first non-advancing iteration. Fix the contract, not the prompt.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

A model returns a confident final answer saying the task is complete, but the required tests fail. What should the harness do?
Question 1 of 2Misconception Check

Focus: Apply the rule that a completion claim requires harness verification before success termination.

Which design best satisfies the minimal loop contract described in the article?
Question 2 of 2Comparison Reasoning

Focus: Select a loop architecture that satisfies the article's state, feedback, and termination contract.

Related sites

Build the foundations behind advanced AI systems

Use LearnLLMFast for practical LLM application foundations and LearnPyFast for the Python mechanisms that support implementation work.

LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast
Python tutorialstutorial

LearnPyFast

Beginner-friendly Python tutorials, examples, and learning paths for practical programming foundations.

PythonProgrammingBeginners
Visit LearnPyFast

Keep exploring

Related AI engineering tutorials

Continue with adjacent system layers, implementation patterns, and current AI engineering ideas.