An agent is a while loop with taste
Strip the frameworks away and a production agent is about eighty lines. Ours have outlived four model generations without structural change.
A meaningful share of our daily work is executed by AI agents: research, code changes, QA passes, report writing. People assume this requires an orchestration framework. It requires a loop:
let messages = [system, task];
for (let step = 0; step < MAX_STEPS; step++) {
const res = await llm(messages, TOOLS);
if (res.type === 'text') return res.text; // the model decided it is done
const tool = TOOLS[res.name]; // no dynamic anything
const result = await tool.run(res.args); // sandboxed, permission-gated
messages.push(res, clip(result, TOOL_BUDGET)); // clip. always.
}
return escalate(messages); // never loop forever
The model decides what to do next. The code owns whether and how long. That division is the entire design.
What the loop taught us
- Clip every tool result. Ours are capped around 2,000 tokens; the full output goes to disk and the model gets the head, the tail and the path. Before the cap, a single verbose build log could eat an entire context window and lobotomize the rest of the session.
- Few orthogonal tools beat many clever ones. Our agents carry six to nine tools with strict schemas. Every time we added a convenience tool that overlapped an existing one, tool-choice accuracy dropped. The model dithers exactly where a human would.
- MAX_STEPS is a safety property, not a tuning knob. When the budget runs out, the transcript escalates to a human with state intact. An agent that can loop forever eventually will, at 4 AM, against a rate-limited API.
- Transcripts are JSONL, and JSONL is replayable. Every step is appended to a log we can re-run against a new model or a fixed tool. Half of our debugging is replaying yesterday's transcript against today's code.
- Reads are free, writes go through gates. The loop can read anything in its sandbox. Deploys, deletions, payments and outbound messages leave the loop and enter the same mechanical gates our cron jobs use. We do not ask the model to remember to be careful.
Four model swaps later, the loop is byte-for-byte the same. Everything around it improved. That is what a good abstraction boundary feels like: the thing that changes fast is quarantined from the thing that must not.