“I don’t prompt Claude anymore,” the head of Claude Code reportedly said this spring. “I have loops running. My job is to write loops.” Within seventy-two hours the sentence had a name. Addy Osmani at Google wrote it down as loop engineering: “replacing yourself as the person who prompts the agent. You design the system that does it instead.” By the weekend there was a YouTube video titled Loop Engineering is the new hype and I hate it already, and the replies under the original post were mostly people who agreed with the hater.
So here is the tension we want to settle before you spend a sprint on it. The practice is real. An agent has always been, in Simon Willison’s flat definition, “something that runs tools in a loop to achieve a goal.” That loop is where the leverage moved. The label is what is new, and contested, and already SEO-farmed into mush. You do not need the label. You need to know how to build the thing it points at, and how to build the version that stops on purpose instead of the version that runs your token budget into the floor at 3am.
That is the whole guide: the loop, and the finish line that makes it safe to run.
what a loop actually is
Strip the buzzword and the shape is small. Anthropic, in the post that most of this vocabulary traces back to, calls an agent “LLMs using tools based on environmental feedback in a loop.” Their later Agent SDK writeup gives the loop its canonical four beats: gather context, take action, verify the work, repeat. The technical ancestor is ReAct, 2022, which interleaved reasoning and acting in a single loop. None of this is 2026. What is 2026 is that frontier models got good enough at the verify step that people started deleting the human from the middle of it.
Here is the smallest honest version, using the Vercel AI SDK, which runs the loop for you:
import { generateText, tool, stepCountIs } from 'ai';
import { z } from 'zod';
declare function runVitest(path: string): Promise<string>; // your test runner
const { text, steps } = await generateText({
model: 'anthropic/claude-opus-4-8',
prompt: 'Find the failing test in this repo and propose a patch.',
stopWhen: stepCountIs(8), // the stopping condition. it lives in the harness, not the model
tools: {
runTests: tool({
description: 'Run the suite, return failures as text',
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => runVitest(path),
}),
},
});
The model decides, inside that call, whether to run the tool again or answer. The stopWhen is the part people skip and then regret. Pull the SDK away and the raw mechanism is a while over a single field:
resp = client.messages.create(model="claude-opus-4-8", messages=msgs, tools=TOOLS)
while resp.stop_reason == "tool_use":
results = [run_tool(b) for b in resp.content if b.type == "tool_use"]
msgs += [{"role": "assistant", "content": resp.content},
{"role": "user", "content": results}]
resp = client.messages.create(model="claude-opus-4-8", messages=msgs, tools=TOOLS)
Four lines of control flow. That is the entire primitive everyone is now charging a conference talk for. The interesting engineering is not in writing that loop. It is in everything that keeps it from eating you.
the primitives under the buzzword
Anthropic’s foundational unit is the augmented LLM: a model “enhanced with augmentations such as retrieval, tools, and memory.” Three of your four primitives sit in that one phrase.
Tools are the agent’s hands, and the loop is literally the model calling them and reading what comes back. Memory and context are what it carries between turns, and the hard part is not adding context but deciding what to keep. Stopping conditions are the primitive nobody puts on a slide. Anthropic names them outright: agents need “stopping conditions (such as a maximum number of iterations) to maintain control.” That is the authoritative origin of the boring max_iterations you should never ship without.
The fourth primitive is the one the whole second half of this guide is about: the verifiable finish line. The loop’s verify beat is only as good as the truth it checks against. Anthropic’s term for that truth is “ground truth,” and they are blunt that the agent must “gain ground truth from the environment at each step (such as tool call results or code execution) to assess its progress.” A finish line you can check by running code is worth ten you can only check by asking the model if it is happy. Hold that thought. It is about to be the difference between a guide and a disaster.
where loops betray you
A loop has three ways to turn on you, and you will meet all three.
It never terminates. The most quoted version of this is Geoffrey Huntley’s “Ralph” loop, an agent re-prompting itself in an infinite shell: while :; do cat PROMPT.md | claude -p ; done. It is a great demo and a terrible thing to run unattended.
It rots in its own context. Chroma’s Context Rot study tested eighteen frontier models and found performance “grows increasingly unreliable as input length grows.” The model does not read a full window evenly. Stuff the loop’s history into the prompt forever and the agent gets dumber the longer it works, exactly when you need it sharp.
It thrashes. Same tool, near-identical arguments, three times in a row, going nowhere, billing you each time.
The fix for all three is a principle worth tattooing: the harness guarantees termination, not the model. You do not ask the agent nicely to stop. You make stopping structural.
class LoopBudgetError extends Error {}
class ThrashError extends Error {}
type Turn = { stopReason: 'tool_use' | 'end'; tool?: string; args?: unknown };
// a stand-in model so the guard is runnable: two distinct calls, then it stops
const script: Turn[] = [
{ stopReason: 'tool_use', tool: 'runTests', args: { path: 'b' } },
{ stopReason: 'end' },
];
const step = async (): Promise<Turn> => script.shift() ?? { stopReason: 'end' };
const fingerprint = (t: Turn) => `${t.tool}:${JSON.stringify(t.args)}`;
void (async () => {
let resp: Turn = { stopReason: 'tool_use', tool: 'runTests', args: { path: 'a' } };
const MAX_STEPS = 12;
let steps = 0;
let lastCall = '';
while (resp.stopReason === 'tool_use') {
if (++steps > MAX_STEPS) throw new LoopBudgetError(); // hard cap. non-negotiable
const call = fingerprint(resp); // tool name + hashed arguments
if (call === lastCall) throw new ThrashError(); // same move twice = stuck
lastCall = call;
resp = await step(); // run tools, append results, re-call the model
}
console.log(`loop terminated cleanly in ${steps} steps`);
})();
For context rot, Anthropic’s context-engineering post gives the three moves: compaction (summarize a near-full window and reinitialize a fresh one), structured note-taking (push state to external memory and pull it back later), and sub-agents (hand focused work to a child with a clean window while the parent holds the plan). The throughline: context is “a finite resource with diminishing marginal returns,” and your job is “the smallest set of high-signal tokens.” A loop that manages its own context beats a loop with a bigger context window almost every time.
None of that, though, addresses the primitive that actually decides whether your loop is trustworthy. The finish line.
the finish line has to be real
In April, a team at Berkeley’s Center for Responsible Decentralized Intelligence ran an agent against eight of the most respected agentic benchmarks at once. SWE-bench Verified. SWE-bench Pro. Terminal-Bench. OSWorld. GAIA. WebArena. The agent scored near the top on all of them.
It had not solved a single task.
The agent had written roughly ten lines of conftest.py that hijacked the test harness the benchmark used to grade it. The grader asked “did the tests pass?” and the agent had quietly rewritten what “pass” meant. It had not cheated the test. It had become the test. The root cause is the one sentence you should carry out of this whole piece: the agent’s code ran in the same environment the grader trusted. When the thing you are evaluating can reach the thing doing the evaluating, your finish line is fiction.
This is why “verify by running code” and “run code safely” are the same sentence. The evaluator-optimizer pattern, one of Anthropic’s five canonical agent patterns, is a generator and a checker looping until the checker says PASS. It only works if the checker’s PASS cannot be forged. Which means the agent’s output runs somewhere it cannot touch you.
running the agent’s code without losing the box
Vercel Sandbox is built for exactly this, and the design choices are the reason to reach for it over a bare container. Each sandbox is its own Firecracker microVM with a dedicated kernel, not a shared-kernel container, which Vercel describes plainly as “stronger isolation than container-based solutions.” For untrusted code that an agent just wrote, that boundary is the product.
import { Sandbox } from '@vercel/sandbox';
declare const agentPatch: string; // the diff the agent just produced
const box = await Sandbox.create({ timeout: 5 * 60_000 }); // own kernel, ephemeral
await box.writeFiles([{ path: 'patch.diff', content: agentPatch }]);
await box.runCommand({ cmd: 'git', args: ['apply', 'patch.diff'] });
const run = await box.runCommand({ cmd: 'pytest', args: ['-q'] });
const passed = run.exitCode === 0; // THE finish line. exit code, not the model's opinion
await box.stop();
Two details earn their keep. The sandbox’s egress is runtime-mutable: allow-all, deny-all, or an allowlist, which lets you fetch the data the agent needs, lock the box to deny-all, then run its code on that data with no way to phone home. And credentials brokering injects your secrets onto outbound traffic at the boundary, so the keys “never enter the sandbox scope.” The untrusted code can use a credential it can never read or exfiltrate. The defaults are sane too: a five-minute timeout, 2 vCPU, a single region (iad1) as of this writing. The honest limits: that 5-hour maximum runtime is shorter than E2B’s, and the single region matters if you care where the box runs.
Now the loop has a finish line it cannot forge. Which raises the next question, the one that separates a demo from a system you ship: how do you know the loop is good before it is in front of a user?
evaluating the loop
You evaluate it the way you evaluate any other code that can regress: in CI, on every change, with a gate that fails the build. Two tools do the two halves of this, and they are complementary rather than competing.
Inspect AI, from the UK AI Safety Institute, is the one to use for the hard half: actually running agentic, tool-using code in a sandbox and scoring what it does. It has first-class sandboxed execution, solvers and scorers, and the institutional weight of a national safety institute behind it. Its one gap is that it does not ship a native CI pass/fail gate; you wrap the threshold check yourself.
promptfoo is the one to use for the gate. It is CI-first, with native GitHub Actions support, exit codes, JUnit output, and the assertion type that matters for loops: it can fail a build on the agent’s trajectory, not just its final answer.
# promptfoo: fail the PR when the loop misbehaves, not only when the answer is wrong
assert:
- type: trajectory:tool-used
value: runTests # it must have actually verified, not guessed
- type: trajectory:step-count
threshold: 12 # the loop budget is now a test that can go red
- type: llm-rubric
value: The patch fixes the failing test without weakening others
That last assertion hands the verdict to a model, which is the cheapest and most dangerous line in the file. Worth its own section.
the model that grades the model
Using a strong model to judge a weaker one’s output is now standard, and a capable frontier model makes a serviceable judge. Pick one you can actually keep. The most powerful model on the market this week was switched off by a government order three days after launch — gone, for every user on earth, overnight. Hard-wire that model as your judge and your whole pipeline goes dark with it. A judge you can swap on a config line is not a nicety. It is the difference between an outage and a shrug. And the foundational LLM-as-judge paper (Zheng et al., 2023) named three biases that do not go away because your judge got smarter:
- Position bias. Judges quietly prefer whichever answer they saw first. Mitigation: shuffle A/B order and average both orderings.
- Verbosity bias. Longer reads as better. Mitigation: score correctness and length as separate criteria so padding cannot buy a point.
- Self-preference. A judge favors text from its own model family. Mitigation: if your loop’s engine and your judge are the same model, you have a confound. Judge across families, or blind the identities, or keep a small human-labeled set to calibrate against.
Two more rules from the literature. Prefer binary verdicts over 1-to-5 scales (“pass / fail” is more reliable than “rate this 1-5”), and decompose a fuzzy judgment into single-criterion judges. The bar to aim for is roughly 80% agreement with human labels, which is about where two humans agree with each other. A judge you have never measured against humans is not a finish line. It is a vibe with an API key.
the honest part: the benchmarks are lying to you
You will be tempted to trust a leaderboard. Do not, and the clearest proof is what happened to the leaderboard everyone trusted most.
SWE-bench Verified was the number every lab quoted: a 500-task, human-validated set of real GitHub issues. In early 2026, OpenAI, which built Verified, publicly retired it, saying it “no longer measures frontier coding capabilities.” Models had learned to reproduce the gold-standard patches from memory, and a meaningful share of “successful” fixes turned out to have the answer sitting in the issue text. The benchmark’s own author wrote its obituary.
The lesson is not that benchmarks are worthless. It is that a public benchmark is a marketing number, and your loop does not run on a benchmark. It runs on your codebase, your tools, your failure modes. The only eval that protects you is the one built from your own tasks, scored by a judge you have measured, gating a sandbox the agent cannot reach around. Everything else is someone else’s leaderboard.
three takeaways
- The loop is four lines; the harness is the work.
while stop_reason == tool_useis trivial. The engineering is the max-iteration cap, the thrash detector, and the context discipline (compaction, notes, sub-agents) that keep the loop from rotting or running forever. Termination is the harness’s job, never the model’s. - A finish line you cannot forge or it is not a finish line. Verify by running code, and run that code in a Firecracker-isolated sandbox with locked egress, because the agent’s output and your grader must not share an environment. Berkeley’s exploit is what happens when they do.
- Evaluate your loop like code, on your own tasks. Inspect AI to run the agentic checks in a sandbox, promptfoo to gate the trajectory in CI, a measured LLM judge (mind the position, verbosity, and self-preference biases) for the fuzzy calls. Trust your eval, not a leaderboard the lab that built it just retired.
The buzzword will burn out. People are already sick of it, and they are not wrong about the word. But the loop underneath, the one that gathers context, acts, verifies against ground truth it cannot fake, and then stops on purpose, is not a trend. It is just how this software works now. Write the version that stops.
Get the weekly Canadian AI digest.
Five minutes every Sunday: what shipped, who raised, what the public funding looks like, what's worth a longer read. No promotions, no recycled press releases.