The agent loop: the pattern under every AI agent
People talk about AI agents like they're exotic. In my experience, once you see the loop, the mystery goes away. Strip every framework, SDK, and abstraction away and what's underneath is a while loop that calls a model, checks whether it wants to use a tool, dispatches that tool, feeds the result back, and repeats until the model says it's done or you run out of budget.
That's it. That's an agent.
I built effective-agents-lab specifically to make this visible. The patterns/agent_loop module hand-rolls that loop against the raw Anthropic API — no Claude Agent SDK, no LangChain, no abstraction layer. The implementation is 81 lines of actual logic. Reading it is the fastest way to understand what every SDK in this space is doing under the hood, and to stop feeling like framework magic is required to build something real.
The loop in plain terms
The loop does three things on each iteration. It calls the model with the current message history and a list of available tools. It reads stop_reason on the response. If stop_reason is not tool_use, the model has given a final answer and the run is done. If it is tool_use, the model wants to call something — so you dispatch the tool, append the result to the message history as a tool_result block, and call the model again.
The Anthropic playbook describes it this way: "The agent then formulates a plan, executes actions based on available tools, observes the results, and adjusts its approach based on feedback... keeps repeating this cycle until the task is completed or it hits a stopping condition." That description maps exactly onto the code:
for turn in range(max_turns):
resp = call(model=BALANCED, system=SYSTEM_PROMPT,
messages=messages, tools=TOOL_SCHEMAS, ...)
if resp.stop_reason != "tool_use":
return Result(output=_text_of(resp), completed=True, ...)
messages.append(_assistant_block(resp))
for tc in _tool_use_blocks(resp):
content, is_error = execute(tc.name, tc.input)
# append tool_result back as user message
messages.append(_tool_results_block(results))
Perceive (the model reads the messages), decide (it picks a tool or says done), act (you execute and append). Repeat. The whole thing fits in one screen.
The three files and what each one does
The module has three files by design. loop.py is the loop itself — just control flow, no business logic. tools.py defines what the model can call: workspace-sandboxed file operations (read, write, list) and a run_python tool, all scoped so the model can't escape a working directory. Every path argument gets resolved and checked against the sandbox root before any file operation runs — that's the WorkspaceEscapeError safety rail, and it's the one piece of the file worth reading closely if you're building your own. prompts.py holds the system prompt, kept separate from the loop logic per the repo's convention that prompts never live inline in control flow.
That separation matters more than it sounds. When you embed your prompt string inside your loop function, you end up editing code to test prompt changes and editing prompts to fix code bugs. Keeping them in separate files keeps those concerns from bleeding into each other.
Failure modes and how the loop handles them
Two things can go wrong mid-run, and the loop handles both without crashing. The first is an unknown tool name: if the model asks for delete_everything and that tool doesn't exist, the loop returns an is_error: true result to the model as its next input, and the model can recover. The run doesn't crash; the model gets the error as information and typically either retries with a valid tool or explains what it was trying to do.
The second is hitting max_turns. Rather than raising an exception, the loop returns whatever partial answer exists with completed: False. You can inspect the step trace to see how far it got. This is deliberate — a crashed run tells you nothing; a partial run with a trace tells you exactly where the model got stuck.
The test suite in patterns/agent_loop/tests/ covers both cases with fabricated SDK responses, so all 206 tests in the repo pass in about 12 seconds without an API key. The limitation is real and stated: model behavior (does the prompt actually work, does the agent actually converge) is unverified without a live API key. The code is tested; the prompt hasn't been. That's an honest distinction worth maintaining in your own projects.
When to use a hand-rolled loop
Honestly? Mostly for understanding. The point of writing the loop yourself is to see every mechanic: how tool calls become user messages, how errors feed back, how budget tracking fits around the outside. Once you've read it once, you know what the Claude Agent SDK is doing for you when you use it for real work — context management, retries, multi-turn state. You don't have to manage any of that manually.
There are cases where hand-rolling is the right call in production: unusual sandboxing needs, environments where you can't add dependencies, or a codebase where you need to control every mechanic of tool dispatch. But that's the exception, not the default. agents/research_agent in the same repo does the same job on the real SDK, and the SDK earns its keep.
The tell for when to avoid a bare loop: if you catch yourself reinventing context management, token truncation, or retry logic, you're doing work the SDK already did. That maintenance cost is real, and this repo pays it deliberately for pedagogy — you shouldn't pay it in production.
What changes when you scale up
The single-agent loop scales less gracefully than people expect. It works well for open-ended problems where you can't predetermine the steps — "read these files, summarize what you find" is a good fit. It works less well when you need the same answer, right, every time; that calls for the more constrained patterns like chaining or routing. And it breaks down for genuinely complex work that benefits from parallelism or specialization, which is where multi-agent architectures enter.
But before any of that: read the 81-line loop. Everything else in this space is built on top of it, including the frameworks that hide it from you.