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.
Prompt caching and what an AI agent actually costs
Your agent resends the entire conversation on every single turn, so cost grows quadratically with steps. Prompt caching fixes most of it — but only if your prefix is stable. Here are the four rules, the mistake that silently triples your bill, and why caching and context trimming pull against each other.
Claude Code vs. Grok CLI: how two production coding agents talk to their models
A source-level comparison of two shipping AI coding agents — which API shape each uses, why Grok speaks OpenAI's wire format on its own servers, and the multi-backend abstraction that lets one client target three APIs.
Chat Completions vs. the Responses API: a practical migration guide
The mental-model shift from messages to items, a field-by-field migration map, tool calling with call_id, conversation state, and how the same code runs through the OpenAI SDK, a gateway, and LangChain.