
Most people still use AI like a 2015 search box. You type, you read, you type again. A newer pattern replaces that manual back-and-forth with a loop. This guide explains loop engineering using two verified artifacts. The sources are Andrej Karpathy’s autoresearch repository and the Bilevel Autoresearch paper. The framing follows a write-up by @0xCodila.
What is Loop Engineering?
To start, compare two modes. A prompt is one instruction, after which you decide the next step. A loop, by contrast, is a goal the model pursues until it arrives. The model plans, acts, checks its own result, then repeats. You define the objective once, and the loop handles iteration. Crucially, a loop only earns its cost when the work is measurable.
The Three Parts That Make A Loop Work
So what separates a real loop from a chatbot on repeat? Every reliable loop has three components.
- A verifier grades each attempt. That check can be a passing test, a moving metric, or a build. Without a verifier, the agent simply agrees with itself on repeat.
- State records what was tried, what failed, and what remains. A small side file lets the next run resume instead of restarting.
- A stop condition prevents runaway cost. The loop halts when the goal is met, or after N attempts.
The Karpathy Loop: Inside ‘autoresearch’
These three parts are not theoretical. On March 7, 2026, Karpathy released autoresearch, an open-source repository under the MIT license. It ships three core files and about 630 lines of code. The project went viral within days and now sits near 90,000 GitHub stars. It was latter presented as the pattern “the Karpathy Loop.”
The design is deliberately small, yet strict. The agent edits only train.py, which holds the GPT model, optimizer (Muon and AdamW), and training loop. It cannot touch the evaluation utilities in prepare.py. That separation stops the agent from making the test easier instead of the model better. Meanwhile, a human writes program.md, the instructions the agent must respect.
Each cycle runs one experiment. The agent reads the code, proposes a change, trains five minutes, then keeps or rolls back. The scoring metric is val_bpb, validation bits per byte, where lower is better. That budget yields roughly 12 experiments per hour, so about 100 run overnight.
The reported outcomes are concrete. Karpathy pointed it at his already-optimized nanochat GPT-2 training code. It ran for two days and completed about 700 experiments, keeping 20 genuine improvements. Stacked together, those fixes cut GPT-2-quality training time by 11%, from 2.02 to 1.80 hours. One finding was a QK-Norm implementation missing a scalar multiplier, which had left attention too diffuse across heads.
Notably, humans tire after roughly a dozen experiments, whereas the loop does not. Separately, Shopify CEO Tobi Lütke ran autoresearch overnight on an internal model. He reported a 19% improvement after 37 experiments. Karpathy’s takeaway: if you have an objective metric, you are the bottleneck.
Prompt vs Loop vs Bilevel Loop
The differences become clearer side by side.
| Aspect | One-shot prompt | Karpathy loop (autoresearch) | Bilevel Autoresearch |
|---|---|---|---|
| You define | Each step | The goal, once | The goal, once |
| Who iterates | You | Inner agent | Inner + outer agent |
| Verifier | You, manually | prepare.py (val_bpb) | Same metric, two levels |
| State | Chat only | Experiment log | Log plus injected code |
| Human role | Engine | Author of program.md | Author of program.md |
| Reported result | Varies | 700 runs → 20 fixes, 11% speedup | 5x larger val_bpb drop |
The Five Building Blocks
Consequently, AI engineering teams now assemble working loops from five reusable pieces:
- Automation fires the loop on a schedule, event, or trigger.
- A skill stores project knowledge in a markdown file, read on every run.
- Sub-agents split the writer from the reviewer, since one model grades itself too generously.
- Connectors let the loop act inside real tools, like an issue tracker or Slack.
- Finally, a verifier remains the gate that rejects bad work. Claude Code and Codex now ship all five.
Bilevel Autoresearch: A Loop On Top Of The Loop
Next, researchers asked a sharper question. If autoresearch is research, can you autoresearch autoresearch? The research paper Bilevel Autoresearch: Meta-Autoresearching Itself answers yes.
The inner loop matches Karpathy’s original: propose, train, evaluate, keep or discard. The outer loop watches the inner loop and reads its code and traces. It identifies where the search itself keeps stalling. Then it writes new Python mechanisms, injects them at runtime, and reruns the inner loop.
The result held on Karpathy’s GPT pretraining benchmark. The outer loop cut val_bpb 5x more than the single loop (-0.045 vs -0.009). Notably, both loops used the same LLM, so the gain came from architecture, not a smarter model. In practice the design splits into three levels. Level 1 runs the base loop. Level 1.5 tunes search parameters every five iterations. Level 2 generates mechanisms through a four-round session. The reported experiments used an RTX 5090 32GB and a 300-second budget.
The reason is worth noting. The inner loop kept returning to the same priors, even after they stopped working. The outer loop broke those patterns by forcing unfamiliar exploration.
Use Cases With Examples
These ideas transfer well beyond pretraining. For model work, a loop searches hyperparameters until val_bpb drops. For software, it refactors until tests, types, and the build pass. For content, it rewrites until every rubric score clears a threshold. For data, it tunes a pipeline until schema checks hold. Each case shares one trait: an automatic gate that can fail the work.
Try It Yourself: A Loop In One Prompt
Theory aside, you can feel the mechanic without Claude Code or Codex. Paste this into any capable model and watch it self-correct.
You will work in a loop until the task meets the bar.
TASK:
[describe exactly what you want produced]
SUCCESS CRITERIA (be strict):
- [criterion 1]
- [criterion 2]
- [criterion 3]
LOOP PROTOCOL, repeat every turn:
1. PLAN - state the single next step.
2. DO - produce or improve the work.
3. VERIFY - score the result 1-10 on each criterion. Be honest.
4. DECIDE - if every criterion is 8+, print FINAL and stop.
Otherwise print ITERATING and fix the weakest point first.
RULES:
- Never call it done until every criterion is 8 or higher.
- Each pass must fix the weakest score from the last VERIFY.
- Do not ask questions. Make a sensible assumption and continue.
Begin.Underneath, the control flow is small. The skeleton below shows those three parts in Python: a verifier, a decision, and two stop conditions.
current = baseline
best = evaluate(current) # verifier: lower val_bpb is better
for step in range(MAX_STEPS): # stop condition 1: experiment budget
candidate = propose_change(current) # agent edits train.py
score = train_and_eval(candidate) # train 5 min, then verify
if score < best: # keep only real improvements
current, best = candidate, score # commit
# else: discard candidate, restore baseline
if best <= TARGET: # stop condition 2: goal met
breakBoth versions are limited. You are still the trigger, and closing the tab erases the state. Adding automation, a state file, and a real gate turns this into an autonomous loop.
See It Run
The interactive demo below animates one full loop: propose, train, verify, then keep or roll back. Adjust the target and step limit, and watch val_bpb fall until the stop condition fires.
// ———- ui ———-
function paintStats(){
$(‘s-exp’).innerHTML=state.exp+’/’+state.maxExp+’‘;
$(‘s-best’).textContent=state.best.toFixed(4);
$(‘s-kept’).textContent=state.kept;
$(‘s-rb’).textContent=state.rb;
}
function clearStages(){stages.forEach(s=>s.className=”stage”);}
function lit(i){clearStages(); stages[i].classList.add(‘on’);}
function log(html,cls){
const d=document.createElement(‘div’); d.className=”ln”; d.innerHTML=html;
const L=$(‘log’); L.appendChild(d); L.scrollTop=L.scrollHeight;
}
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
// ———- the loop ———-
async function runLoop(){
if(running) return; running=true;
$(‘btn-run’).disabled=true; $(‘log’).innerHTML=”;
log(‘$ agent: read program.md → start experiments‘);
// reset run state, keep controls
state.best=1.20; state.exp=0; state.kept=0; state.rb=0; state.hist=[1.20]; state.mechs=0;
paintStats(); drawChart();
let improveScale=0.020; // typical improvement magnitude when a change helps
let stuck=0; // consecutive no-gain runs (inner loop plateau)
while(state.exp
state.exp++;
// 01 propose
lit(0); $(‘status’).textContent=”Experiment “+state.exp+’ · proposing a change to train.py…’;
await sleep(220);
// outer (bilevel) loop: every few stuck runs, inject a new search mechanism
let injected=false;
if(state.bilevel && stuck>=3){
state.mechs++; stuck=0; improveScale*=1.6; // illustrative: breaks the plateau
injected=true;
log(‘↻ outer loop: search stalled → injected mechanism #’+state.mechs+’ (explore new directions)‘);
await sleep(340);
}
// 02 train (animate 5-min budget)
lit(1); $(‘status’).textContent=”Experiment “+state.exp+’ · training (5-min budget)…’;
const tb=$(‘tbar’); tb.style.width=”0%”;
for(let p=0;p<=100;p+=8){ tb.style.width=p+’%’; await sleep(16); }
// 03 verify — produce a candidate val_bpb
lit(2); $(‘status’).textContent=”Experiment “+state.exp+’ · verifying val_bpb…’;
await sleep(200);
const helps=Math.random() < (state.bilevel?0.55:0.42); // fraction of changes that help
let cand;
if(helps){
const gain=improveScale*(0.4+Math.random());
cand=Math.max(state.target-0.02, state.best-gain);
}else{
cand=state.best + Math.random()*0.03; // regression
}
// 04 decide
if(cand < state.best-1e-6){
state.best=cand; state.kept++; stuck=0;
stages[3].className=”stage pass”; stages[2].className=”stage pass”;
log(‘exp ‘+String(state.exp).padStart(2,’0′)+’ · val_bpb ‘+cand.toFixed(4)+
‘ ✓ KEEP‘+(injected?’ (post-inject)‘:”));
}else{
state.rb++; stuck++;
stages[3].className=”stage fail”; stages[2].className=”stage fail”;
log(‘exp ‘+String(state.exp).padStart(2,’0′)+’ · val_bpb ‘+cand.toFixed(4)+
‘ ↻ ROLL BACK‘);
}
tb.style.width=”0%”;
state.hist.push(state.best);
paintStats(); drawChart();
await sleep(260);
}
clearStages();
const hit=state.best<=state.target;
$(‘status’).textContent = hit
? ‘Stop condition: target reached at experiment ‘+state.exp+’.’
: ‘Stop condition: experiment budget (‘+state.maxExp+’) reached.’;
log(‘— loop halted — best val_bpb ‘+state.best.toFixed(4)+
‘ · kept ‘+state.kept+’ · rolled back ‘+state.rb+
(state.bilevel?(‘ · mechanisms injected ‘+state.mechs):”));
running=false; $(‘btn-run’).disabled=false;
postHeight();
}
function resetAll(){
if(running) return;
state.best=1.20; state.exp=0; state.kept=0; state.rb=0; state.hist=[1.20]; state.mechs=0;
clearStages(); $(‘tbar’).style.width=”0%”; $(‘log’).innerHTML=”;
$(‘status’).textContent=”Ready. Press “Run loop” to start.”;
paintStats(); drawChart();
}
// ———- wire controls ———-
$(‘i-target’).addEventListener(‘input’,e=>{state.target=+e.target.value; $(‘v-target’).textContent=state.target.toFixed(3); if(!running){paintStats();drawChart();}});
$(‘i-steps’).addEventListener(‘input’,e=>{state.maxExp=+e.target.value; $(‘v-steps’).textContent=state.maxExp; if(!running){paintStats();drawChart();}});
$(‘i-bilevel’).addEventListener(‘change’,e=>{state.bilevel=e.target.checked; $(‘tg’).classList.toggle(‘amberon’,e.target.checked);
$(‘c-tag’).textContent=e.target.checked?’lower is better · bilevel on’:’lower is better’;});
$(‘btn-run’).addEventListener(‘click’,runLoop);
$(‘btn-reset’).addEventListener(‘click’,resetAll);
// ———- resize + auto-height for WordPress ———-
function postHeight(){
const h=document.documentElement.offsetHeight+40;
if(window.parent) window.parent.postMessage({loopDemoHeight:h},’*’);
}
function onResize(){fitCanvas(); drawChart(); postHeight();}
window.addEventListener(‘resize’,onResize);
// ———- init: real values on load ———-
fitCanvas(); paintStats(); drawChart(); postHeight();
setTimeout(postHeight,300);
})();






