What Is the Agent Loop? A Visual Guide
The agent loop explained without the jargon: a labeled skeleton you can run today, plus the one detail every tutorial skips. See exactly what turns a model call into an agent.
def agent_loop(goal, tools, model, max_steps=8):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": goal},
]
for step in range(max_steps):
reply = model.call(messages, tools=tools)
messages.append(reply.as_message())
if reply.tool_calls:
for call in reply.tool_calls:
result = tools[call.name](**call.args)
messages.append(tool_result(call.id, result))
else:
return reply.text # model answered in prose = done
return "Stopped: hit step limit before finishing."Most first-time agent builders wire up a single tool call, watch the model use it once, and call it an agent. It isn't. Roughly nine out of ten "my first agent" repos I've reviewed contain no loop at all — just one request, one tool, one response. The thing that actually makes an agent an agent is the loop wrapped around the model, and it's the part tutorials sprint past. So here's the agent loop explained the way I wish someone had drawn it for me: every moving part labeled, plus a skeleton you can paste and run in the next five minutes.
An agent loop is a
whileQuick-Start (Copy This Right Now)
[object Object], ,[object Object],(,[object Object],):
messages = [
{,[object Object],: ,[object Object],, ,[object Object],: SYSTEM_PROMPT},
{,[object Object],: ,[object Object],, ,[object Object],: goal},
]
,[object Object], step ,[object Object], ,[object Object],(max_steps):
reply = model.call(messages, tools=tools)
messages.append(reply.as_message())
,[object Object], reply.tool_calls:
,[object Object], call ,[object Object], reply.tool_calls:
result = tools[call.name](**call.args)
messages.append(tool_result(call.,[object Object],, result))
,[object Object],:
,[object Object], reply.text ,[object Object],
,[object Object], ,[object Object],What this does: runs the model, executes whatever tool it asks for, appends the result to the running transcript, and repeats — until the model replies in plain text or the step counter runs out.
That's a complete agent in fourteen lines. Everything else — planning, memory, reflection — is a variation on this shape.
Understanding the Variables
Four things flow through every turn of the loop, and naming them makes debugging far easier.
State is
messagestool_resultfinishThe observation is the piece people forget. If you run a tool and don't feed its output back into
messages⚡ Pro tip: Log the four variables on every iteration — step number, chosen tool, args, and observation length. When an agent misbehaves, that four-column trace tells you within seconds whether the model chose badly or the tool returned junk.
Step-by-Step: The Agent Loop Explained
Walk one full turn, slowly.
First, the model reads the entire
messages[object Object],
reply = model.call(messages, tools=tools) ,[object Object],
,[object Object], reply.tool_calls:
call = reply.tool_calls[,[object Object],]
obs = tools[call.name](**call.args) ,[object Object],
messages.append(tool_result(call.,[object Object],, obs)) ,[object Object],
,[object Object],What this does: shows the decide → act → observe cycle for a single iteration, making clear that execution happens in your runtime while the model only chooses.
Here's the counterintuitive part. The model doesn't "remember" anything between turns. Each call is stateless. The only reason the agent seems to remember is that you keep handing it the full transcript. Memory in an agent loop is an illusion produced by re-sending history. Once that clicks, context-window problems and state bugs stop being mysterious.
⚡ Pro tip: Give the model an explicit
finish(answer)Pro-Level Variations
Once the base loop runs, three upgrades cover most real needs.
A support engineer building a ticket-triage agent adds a routing tool and a
finishA QA engineer testing a flaky checkout flow wires an agent that retries a failing step up to three times before escalating — the same loop, with a retry counter folded into the observation. Same skeleton, four behaviors. That's the point of understanding the shape: you stop copying whole frameworks and start adjusting a handful of knobs.
DESTRUCTIVE = {,[object Object],, ,[object Object],, ,[object Object],}
,[object Object], call.name ,[object Object], DESTRUCTIVE ,[object Object], ,[object Object], human_approved(call):
obs = ,[object Object],
,[object Object],:
obs = tools[call.name](**call.args)What this does: intercepts high-risk tool calls and returns a blocked observation instead of executing, so the model has to route around the action or wait — without you rewriting the loop.
⚡ Pro tip: Put the step number in the system prompt on each turn ("This is step 3 of 8"). Models pace themselves better when they can see the budget shrinking, and they're far likelier to call
finishTroubleshooting Common Issues
Three failures cover most of what goes wrong early.
The agent repeats the same tool call forever? Its last observation didn't change its plan — usually because the observation was empty or unparseable. Print the observation; you'll find the tool returned
NoneThe agent quits too early? Your stop condition is too loose. A model that replies "Let me check that for you" in prose looks done to a naive
if not reply.tool_callsThe agent runs out of context around step ten? You're re-sending a transcript that grows every turn. That's expected — it's why compaction and scratchpads exist — but it means the loop needs a summarization step, not a bigger model.
And the agent calls a tool that doesn't exist? The model invented a plausible-sounding name. Return a clear observation —
No such tool: fetch_orders. Available: track, eta⚠️ Common mistake: Treating
max_steps⚡ Pro tip: When an agent stalls, replay its exact transcript through the model once, by hand, and read what it "sees." Nine times out of ten the bug is obvious the moment you look at the same context the model looked at — a truncated observation, a tool error dressed as data, a contradictory instruction.
Your Turn
Paste the quick-start loop, give it two toy tools — say a calculator and a fake web-search that returns canned text — and a goal that needs both. Watch the trace. You'll see the model call one tool, read the observation, then call the other. That handoff, decision to observation to next decision, is the agent loop explained in a single run you can watch live.
⚡ Pro tip: Keep your first agent's tools fake and deterministic. Real APIs add latency and flakiness that mask loop bugs. Once the loop is provably correct against canned tools, swap in the real ones — now any new failure is the tool's fault, not the loop's, and you know exactly where to look.
The moment that click happens — that the loop, not the model, is what you're really building — the whole field gets simpler. Frameworks stop looking like magic and start looking like opinionated wrappers around these same four variables. You'll read one, recognize the skeleton underneath, and know exactly which knob it's turning.
Once your loop works, you'll build a dozen variations of it, and you'll want the good versions of each prompt — the system prompt, the finish-tool description, the reflection step — somewhere you can reuse them instead of rewriting from memory. That's exactly what I keep in PromptABCD: a saved, versioned set of the loop prompts that actually shipped, so the next agent starts from a known-good baseline instead of a blank file.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
