Frontend Labs
⋯

Chapter 75 — Loops that run for hours: long-horizon agents

In one sentence

To make an agent work for hours or days, you stop trying to keep one conversation alive. Instead you run many short sessions in an outer loop, each starting with a fresh context and picking up from state stored in files, with the harness (not the model) checking whether each step really worked.

The problem

The length of tasks agents can complete has grown fast. METR's measurements (2025) found the length of task, in human working time, that frontier agents complete at 50% reliability had been doubling roughly every seven months for several years. Teams naturally started asking agents for whole features, migrations, even entire programs.

But any single conversation hits three walls:

  1. The context window fills. Even with compaction, after a few rounds of summarising-the-summary the agent loses track of details it needs.
  2. Errors compound. If each step is 98% reliable, 200 steps in a row succeed only 2% of the time (0.98^200 ≈ 0.018).
  3. The agent stops early or lies about being done. Long tasks give a model many chances to decide "that's probably good enough."

The breakthrough was not a bigger window. It was a change of shape.

The idea: the relay team with a shared notebook

Picture a relay team of engineers on short shifts. Nobody remembers the previous shift, but the team keeps:

  • a task board listing every feature and whether it's verified done
  • a logbook where each shift writes what it did and what it learned
  • the code itself, with a clean commit after every completed piece

Each new engineer reads the logbook and the board, picks one unfinished item, does it, proves it works, commits, writes three lines in the logbook, and leaves. Nobody needs a long memory. The notebook is the memory.

That's the core design of every successful long-running agent setup of 2025–26.

The four loops (they nest)

It helps to name the loops, because "agent loop" now means four different things:

   LOOP 4: SCHEDULE / EVENT     cron, webhook, queue message, "every night at 2am"
     |
     +-- LOOP 3: OUTER (relay)  fresh session per task; state in files + git
           |
           +-- LOOP 2: AGENT    think -> act -> observe (Chapter 51), inside
                 |              one context window, with a harness (Ch 74)
                 |
                 +-- LOOP 1: REASONING   the model's own internal "thinking"
                                         before each response

Loop 1 — reasoning inside the model (test-time compute)

Chapter 34 mentioned reasoning models trained with RL on verifiable rewards. What they do at inference time is itself a loop: before answering, the model generates a long hidden or summarised chain of thought: trying approaches, checking them, backtracking. Spending more tokens here (test-time compute) reliably improves results on maths, code, and planning. This is the other big scaling axis besides model size and training data.

For agent builders, two practical consequences:

  • Most APIs now expose a thinking budget or effort setting. More thinking means better decisions per step but slower, more expensive steps. For agents, a moderate budget on planning steps and a low one on routine tool calls is often the best trade.
  • Interleaved thinking (reasoning between tool calls) means the model can reflect on a tool result before choosing its next action. That's Loop 1 inside Loop 2.

Loop 2 — the agent loop

Chapters 51 and 74. One context window, many tool calls, a harness around it.

Loop 3 — the outer loop (the relay)

This is the new one. In its simplest, famous form it's a shell one-liner. In mid-2025 developer Geoffrey Huntley described what he named the Ralph loop (after Ralph Wiggum from The Simpsons, who is not clever but never stops trying):

while :; do cat PROMPT.md | claude -p ; done

That's it: the same prompt, fed to a headless coding agent, forever. Each iteration starts with an empty context. PROMPT.md tells the agent to read the spec and the plan, pick the most important unfinished item, do it, test it, commit, and update the plan. The codebase converges on the spec over many iterations, even though no single session understands the whole thing.

Why does something so dumb work?

  • Fresh context every time means no context rot and no compounding confusion. A bad session's mistakes don't poison the next one's reasoning; they're only visible as code and notes, which the next session can judge.
  • One task per session keeps each run short and inside the model's reliable range. You turn a 200-step task into 40 five-step tasks.
  • Files and git are the memory, and they're precise, inspectable, and version-controlled. You can read them over breakfast.

