23 · Loop Engineering
Loop engineering taught from real source code — mini-swe-agent, smolagents, Codex: budgets, stopping, error taxonomy, trajectory persistence, verification.
Chapter brief
Question to answer
How does a working while loop become an engineered system that stops, resumes, verifies, and replays?
By the end, you can
- Implement the five minimum parts: budget, stop, errors, trajectory, and verification
- Build a runnable loop in roughly 30 lines
- Test infinite retry, false completion, and interruption
- Read this now if
- Engineers writing a first agent or stripping away frameworks to understand the runtime
- Prerequisites
- Read basic Python; Understand tool calling
- Deliverable
- A runnable minimal loop and five failure tests
- Evidence boundary
- Examples show engineering patterns; budgets, thresholds, and verifiers require calibration to your model and task set
Break the ten-line loop three times first
Section titled “Break the ten-line loop three times first”This code demonstrates tool calling, but it is not yet a runtime:
context = [user_message]while True: step = llm(context) context.append(step) if step.intent == "done": return step.final_answer context.append(execute(step))Do not add a framework yet. Inject three failures:
| Failure | Bare-loop behavior | Engineering component to add |
|---|---|---|
| The model calls the same failing tool 20 times | Retries until a human kills it or the bill runs out | Error taxonomy, consecutive-failure breaker, step/cost/wall-clock budgets |
| The model says done while tests still fail | Returns “complete” | Structured completion action and external verifier |
| A tool edits a file, then the process dies before persisting the result | Restart cannot tell whether to redo it | Per-step persistence, operation IDs, replayable events, and recovery state |
A first loop is sufficient when it passes five checks:
- Step, cost, and wall-clock budgets are checked before every model call.
- Model mistakes can be repaired; framework failures raise; repeated same-class failures trip a breaker.
- Completion is structured and must pass at least one external check.
- Every step persists on success or failure; exits and continuations have reasons.
- After interruption, the harness can safely replay, skip committed work, or enter manual review.
Those checks are budget, stopping, errors, trajectory, and verification. Read the real source next not to copy a framework API, but to see how all five fit into 191 lines. ReAct supplies the observe/act research skeleton; loop engineering turns it into a system that can stop and be reconstructed.
Read a real one first: mini-swe-agent in 191 lines
Section titled “Read a real one first: mini-swe-agent in 191 lines”Before the method, the artifact. mini-swe-agent (from the SWE-bench team) implements its whole loop in agents/default.py — 191 lines that post real benchmark scores. Block by block (quoted from commit a83fcae):
The config is the loop-engineering checklist. AgentConfig has 7 fields, and 4 of them are safety nets:
step_limit: int = 0 # max stepscost_limit: float = 3.0 # stop after spending $3wall_time_limit_seconds: int = 0 # wall-clock capmax_consecutive_format_errors: int = 3 # exit after 3 format errors in a rowNote cost_limit defaults to 3.0 — dollars. Budgets aren’t just steps; they’re money. All three checks (steps, cost, wall time) sit at the top of query(), i.e. before every model call, raising LimitsExceeded / TimeExceeded when breached.
Exit is a message, not an escaping exception. The loop’s stop check is one line:
if self.messages[-1].get("role") == "exit": breakEvery exit path — normal submission, budget breach, repeated format errors — ends up as a role="exit" message appended to the list. Exceptions are just transport (the LimitsExceeded exception literally carries the exit message inside it). The payoff: the exit reason and final state land in the trajectory automatically; read the exit_status field and you know whether the run was Submitted, LimitsExceeded, or RepeatedFormatError.
The circuit breaker counts consecutive failures. On a format error, the error text is fed back as a message for retry, and n_consecutive_format_errors += 1; any clean step resets it to zero. Only 3 in a row exits. The counter isn’t “how many errors total” — it’s “are we stuck in the same hole.”
Every step hits disk, including failed ones. The loop body has finally: self.save(...) — success, format error, or uncaught exception, the trajectory file gets written first. After a crash, the disk always holds the complete scene of the last step.
None of the 191 lines is wasted. The five components below generalize them, plus other systems’ answers, into a portable method.
The five components
Section titled “The five components”A bare loop dies four ways in production: it won’t stop, it repeats one error forever, its context blows up, a crash loses everything. Five components answer them:
1. Stop conditions: multiple signals, trust none alone
Section titled “1. Stop conditions: multiple signals, trust none alone”The model’s own completion signal is unreliable. Cross-check several:
- An explicit completion action. smolagents (
agents.py, commite3a5b89) force-registers afinal_answertool; the model must call it explicitly to finish — “done” becomes a structured action, not free text. The loop condition is one line:while not returned_final_answer and self.step_number <= max_steps. - Verify after completion. smolagents’
final_answer_checksis a list of callbacks: after the model produces a final answer, each check runs, and any failedassertthrows anAgentErrorthat bounces the run back. The definition of “done” stays in your hands. - Don’t trust stop_reason. A Claude Code source comment states
stop_reason === 'tool_use'is sometimes wrong, so it counts tool_use blocks in the stream itself (see chapter 02). - A hard cap. maxTurns / step_limit is the final gate for when everything above fails.
2. Budgets: three dimensions, plus one graceful ending
Section titled “2. Budgets: three dimensions, plus one graceful ending”mini-swe-agent gives the complete definition: steps (stops spinning), cost in dollars (stops wallet fires), wall-clock time (stops hangs on slow tools). All three checked before each model call.
Budget exhaustion shouldn’t be a silent cutoff. smolagents’ _handle_max_steps_reached makes one extra call — provide_final_answer(task) — forcing the model to produce a final reply from its existing memory, recorded with an AgentMaxStepsError marker. Hermes calls this a grace call. The user gets “here’s what I did and what’s missing,” not empty hands.
One more smolagents mechanism worth its own note: planning_interval. Every N steps, an independent planning step is inserted — no actions, just the model re-examining the task and progress. On long tasks models sink into detail and forget the goal; periodic re-planning is the anti-drift alarm clock.
3. Error taxonomy: not every error deserves a retry
Section titled “3. Error taxonomy: not every error deserves a retry”smolagents splits error handling in two, plainly visible in _run_stream:
except AgentGenerationError as e: raise e # implementation bug: retrying won't help, exitexcept AgentError as e: action_step.error = e # model's mistake: record and keep iteratingThe framework’s own bugs get raised immediately; the model’s mistakes (bad format, bad tool args) get fed back for it to fix. mini-swe-agent adds the breaker on top: only 3 consecutive same-class failures exit, any success resets.
What goes back into context must be compacted. 12-Factor Agents Factor 9: not the raw stack, but “what failed, what was tried, what options remain.” One step further is Reflexion (Shinn et al. 2023): have the model write a verbal reflection on each failure into a separate episodic buffer, injected on every retry — no weight updates, language feedback only, and HumanEval pass@1 went from 80% to 91%. A retry that carries its lessons is the only retry worth paying for.
4. Trajectory persistence: every step is a replayable record
Section titled “4. Trajectory persistence: every step is a replayable record”mini-swe-agent saves in a finally on every step; the trajectory file holds the full message sequence, per-step cost, exit status, and a config snapshot (trajectory_format: "mini-swe-agent-1.1" — even the format is versioned). Codex goes further: the loop is an event machine, every event appended to a rollout JSONL, state rebuilt from the rollout after restart, and agents observe each other by reading each other’s rollouts.
The principle is 12-Factor’s Factor 12: the agent is a stateless reducer; the state is the event sequence. Once true, pause, resume, and machine migration are the same operation: read the log, rebuild, continue.
5. A verifier: completion is ruled by facts outside the loop
Section titled “5. A verifier: completion is ruled by facts outside the loop”Models fake completion. Verifiers come in hardness grades:
- Hard: test exit codes, patch syntax validation, command allowlists. Codex chains four in coding scenarios, all machine rulings (in our cloned
codexrepo,codex-rs/core/’sapply_patch.rsand the exec-policy modules are these verifiers). - Soft: smolagents’
final_answer_checkscallbacks, another model as reviewer (Anthropic’s evaluator-optimizer), Reflexion-style self-critique.
Verifier hardness sets the autonomy budget. With tests to run, let the loop go dozens of steps; with only soft verification, keep it short and bring humans in early.
Build it yourself: a skeleton
Section titled “Build it yourself: a skeleton”The five components reassembled (structure mirrors mini-swe-agent and smolagents; copy details straight from the two repos):
def run(task, step_limit=50, cost_limit=3.0): log = TrajectoryLog(task.id) # component 4 ctx = log.replay() or [task.as_message()] errors = ConsecutiveErrorCounter(limit=3) # breaker for component 3
while True: if over_budget(step_limit, cost_limit): # component 2: check before the call ctx.append(grace_prompt()) # one last chance to wrap up step = llm(ctx); log.append(step)
if step.calls("final_answer"): if all(check(step.answer) for check in final_checks): # component 5 return exit_message(log, "Submitted", step.answer) ctx.append(check_feedback()); continue
try: ctx.append(compact(execute(step))) errors.reset() except ModelError as e: # model's fault: feed back to fix ctx.append(compact_error(e)) if errors.bump(e): return exit_message(log, "RepeatedError") except FrameworkError: # your fault: don't blame the model raise finally: log.save() # every step, failures included
if ctx[-1].role == "exit": break # component 1: exit is a messageTrade-off notes:
- Persistence and stop conditions before compression. They’re the safety net. mini-swe-agent’s 191 lines contain zero context compression — short tasks don’t need it; don’t optimize early.
- Set all three budget dimensions. Steps alone won’t stop a hang on a slow tool; cost alone won’t stop a free-tier infinite loop.
- Leave a reason on every continue. Claude Code tags each continuation with transition.reason; mini-swe-agent stamps every exit with exit_status. Even one extra log field pays for itself.
Common traps
Section titled “Common traps”Treating the framework’s loop as a black box. Anthropic’s advice is to start with the raw API. mini-swe-agent proves a production loop fits in 191 lines — read one that size before deciding you need a framework.
Using chat history as recovery. Recovery needs an event log plus deterministic replay. Chat history lacks the side-effect record (was the file changed? did the command run?), so replaying it runs side effects twice.
Making exit an escaping exception. Exceptions thrown up the stack lose the exit reason. Copy mini-swe-agent: exit is a message with exit_status; exceptions are just transport; everything lands in the trajectory.
Retrying every error alike. Classify first: implementation errors raise, model errors feed back, consecutive same-class errors trip the breaker. Unclassified retry systems retry their most expensive bug until the budget dies.
Verifying only at the end. Verification sits inside the loop, firing every time the model claims completion. Finding out at the finish line that step 3 was wrong wastes every step in between.
What to carry forward and the next experiment
Section titled “What to carry forward and the next experiment”Compress loop engineering into five non-negotiable components: budgets are checked before model calls; stopping uses multiple signals; errors are classified before retry; every successful or failed step is persisted; completion claims pass through external facts.
Do not add more framework features next. Run the chapter skeleton on one verifiable task and trigger three failures in order: three same-class tool errors, false completion, and process death after a committed tool effect. Report only four metrics: correct exit reason, complete trajectory, duplicate side-effect count, and whether the verifier resumed from the correct checkpoint.
Add compaction, parallelism, and plugins only after those four are stable. Otherwise each feature expands an unexplained state surface.
Appendix: open when you want to review
Section titled “Appendix: open when you want to review”Six checks and three exercises
Check questions
Section titled “Check questions”Q1 · Basics: The agent loop is ten lines — why do production loops run to thousands? What’s the extra code?
The extra code is the safety ring around the loop, countable as five components: stop conditions (multi-signal + completion checks), budgets (steps/dollars/wall-clock + grace call), error handling (taxonomy, compaction, breaker), trajectory persistence (every step on disk, replayable), and a verifier (external facts rule on completion). A demo needs the loop body; production needs all five.
Reference point: mini-swe-agent fits all five in 191 lines — “production-grade” means “components complete,” not “large.” Conversely, thousands of lines missing budgets or persistence is still a demo.
Source: research/mini-swe-agent/src/minisweagent/agents/default.py (commit a83fcae).
Follow-up: “Which component first?” Persistence and stop conditions. Without them you can’t even answer “why did the loop die.”
Q2 · Budget design: Which two incidents does a bare max_turns fail to prevent? How many dimensions does a complete budget have?
Two: slow-tool hangs (every step legal, one step stuck 10 minutes — the step count never runs out) and expensive-call burns (steps fine, one call swallows a 10MB file).
Three dimensions: steps (stops spinning), cost in dollars (stops the wallet), wall-clock (stops hangs). mini-swe-agent’s AgentConfig is exactly these three plus a consecutive-error cap, all checked before the model call — check first, then spend.
Plus one more piece on exhaustion: the grace call. Force one final summarizing call so the user gets “here’s what I did and what’s missing” instead of silence.
Source: mini-swe-agent query(); smolagents _handle_max_steps_reached.
Follow-up: “Does the grace call itself blow the budget?” It costs one extra call by design. Implementations disable tool dispatch on the grace turn — talk only, no work — capping the overhead at one text call.
Q3 · Error handling: Tools keep failing — when to retry, when to stop? Give an implementable criterion.
Classify first. Implementation errors (framework bugs, protocol errors) gain nothing from retries — raise immediately. Model errors (bad args, bad format) get compacted and fed back for the model to fix. In smolagents’ _run_stream these are two except branches going opposite directions.
The implementable criterion is a consecutive counter: N same-class failures in a row (3 is common) stops the run; any clean step resets to zero. The counter measures “stuck in one hole,” not “total errors” — 20 errors across a 200-step task can be normal; 3 identical ones in a row means the model can’t get out.
What goes back must be compacted to three things: what failed (one line), what was tried, what options remain. Paste the raw stack and the model retries verbatim.
Source: mini-swe-agent max_consecutive_format_errors; smolagents error branches; 12-Factor Factor 9.
Follow-up: “Reflexion vs plain error compaction?” Reflexion has the model write a verbal reflection per failure into a separate buffer injected on retries — retries carry lessons, not just facts. HumanEval went 80% → 91%.
Q4 · Recoverability: Why can’t “resend the chat history” serve as recovery? What’s the key to doing it right?
Chat history lacks the side-effect record. Was the file changed? Did the command run? Was the patch submitted? None of that is in the message list, so replaying executes side effects twice (double-submitting the same patch is the classic incident).
The right mechanism is an event log plus deterministic replay: every step appended as an event (tool calls and results included), recovery reads the log, rebuilds state, and skips already-executed steps. The principle is 12-Factor’s Factor 12: the agent is a stateless reducer; the state is the event sequence.
Two details worth copying: mini-swe-agent saves in a finally (the failed step also has a complete scene on disk), and exit is a message carrying exit_status rather than an escaping exception (the exit reason lands in the trajectory automatically).
Source: mini-swe-agent run()’s finally: self.save(...); Codex rollout JSONL.
Follow-up: “Version the trajectory file?” Yes — mini-swe-agent writes trajectory_format: "mini-swe-agent-1.1", so old trajectories still parse after a format change.
Q5 · Verifier: Why does “verifier hardness set the autonomy budget”? One example in each direction.
The verifier decides who rules on “done.” Hard verifiers are machine rulings — test exit codes, patch syntax checks, command allowlists — the model has no appeal and lies get caught instantly, so the loop can safely run dozens of autonomous steps. Codex chains four hard verifiers in coding scenarios on exactly this logic.
With only soft verification (another model as reviewer, or a human), “faked completion” can slip through. Those scenarios should shorten the loop, lower max_turns, and bring humans in early — giving a research-report agent a 90-step budget is gambling.
The reverse inference also works: to grant more autonomy, first ask whether you can build a harder verification signal (a fact-check script for writing tasks, schema validation for data tasks) rather than just raising the budget.
Source: Codex apply_patch.rs and exec-policy modules; smolagents final_answer_checks.
Follow-up: “When should the verifier run?” On every completion claim, not once at the end. Finding out at the finish line that step 3 was wrong wastes everything in between.
Q6 · Open-ended: Half a day to upgrade a bare while-loop agent — in what order do you add components, and why?
Order: persistence → stop conditions → budgets → error taxonomy → verifier.
Persistence first (1h): every step saved in a finally as JSONL; exit becomes a message with exit_status. This is the precondition for debugging everything else — without a trajectory, every later component is blind repair.
Stop conditions next (1h): register an explicit final_answer action, add a hard max_turns. Now the loop at least can’t run away.
Then budgets (30min): add cost and wall-clock dimensions plus the grace call. Then error taxonomy (1h): two except branches plus the consecutive counter.
The verifier goes last not because it matters least but because it’s the most domain-dependent (are there tests to run? a schema to validate?). The first four are pure engineering — copy mini-swe-agent block by block; the verifier requires thought.
Source: the whole order maps onto mini-swe-agent’s 191 lines. Follow-up: “When does context compression get added?” When long tasks appear. mini-swe-agent contains zero compression — adding it for short tasks is premature optimization plus a new class of information-loss bugs.
Exercises
Section titled “Exercises”- Read source: open
research/mini-swe-agent/src/minisweagent/agents/default.pyand mark the lines for each of the five components. Which component uses the fewest lines? (Hint: one is a singlefinally.) - Modify code: add a fourth budget dimension to this chapter’s skeleton — a per-step token cap (truncate and flag any tool result over N tokens). Decide: does the truncation notice go into the trajectory?
- Design: your agent is a long-running “compile the team’s weekly report” task with no tests to run. Design its stop conditions and verifier: at least two stop signals, one soft-verification scheme, and whether max_turns should be large or small.
Research notes
Section titled “Research notes”This chapter is grounded in first-hand material (repos cloned into this workspace’s research/ directory; line numbers refer to the listed commits):
| Source | Version | What was read |
|---|---|---|
| SWE-agent/mini-swe-agent | a83fcae | src/minisweagent/agents/default.py, all 191 lines |
| huggingface/smolagents | e3a5b89 | agents.py: _run_stream, _handle_max_steps_reached, final_answer_checks, planning_interval |
| openai/codex | fa1d4c4 | loop and rollout modules under codex-rs/core/ |
| ReAct | arXiv:2210.03629 | the thought-action-observation structure |
| Reflexion | arXiv:2303.11366 | verbal-feedback retries, episodic reflection buffer |
| Building Effective Agents | Anthropic | augmented LLM, evaluator-optimizer |
| 12-Factor Agents | HumanLayer | Factors 6 / 8 / 9 / 12 |
For the four harnesses (Codex, Claude Code, OpenClaw, Hermes) compared loop-by-loop, see chapter 02 — that’s the line-by-line implementation detail; this chapter is the method.
Loop engineering governs the inside of one agent. When the job outgrows one loop — split across agents, parts frozen into code, humans approving — you’re in Graph Engineering territory.