Written by Jakub Rusinowski · Last updated 2026-08-04 · Hardware figures computed by our VRAM engine
Loop engineering is designing the act → observe → verify → retry cycle around a model instead of prompting it one step at a time. It matters more locally than it does on a frontier API: a deterministic verifier (test suite, type checker, build) supplies the judgement a 27B model lacks, and an iteration cap plus a clean context reset stops it spiralling. The rule that decides everything: no objective check, no loop.
Three practices have stacked up in three years. Prompt engineering was about the wording of one request. Context engineering was about what the model can see when it answers. Loop engineering — the term went mainstream in mid-2026 — is about what happens *after* the answer: the model acts, something objective observes the result, and the system decides whether to accept it, retry it, or stop. It's the layer underneath every serious coding agent shipped this year, and it's the layer where local models gain the most ground.
The reason is unglamorous. A frontier model can often carry a vague instruction to a decent result on the first try, so a sloppy loop is survivable. A 22–32B model running on your own GPU frequently cannot — but it *is* very good at making a failing test pass when you hand it the failure output. Loop engineering converts the model's weakness (judgement, long-horizon planning) into something the loop supplies for free, and leans on its strength (fast, local, unmetered iterations). A well-designed loop on a 24 GB card routinely beats a poorly-designed loop on an API model that costs money per attempt.
Every coding agent — Aider, Cline, OpenCode, Claude Code, Codex — is the same five-step cycle underneath. The differences are in what each step is allowed to do.
Step 4 is what separates loop engineering from "running the agent again." A verifier has one job: return pass or fail identically every time, with no model in the decision. pytest -q, tsc --noEmit, cargo test, go build ./..., ruff check, npm run lint — all valid. "Ask a second model whether the code looks right" is not, at least not as the gate; LLM-as-judge belongs *outside* the objective check, never in place of it.
This is why the first question of any local loop project is not "which model?" but "what is my pass/fail command, and how long does it take to run?" A repo without a fast, trustworthy test command cannot be looped on. Fixing that comes first.
Loops nest. Each level wraps the one beneath it and adds a different kind of correction. Knowing which level you are operating at tells you what your next improvement should be.
| Level | What it is | Who decides "done" | Local-hardware reality |
|---|---|---|---|
| 1 · Tool loop | Model calls tools repeatedly until it declares completion (the classic ReAct cycle) | The model | Works on 22B+ with solid tool calling. Fails silently when it "declares" success it didn't achieve |
| 2 · Goal loop | You define a measurable success condition; the loop keeps going until it's met | A deterministic check | The sweet spot for local models. The check is the intelligence the model doesn't have |
| 3 · Verification loop | Multiple gates stacked — tests, then types, then lint, then a review pass | A chain of checks | Cheap locally: gates are CPU work, not tokens. Each gate you add reduces the model tier you need |
| 4 · Meta loop | The loop improves its own inputs — failures feed back into the rules file, prompts, or task decomposition | You, periodically | Where local setups compound. Every fix you write into AGENTS.md is permanent and free |
Most people who say "the agent doesn't work" are stuck at level 1 with a model that isn't strong enough to self-assess. Moving to level 2 — same model, same hardware, one added pytest gate — is usually the largest single quality jump available, and it costs nothing.
The concept is provider-neutral; the engineering constraints are not. Five differences drive every design decision on your own hardware.
Before adopting a framework, build the twenty-line version — it makes the moving parts obvious and it is genuinely useful. This drives any OpenAI-compatible local endpoint (Ollama on :11434, LM Studio on :1234, llama-server, vLLM) and gates on your real test command.
# loop.py — goal loop (level 2) against a local endpoint
import subprocess, json, urllib.request
ENDPOINT = "http://localhost:11434/v1/chat/completions"
MODEL = "qwen3-coder:8b" # whatever your VRAM supports
VERIFY = ["pytest", "-q"] # THE gate. Must be deterministic.
MAX_ITERS = 6 # always cap. always.
def verify():
p = subprocess.run(VERIFY, capture_output=True, text=True)
return p.returncode == 0, (p.stdout + p.stderr)[-4000:] # tail only
def ask(messages):
body = json.dumps({"model": MODEL, "messages": messages,
"temperature": 0.1, "stream": False}).encode()
req = urllib.request.Request(ENDPOINT, body, {"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return json.load(r)["choices"][0]["message"]["content"]
goal = "Make the failing tests in tests/ pass. Change only src/."
for i in range(1, MAX_ITERS + 1):
ok, output = verify()
if ok:
print(f"PASS after {i-1} iteration(s)"); break
# Fresh context every attempt: goal + current failure only.
reply = ask([
{"role": "system", "content": "You are editing a repo. Reply ONLY with a unified diff."},
{"role": "user", "content": f"{goal}\n\nTest output:\n{output}"},
])
subprocess.run(["git", "apply", "-"], input=reply, text=True)
print(f"--- iteration {i} applied ---")
else:
print("Cap reached without a green suite — reverting.")
subprocess.run(["git", "checkout", "--", "."])
Four design decisions in that file are the whole lesson: the verifier is a subprocess, not a prompt; the failure output is truncated to its tail so context doesn't explode; context is rebuilt each iteration instead of accumulated; and the loop reverts on cap rather than leaving half-finished edits behind. Run it inside a git worktree or a container so a bad diff costs you nothing — see sandboxing a local coding agent.
A loop inherits the quality of its check exactly. Grade yours against these, in order of value per minute spent:
1. Speed before coverage. A 4-second subset that runs every iteration is worth more than a 6-minute full suite that runs once. Point the loop at the fast subset; run the full suite once at the end. On local hardware the test run often costs less wall-clock than one model turn — that asymmetry is the reason to gate aggressively. 2. Fail loudly and specifically. The failure text *is* the next prompt. Assertion messages that say what was expected versus received teach the model in one turn; AssertionError alone burns three. 3. Stack cheap gates first. Type check (tsc --noEmit, mypy) and lint catch a large share of local-model errors — wrong import paths, invented method names, argument-count slips — in under a second, before a single test runs. 4. Close the reward-hacking holes. A model told "make tests pass" will, given the chance, edit the test, delete the assertion, or add @pytest.mark.skip. Constrain the writable path (Change only src/), and add a gate that fails if the test files changed: git diff --exit-code -- tests/. This is not paranoia; it is the single most common way loop output looks green and is worthless. 5. Know what the check does not cover. Green tests mean "no known regression," not "correct." Every loop needs a human diff review at the end. The loop's job is to hand you a small, plausible, already-passing diff — not to merge it.
| Symptom | What's actually happening | Fix |
|---|---|---|
| Same edit reattempted forever | Model can't see it already tried this | Include the previous attempt's diff — or its *rejection reason* — in the next prompt, not the whole transcript |
| Quality collapses around iteration 5 | Context rot: window filled with stale failures | Rebuild context each attempt; keep goal + current diff + latest failure only |
| Tests pass, code is nonsense | Reward hacking on the gate | Freeze the test files, add a second gate (types, build, a golden-output check) |
| Loop never terminates | No cap, or a flaky test that fails randomly | Hard cap iterations; quarantine flaky tests before looping |
| Agent stops mid-task claiming success | Level-1 loop with a model that self-assesses badly | Move to level 2 — the exit condition must be the verifier, never the model's opinion |
| Broken tool call, loop dies on parse | Local model drifted off the schema | Grammar-constrained decoding, plus one parse-repair retry that never counts against the cap |
| Machine unusable while it runs | Model + KV cache saturating the GPU | Smaller quant or shorter context; or run the loop on a rented GPU overnight |
Two habits prevent most of these: cap everything (iterations, per-iteration wall clock, tokens per turn), and log every iteration to disk — prompt, tool call, verifier output. The log is how you tell "the model is too small" apart from "my prompt was ambiguous," and they demand opposite responses.
A good loop lowers the bar, but it doesn't erase it. The floor is set by two abilities: emitting a valid tool call or diff, and correctly interpreting a stack trace.
Per-tier reasoning and exact VRAM figures live in best local coding models; the honest capability ladder for autonomy is in VRAM for coding models.
Not every task deserves a loop. The ones that do share a shape: a machine-checkable definition of done, and enough repetition to justify writing the check.
tsc --noEmit and let it grind the error count down overnight on hardware you already own.Tasks that resist loops: anything whose success is a matter of taste (API design, naming), anything where the check costs more to write than doing the work by hand, and anything touching a system you cannot safely re-run. For those, stay in the interactive assistant mode covered in the tool matrix.
Loops are wall-clock-bound: the same 12-iteration job that takes an evening on an 8 GB card finishes over lunch on a rented 48 GB GPU. Renting by the hour is also the cheapest way to find out whether a bigger model actually fixes your loop, or whether your verifier was the problem.
Full list on the cloud AI directory.