Ralph spread quickly (official plugins for coding agents, a Thoughtworks Technology Radar entry). Its weaknesses are just as instructive: with no verifier it can loop forever on an impossible item, "complete" things by weakening tests, or burn money overnight. The production version adds exactly the pieces from Chapter 74: verification by the harness, attempt limits, budgets, and a kill switch.

Anthropic's engineering team published a more structured version for building large apps over many sessions (Nov 2025):

  1. An initializer session runs once: it writes a detailed feature list as JSON (hundreds of end-to-end features, all marked "passes": false), an init.sh that boots the dev environment, a progress log file, and an initial git commit.
  2. Every later coding session follows a fixed routine: check the working directory, read the progress log and recent git history, run init.sh and a quick smoke test (to catch anything the previous shift broke), pick one failing feature, implement it, test it end to end (for web apps, driving a real browser), flip it to passing only when verified, commit, and update the log.

Two details made the difference in their reports. The feature list was JSON, which models are less tempted to casually rewrite than prose. And sessions were told explicitly that it was unacceptable to remove or edit tests to make them pass.

Scaled up, the same idea runs many agents in parallel. In early 2026 Anthropic described a team of 16 parallel agents that, over roughly 2,000 sessions and about $20,000 of API usage, wrote a C compiler in Rust capable of building the Linux kernel. The harness design was the bulk of the work: high-quality tests as the oracle, task locking so agents didn't collide, and logs written for agents rather than humans. Cursor reported similar lessons from long-running agents building large codebases: flat groups of equal agents coordinating through shared files got stuck, while a planner / worker hierarchy with clear ownership scaled much further.

Loop 4 — schedules and events

Finally, something has to start the outer loop: a cron schedule ("every night, upgrade one dependency"), a webhook ("new issue labelled agent"), a queue message, or an alert ("error rate up, investigate"). Chapter 77 covers running these in production; Chapter 79 builds one.

The verify loop: generator plus checker

Underneath all four loops is one pattern worth naming on its own:

   GENERATE  -->  VERIFY  --(fail: feed the evidence back)-->  GENERATE ...
                    |
                    +--(pass)--> commit / accept

The verifier can be:

Verifier Strength Example
Deterministic check Best: objective and cheap tests pass, it compiles, the JSON validates, the SQL runs
Execution in a real environment Strong drive the UI in a browser, run the migration on a copy of the DB
A second model (critic) Good for things code can't check "does this PR description match the diff?"
A human Strongest, but slow and costly approve the PR in the morning

Long-running agents work in exactly the domains where good verifiers exist, which is why coding went first: tests, compilers, and type-checkers are free, fast verifiers. If your domain has no verifier, building one is step one. An agent without a checker isn't long-running; it's long-wandering.

Build an outer loop (~100 lines)

This is a Ralph loop with the production pieces added: one task per fresh session, the harness runs the verifier, verified work is committed by the harness, broken attempts are thrown away, each task gets a limited number of attempts, and there are time/iteration budgets and a STOP file kill switch.

"""outer_loop.py -- run a headless agent over and over, one task per fresh context.

State lives in FILES, not in any context window:
  features.json   the task list: [{"id", "desc", "verify", "passes", "attempts"}]
  progress.md     an append-only log every iteration reads first
  git history     the record of what actually changed
The agent is told what to do; the HARNESS decides whether it was done.
Add features.json, progress.md and STOP to .gitignore: the harness's own
state must survive the `git reset` that throws away a failed attempt.
"""
import json
import pathlib
import subprocess
import sys
import time

AGENT_CMD = sys.argv[1:] or ["claude", "-p"]   # any headless agent CLI: claude -p, codex exec, ...
MAX_ITERATIONS = 40
MAX_HOURS = 8
MAX_ATTEMPTS_PER_TASK = 3
MAX_FAILS_IN_A_ROW = 5          # no progress for this long -> stop and page a human
AGENT_TIMEOUT_S = 30 * 60

FEATURES = pathlib.Path("features.json")
PROGRESS = pathlib.Path("progress.md")
STOP_FILE = pathlib.Path("STOP")   # `touch STOP` is the kill switch


