Every "build an agent" tutorial converges on the same five lines, and they are correct: call the model, let it ask for a tool, run the tool, hand the result back, repeat. Anthropic's own definition is exactly this — an agent is a model "called in a loop," directing its own tool use until the work is done. There is no hidden machinery. A framework is just this loop with a logo on it.
So if the loop is five lines, why does the hand-rolled version so often 400 on turn two, or spin forever, or quietly burn a hundred dollars? Because the loop's body is trivial and its edges are not. The correctness lives in two places nobody puts in the title: four message-shape rules the API will reject you for breaking, and four stopping conditions that decide whether the loop ever ends. Get those eight things right and you own a durable agent outright — no framework, no runner.
The core loop#
Here is the whole thing in the Claude Messages API. Read it once; then we spend the rest of the piece on the four lines that look boring and aren't.
import anthropic
client = anthropic.Anthropic()
messages = [{"role": "user", "content": task}]
while True:
resp = client.messages.create(
model="claude-sonnet-5", # confirm the current id on the models page
max_tokens=8000,
tools=tools,
messages=messages,
)
if resp.stop_reason != "tool_use":
break # end_turn / max_tokens / refusal → done
messages.append({"role": "assistant", "content": resp.content}) # rule 1
results = []
for block in resp.content:
if block.type == "tool_use": # rule 2 + 4
try:
out = run_tool(block.name, block.input)
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": out})
except Exception as e: # rule 3
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": f"error: {e}", "is_error": True})
messages.append({"role": "user", "content": results})
The model responds with a tool_use block — {"type": "tool_use", "id": "toolu_…", "name": "get_weather", "input": {…}} — and you answer with a matching tool_result — {"type": "tool_result", "tool_use_id": "toolu_…", "content": "…"}. That handshake is the entire protocol.
The four rules the API enforces#
These are not style. Break one and your next request comes back 400.
- Preserve the assistant's full
content. Appendresp.content, not just its text. Thetool_useblocks have to stay in the transcript, because yourtool_resultblocks point back at their ids. Drop them and the ids reference nothing. - Every
tool_usegets exactly onetool_result. If the model made three calls, you return three results — same ids. A missing id is the single most common 400 in a hand-rolled loop. - The results turn is
tool_resultblocks only. It's ausermessage that contains nothing else — no chatty text block wedged alongside. Errors don't break the rule; they use it, with"is_error": true. - Parallel calls come back in one message. When the model emits several
tool_useblocks at once, return all their results in a single user message. Splitting them across turns quietly trains the model to stop calling tools in parallel — a silent performance regression, not an error.
The through-line: the API treats the tool exchange as one atomic, fully-accounted turn. Account for every call, keep the shape clean, and the 400s vanish.
The four things that keep it bounded#
A loop that is shaped correctly can still be a disaster, because nothing above makes it stop. Four guards do:
- A turn cap. Wrap the loop in a counter and break at, say, 25 iterations. Anthropic names this directly: include "stopping conditions such as a maximum number of iterations." Without it, one confused model that keeps re-calling the same tool runs until your credits do.
- A wall-clock deadline. The SDK's
timeoutis per request (10 minutes by default) — it does nothing about a loop that makes forty fast requests. Stampdeadline = time.monotonic() + 300before the loop and check it each pass. Library timeouts guard a call; only you can guard the loop. - A real error path. Rule 3's
is_error: trueisn't just for 400-avoidance — it's what lets the agent recover. A failed tool becomes an observation the model can react to, exactly the tool-call error handling discipline that separates robust agents from brittle demos. Pair it with a retry budget so a flapping tool can't drive the turn cap by itself. - A context plan. Every turn re-sends the entire transcript, and tool results compound fast — a long run will crawl toward the context ceiling and get slower and dumber on the way. This is where context engineering stops being theory: cache the stable prefix, and clear or summarize stale tool output as the window fills. The division of labor between context editing, compaction, and the memory tool is the whole answer to "my agent gets worse the longer it runs."
When to reach for more#
Once this runs, the honest advice is: don't rush to a framework. The hand-rolled loop gives you total visibility into every turn, which is precisely what you want while learning and for any agent small enough to hold in your head. A framework or the SDK's built-in runner earns its keep when you need durability across restarts, checkpointing, orchestration, or tracing you'd otherwise hand-build — the when-to-hand-roll-vs-adopt-a-runner call, made on real pain rather than default.
But the loop itself is not the thing you outsource. It's five lines, four rules, and four guards — and now it's yours. The next question a real agent forces on you is what to do at each exit: end_turn is easy, but max_tokens, refusal, and pause_turn each demand their own handling, which is where a loop stops being a toy.



