What is an agent loop, really? (It's about 40 lines of code)
Agent frameworks make the loop look complicated. It isn't. Here is the whole thing in Python and TypeScript — the exchange, the stop rule, the step limit, and why writing it yourself changes how you debug everything else.
There’s a lot of mystique around “agents.” Frameworks sell orchestration graphs, state machines, and abstractions with names like AgentExecutor. It’s easy to conclude that an agent is a complicated thing.
It isn’t. An agent loop is roughly forty lines of code, and once you’ve written it yourself, every framework becomes readable.
The difference between a chatbot and an agent
A chatbot answers. An agent acts, checks the result, and acts again.
That’s the whole distinction. A chatbot is one request and one response. An agent is a loop that keeps going until the work is done — calling tools, reading what came back, and deciding what to do next.
The exchange
Everything is built on one exchange:
- You describe some tools to the model.
- The model responds with a structured request to use one — a name and arguments.
- You run the actual function.
- You feed the result back.
- The model either calls another tool or answers.
Step 5 is the loop. That’s it.
The loop in Python
MAX_STEPS = 10
def run_turn(context):
for _ in range(MAX_STEPS):
r = client.responses.create(model=MODEL, input=context, tools=TOOLS)
# The model's output IS the memory — feed it back in.
context += r.output
# Stop rule: no tool call means the agent is done.
if not any(i.type == "function_call" for i in r.output):
return r.output_text
for item in r.output:
if item.type != "function_call":
continue
args = json.loads(item.arguments)
result = TOOL_FNS[item.name](**args)
context.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": result,
})
return "Stopped: hit the step limit."
The same loop in TypeScript
const MAX_STEPS = 10;
export async function runTurn(context: OpenAI.Responses.ResponseInput): Promise<string> {
for (let step = 0; step < MAX_STEPS; step++) {
const r = await client.responses.create({ model: MODEL, input: context, tools: TOOLS });
context.push(...(r.output as OpenAI.Responses.ResponseInputItem[]));
if (!r.output.some((i) => i.type === "function_call")) return r.output_text;
for (const item of r.output) {
if (item.type !== "function_call") continue;
const args = JSON.parse(item.arguments);
const result = await TOOL_FNS[item.name](args);
context.push({ type: "function_call_output", call_id: item.call_id, output: result });
}
}
return "Stopped: hit the step limit.";
}
The four decisions that actually matter
The code is short. The engineering is in four choices it encodes.
1. The conversation list is the memory
There is no memory on the server. The model knows exactly what you put in input and nothing else. Appending the model’s own output back into that list is what makes the next turn coherent — and forgetting to is the single most common agent bug.
2. The stop rule
No tool call means the agent is done. That one line is what separates “an agent that finishes” from “an agent that loops forever asking itself questions.” It’s a rule you choose, not a property of the model.
3. The step limit
An agent without a step limit is an unbounded bill and a potential infinite loop. Ten steps is a reasonable default. What you do when you hit it — fail loudly, ask the user, summarize progress — is a product decision.
4. Errors are data, not exceptions
When a tool fails, don’t crash. Return the error text to the model as a normal tool result. A capable model reads <tool_error>No such file: confg.json</tool_error> and retries with config.json. If you throw instead, you’ve thrown away a free self-correction.
Why write it yourself
Frameworks hide exactly this loop, which means when your agent misbehaves you’re debugging someone else’s abstraction instead of your own control flow.
Once you’ve written the loop:
- Runaway loops are obvious — you know where the step counter lives.
- Context bloat is obvious — you know exactly what’s being appended each turn.
- Bad tool selection is obviously a description problem, not a mystery.
- Frameworks become readable. You can look at any agent library and map it back to these forty lines.
That last point is the real argument. This isn’t anti-framework — it’s that you can’t evaluate a framework you don’t understand the shape of.
What comes next
The loop is the foundation, but a loop alone isn’t a product. Around it you need tools worth calling, evals that prove it works, context management so long conversations stay affordable, and human approval before anything irreversible.
Each of those is a real subsystem. But they all hang off the forty lines above.
Building this loop by hand — plus tools, evals, context management and human approval — is Week 1 of the AI Engineering mentorship.
Build it, don't just read about it
The 12-week AI Engineering mentorship takes you from an empty file to a complete AI coding agent — every subsystem, real code, no frameworks.
Six tools and a lot of caution: reading Claude Code's tool layer
Your agent's six tools are 600 lines. Claude Code's are thousands — and the whole difference is edge cases somebody watched break. Fail-closed defaults, an A/B test they ran and reverted, curly quotes, why Bash(python:*) is an off switch, and the tool list that's sorted for the cache.
Sandboxing: where your agent's code is allowed to run
Letting the model write code is the highest-leverage tool you can give an agent — twelve tool calls collapse into one. It also turns text your agent read into a program running in your process. Thirty lines of node:vm, and the one idea that survives every upgrade to real isolation.
Reading the real Claude Code: six files that teach you the REPL
Claude Code's REPL is 5,005 lines. You should not read it. Here are six small files — streaming, slash commands, abort trees, cancellation, and the cost meter — that carry the actual lessons, with the code inline and every line linked.