def sh(cmd, timeout=None):
    p = subprocess.run(cmd, shell=isinstance(cmd, str), capture_output=True,
                       text=True, timeout=timeout)
    return p.returncode, (p.stdout + p.stderr)[-3000:]


def log(line):
    with PROGRESS.open("a") as f:
        f.write(f"- {time.strftime('%Y-%m-%d %H:%M')} {line}\n")


def next_task(features):
    for f in features:
        if not f["passes"] and f.get("attempts", 0) < MAX_ATTEMPTS_PER_TASK:
            return f
    return None


def prompt_for(task):
    return f"""You are one shift in a relay of engineers. You have no memory of earlier shifts.
1. Read progress.md and `git log --oneline -20` to see where things stand.
2. Work ONLY on this task: [{task['id']}] {task['desc']}
3. Verify it yourself with: {task['verify']}
4. Leave the repo clean: commit your work with a message starting "{task['id']}:".
5. Append 1-3 lines to progress.md, each starting with "[{task['id']}]":
   what you did, what you learned, what is left.
Do not edit features.json. Do not work on any other task."""


def main():
    started, fails_in_a_row = time.time(), 0
    for i in range(MAX_ITERATIONS):
        features = json.loads(FEATURES.read_text())
        task = next_task(features)
        if task is None:
            done = sum(f["passes"] for f in features)
            log(f"loop finished: {done}/{len(features)} tasks pass")
            return 0 if done == len(features) else 2
        if STOP_FILE.exists():
            log("STOP file found; halting"); return 3
        if time.time() - started > MAX_HOURS * 3600:
            log("time budget exhausted"); return 4

        start = sh("git rev-parse HEAD")[1].strip()
        try:                                       # 1. fresh context, one task
            sh(AGENT_CMD + [prompt_for(task)], timeout=AGENT_TIMEOUT_S)
        except subprocess.TimeoutExpired:
            log(f"[{task['id']}] agent timed out")

        code, output = sh(task["verify"], timeout=600)   # 2. the HARNESS verifies
        task["attempts"] = task.get("attempts", 0) + 1
        if code == 0:
            task["passes"], fails_in_a_row = True, 0
            # checkpoint verified work, so a later failed attempt can't take it away
            sh(f"git add -A && git commit -q -m '{task['id']}: verified by harness'")
            log(f"[{task['id']}] PASS on attempt {task['attempts']}")
        else:
            fails_in_a_row += 1
            # roll back everything this attempt did, committed or not
            # (ignored state files like progress.md are left alone)
            sh(f"git reset -q --hard {start} && git clean -fdq")
            last = output.strip().splitlines()[-1][:200] if output.strip() else "no output"
            log(f"[{task['id']}] FAIL attempt {task['attempts']}: {last}")
        FEATURES.write_text(json.dumps(features, indent=2))

        if fails_in_a_row >= MAX_FAILS_IN_A_ROW:
            log("no progress; stopping for a human"); return 5
    log("iteration budget exhausted"); return 6


if __name__ == "__main__":
    sys.exit(main())

A features.json for a real migration (moving a codebase from one HTTP library to another) might look like:

[
  {"id": "M1", "desc": "Replace requests in app/clients/billing.py with httpx; keep retries",
   "verify": "pytest tests/clients/test_billing.py -q", "passes": false},
  {"id": "M2", "desc": "Replace requests in app/clients/shipping.py with httpx",
   "verify": "pytest tests/clients/test_shipping.py -q", "passes": false},
  {"id": "M9", "desc": "Remove requests from pyproject.toml; whole suite green",
   "verify": "! grep -rq 'import requests' app/ && pytest -q", "passes": false}
]

Run it inside a sandbox (Chapter 77), never on your laptop's real home directory:

python outer_loop.py claude -p      # or: codex exec, or any headless agent CLI

Headless agents can't stop to ask for permission, so configure the agent's own permission allowlist (in its settings file) to let it edit files and run exactly the commands it needs (pytest, git add, git commit) and deny everything else, such as git push and network tools. The sandbox is the second fence in case that list is wrong.

Things this design gets right, each learned the hard way:

Line Failure it prevents
fresh AGENT_CMD per task context rot, compounding confusion
harness runs task["verify"] the agent claiming success it didn't achieve
harness commits on PASS a later failed attempt destroying earlier verified work
git reset --hard to the attempt's start on FAIL broken changes (even ones the agent committed) contaminating the next attempt
MAX_ATTEMPTS_PER_TASK looping forever on an impossible item
MAX_FAILS_IN_A_ROW burning a whole night when something global is broken (e.g. the dev DB is down)
STOP file, MAX_HOURS runaway cost; a human can halt it without killing processes
"Do not edit features.json" the agent marking its own homework

(When this loop was tested with fake agents while writing this chapter, it went through three bugs. The first version threw away the loop's own state files along with a failed attempt. The second let a later failed attempt wipe out an earlier passing one. The third used git stash to discard failures, which can't undo work the agent had already committed. All three are fixed in the code above. Test your harness with a fake agent, including one that misbehaves, before you point it at a real one.)

When not to build a long-running agent

  • No verifier. If you can't automatically check a step, you'll find out what went wrong in the morning, across 40 commits.
  • Irreversible actions. Outer loops are for work that can be reviewed and rolled back: code on a branch, drafts, reports. Not payments, not emails to customers, not production changes. Those need a human at the gate.
  • The task isn't decomposable. If you can't write the task board, the agent can't either. Try a single-session agent with a human in the loop.

Practice (90 min)

  1. Create a tiny repo with three functions that have failing tests. Write a features.json with one entry per function, plus one impossible task.
  2. First run the loop with a fake agent (a script that does nothing), then with one that randomly succeeds. Confirm the harness logic: verified tasks are committed, the impossible task stops after 3 attempts, touch STOP halts it.
  3. Now run it with a real headless agent, inside a container. Read progress.md and git log afterwards. Do the progress notes actually help the next session? Rewrite prompt_for() to improve them.
  4. Remove the "do not edit tests" rule and give it a hard task. Does it cheat? Add a verifier step that fails if any file under tests/ changed.

Common confusions

  • "Why not just use a model with a 10-million-token window?" Bigger windows help, but quality still degrades with length, and errors still compound across steps. Fresh contexts plus external state is more robust than one gigantic context, and it's cheaper.
  • "The outer loop is just retrying." Retrying repeats the same attempt. The outer loop makes progress: each session starts from a better codebase and better notes than the last.
  • "Reasoning models make outer loops unnecessary." They make each step smarter (Loop 1). They don't stop the context window filling up or let the agent verify itself. The loops are complementary.
  • "More parallel agents = faster." Only when the tasks are genuinely independent and there's coordination: locks, ownership, a planner. Otherwise agents duplicate work and overwrite each other.

Check yourself

  1. Name the four nested loops, inner to outer.
  2. Why does a Ralph loop start each iteration with an empty context?
  3. Why must the harness, not the agent, run the verifier and update the task list?
  4. Using 0.98^200, explain why a single 200-step session is fragile, and how the outer loop changes the maths.
  5. Name two situations where you should not use a long-running agent.

Further reading

  • Article: Geoffrey Huntley, "Ralph Wiggum as a 'software engineer'" (ghuntley.com, 2025). The original.
  • Article: "Effective harnesses for long-running agents" — Anthropic Engineering (Nov 2025). The initializer / feature-list / progress-file design.
  • Article: "Building a C compiler with a team of parallel Claudes" — Anthropic Engineering (2026). What breaks when you scale to 16 agents.
  • Research: METR, "Measuring AI Ability to Complete Long Tasks" (arXiv:2503.14499, 2025). The time-horizon doubling result.
  • Radar: Thoughtworks Technology Radar, "Ralph loop" entry. An industry view of where the technique fits and its risks.

Generated from real-life-ai-example-v1.md · 8 Oct 2026 16:36