One page to build from. Six failure modes to debug with.
One page to build from. Six failure modes to debug with.
Day 03 of 30 Days of AI Agents · @mewcp_ai
The carousel explained why the loop exists. This is the part you need when you actually open an editor: how to build one, how to stop one, and how to work out what went wrong at 3am when it has been running for six hours and your bill is still climbing.
I've kept it framework-agnostic and model-agnostic on purpose. Nothing here breaks when a library renames a method.
What you need to know already: functions, API calls, while loops.
What you don't: any agent framework, orchestrator, or tool-calling schema.
Where to start, depending on why you're here:
| If you are | Read |
|---|---|
| Building your first loop | Sections 1 through 5, in order |
| Debugging something that's misbehaving | Section 4, which is indexed by symptom |
| Reviewing someone else's agent code | Section 3 and Section 7, as a checklist |
| Explaining this to a stakeholder | Sections 1 and 2 |
Four moves, one pass, repeat until an exit fires.

| In | The goal, plus every observation from previous passes |
| Out | One decision: which tool to call with which arguments, or "I'm done" |
| What the model actually receives | The message history you chose to send. On pass 1 that's just the goal. On pass 3 it's the goal plus two rounds of calls and results. |
| Where this usually goes wrong | Planning the whole route on pass 1 and never re-planning. If your code decides the tool sequence up front and the model only fills in the arguments, you've written a workflow, not an agent. |
| In | The tool name and arguments the model picked |
| Out | Whatever your function returns: a string, JSON, an error |
| What the model actually receives | Nothing yet. Act happens in your code. The model isn't running while your tool executes. |
| Where this usually goes wrong | Executing a tool call without validating it first. Those arguments are model output, not trusted input. Check types, check bounds, and put an approval gate in front of anything destructive. |
| In | The tool's return value, including failures |
| Out | A tool result appended to the message history |
| What the model actually receives | Only what you send back. Swallow an exception and send nothing, and the model sees a tool call that simply vanished. |
| Where this usually goes wrong | Dropping failures. A tool that raises still has to produce an observation, flagged as an error. Silence teaches the agent to retry the same broken call forever. |
| In | The accumulated history: goal, all prior actions, all prior observations |
| Out | The next pass, or an exit |
| What the model actually receives | Everything your loop chose to resend. Section 2 covers this properly. |
| Where this usually goes wrong | No exit condition at all. See Section 3. |
The one-line version: the model decides, your code acts, the result becomes the next input.
Goal: book 30 minutes with Sam.

| Pass | Knows | Plans |
|---|---|---|
| 1 | The goal. Nothing else. | Check my calendar |
| 2 | The goal, plus my week is free Tue and Thu | Check Sam's calendar |
| 3 | The goal, my availability, and that Sam is busy Tue but free Thu | Book Thursday |
The plan changed on pass 2. Not because pass 1 was wrong, but because at pass 1 the agent had never looked at anything. A rewritten plan is the loop working. It isn't a defect.
The model is stateless between calls.
Each pass is a fresh API request. The model isn't sitting there holding the thread. Everything it "remembers" from pass 1 is something your code deliberately sent again in pass 2.
So the loop is really doing two jobs at once:
Job 2 is where most first agents quietly break. Drop the observation from pass 1 and the agent re-derives it, calls the same tool, gets the same answer, and calls it again. It isn't confused. It's genuinely seeing the task for the first time, every time.
| Strategy | What it does | What it costs |
|---|---|---|
| Full transcript | Resend every message, every pass | Context grows each pass and eventually you hit the window |
| Summarised history | Compress older passes into a summary | You might compress away the one detail that mattered |
| Structured state | Carry an explicit object ({availability: ..., booked: ...}) alongside a trimmed transcript | More code to write, but far more predictable |
None of these is the right answer everywhere. Full transcript is fine for short loops and it's what most APIs make easiest. Structured state is where long-running agents tend to end up. Choose based on how many passes you expect and how much each tool hands back.
On sizing it: don't guess. Context growth per pass is roughly the assistant message tokens plus the tool result tokens. Measure it with your provider's token counting endpoint against your own prompts rather than borrowing a number from someone's blog post. Token counts vary by model and by content.
A loop without an exit isn't an agent. It's a bill.
"Stop when the goal is met" sounds like an instruction until you try to implement it. It's actually four separate mechanisms, each with its own check.

When it applies: the result actually satisfies the goal.
The check:
def goal_met(final_text, state):
# Do NOT trust "I have completed the task."
# Verify against something outside the model.
return state.get("event_id") is not NoneWhat to return: the result, plus the evidence it worked. An ID, a diff, a file path.
How it's usually got wrong: treating self-reported completion as completion. A model saying "Done!" is a claim, not a verification. Where the goal has an observable side effect (a row written, a file created, an event booked) check for the side effect. Where it doesn't, verification is a real design problem, and you have options: a second cheap model call acting as a grader, schema validation, or a human. Pick one deliberately. "The model said so" isn't one of them.
When it applies: the run has cost more than it's worth.
The check:
if pass_no > MAX_PASSES: return stop("budget: passes")
if time.monotonic() - t0 > MAX_SECS: return stop("budget: wall clock")
if tokens_used > MAX_TOKENS: return stop("budget: tokens")What to return: partial work, plus a note on what's left. A budget exit isn't an error. It's a scheduled stop.
How it's usually got wrong: having only one ceiling. Max passes won't save you from a single pass that hangs on a slow tool. Wall clock won't save you from 400 fast, cheap, useless passes. You want at least two ceilings.
Cost is something you can compute rather than guess at: sum across passes of (input_tokens × input_rate) + (output_tokens × output_rate). Note that input tokens grow every pass under a full-transcript strategy, so cost per pass is not flat.
When it applies: the loop has stopped accumulating.
The check:
signature = (tool_name, canonical_json(tool_args), canonical_json(result))
if signature in recent_signatures:
return stop("no progress: repeated action/result pair")
recent_signatures.append(signature)What to return: the repeated action, so a human can see exactly where it got stuck.
Picking N: compare against the last N action/result pairs, not just the previous one, because an agent can oscillate A, B, A, B quite happily. N of 3 catches simple oscillation cheaply. A larger N catches longer cycles but risks flagging legitimate repetition. Polling a job status is the same call with the same result on purpose. If your agent legitimately polls, exclude those tools from the check rather than raising N.
How it's usually got wrong: comparing only the action. The same tool, called with the same arguments, returning a different result is progress. It's the identical pair that means the loop has stalled.
When it applies: the agent hits something a human should decide. An ambiguity, a permission, an irreversible action.
The check: make it a tool.
{
"name": "needs_human",
"description": "Escalate when the goal is ambiguous, you lack permission, "
"or the next step is irreversible. Say what you need.",
"input_schema": {
"type": "object",
"properties": {"question": {"type": "string"},
"work_so_far": {"type": "string"}},
"required": ["question"],
What to return: the question and the partial work. An escalation that returns only "I couldn't do it" throws away everything the run learned.
How it's usually got wrong: designing escalation as an error path. It's a success path with a different recipient. If your escalation raises an exception and carries no state, you've built a loop that punishes the agent for correctly noticing its own limits.
Indexed by symptom, because a symptom is what you've got when something is wrong.

| # | Symptom | Likely cause |
|---|---|---|
| 1 | Runs forever, same call repeating | Observations aren't being carried forward |
| 2 | Stops immediately, declares success | No verification of the goal condition |
| 3 | Takes a wildly different path each run | Goal underspecified, success condition ambiguous |
| 4 | Calls a tool that already failed, again | Failures aren't fed back as observations |
| 5 | Slower and more expensive every pass | Unbounded context growth |
| 6 | Right answer, wrong action taken | Plan and act steps aren't separated |
Fix: append both the assistant message and the tool result to history on every pass. Check it by printing len(messages) each pass. If it isn't growing by two, your loop has amnesia.
Trace signature: pass 4 looks byte-identical to pass 2.
Fix: Exit 1 above. Verify against a side effect or an external grader, never the model's own claim.
Trace signature: one pass, no tool calls, confident prose.
Fix: this is often fine. Two correct routes to the same outcome is not a bug. It's only a problem when the outcome differs. Tighten the goal statement and make the success condition explicit and checkable before you start tuning anything else.
Trace signature: same goal, different tool sequence, different final answer.
Fix: catch the exception and return it as an observation flagged as an error, instead of letting it propagate or swallowing it.
Trace signature: a tool call with no matching result in the transcript, followed by the same call.
Fix: you're resending the full transcript and every tool result is large. Trim results before they enter history, move to structured state (Section 2), or cap how much of any single result you carry forward.
Trace signature: input token count climbing steeply pass over pass while output stays flat.
Fix: the model reasoned correctly and then called a different tool than its reasoning implied. This is usually a tool description problem: two tools whose descriptions overlap. Make each description say when to use this one and not the other.
Trace signature: the reasoning text and the tool call that follows it disagree.
This is a skeleton, not a framework. It exists to show you there's no magic in the middle.
function run_agent(goal, tools, max_passes, deadline, token_cap):
history ← [ user_message(goal) ]
seen ← [] # recent (action, result) pairs
tokens ← 0
for pass_no from 1 to max_passes: # EXIT 2: pass budget
if now() > deadline: return stop("budget: time", history)
if tokens > token_cap: return stop("budget: tokens", history)
decision ← model.plan(history, tools) # PLAN
history ← history + [ decision.message ] # accumulate the plan
tokens ← tokens + decision.tokens_used
if decision.wants_no_tools:
ok, why ← verify(decision.text) # EXIT
All four exits are in there. The accumulation step is those two history ← history + [...] lines. Delete either one and you get failure mode 1.
Python, using the Anthropic Messages API with a hand-written loop. A manual loop is the right choice here because the loop is the thing being taught. In production, most SDKs ship a tool runner that handles this for you, and you should generally use it.
import json, time
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the env
MODEL = "claude-opus-5"
def canonical(obj) -> str:
"""Stable serialisation so identical calls compare equal."""
return json.dumps(obj, sort_keys=True, default=str)
def run_agent(goal, tools, execute, verify,
max_passes=
A few notes on the parts that actually matter:
The assistant message gets appended before tool execution. If your loop appends only tool results, the model loses its own reasoning and rebuilds it from scratch every pass.
All tool results for one assistant turn go back in a single user message. Splitting them across several messages breaks the pairing the API expects.
is_error: True is how a failure becomes an observation instead of a silence. That one flag prevents failure mode 4.
The no-progress check compares the action and the result together. Same call, new result, that's progress.
Notice that the stall check doesn't return immediately. Every tool call in the turn needs a matching result before the loop exits, otherwise you leave a dangling tool call in the transcript and any resumption of that conversation will be rejected. Set a flag, finish the turn, then exit.
Every return carries either the result or the accumulated messages. Nothing exits empty-handed.
verify and execute are yours to write. The loop is generic. The judgement isn't.
A note for whoever ships this. Check the API surface, parameter names, and model identifiers against live provider docs before publishing, and run the code end to end against a real key. This example was written against current Anthropic Messages API documentation and syntax-checked, but it has not been executed against the live API, because no API key was available where it was written. Treat that end-to-end run as an open task before this goes out. Don't add latency, cost or token figures anywhere in this document. Where a number would help, show the formula instead.
This is the most common loop. It isn't the only one, and knowing the alternatives stops you over-generalising from a single diagram.
| Variant | How it differs | When it fits |
|---|---|---|
| Plan-first | Produce the whole plan on pass 1, then execute steps without re-planning | The environment is known and stable, and you want predictability and auditability more than adaptability |
| Plan and revise | Plan up front, but re-plan when an observation contradicts the plan | Long tasks where re-planning every pass is wasteful but rigidity is fatal |
| Delegating | The loop's "tool" is another agent with its own loop | Independent sub-tasks that would otherwise fill one agent's context with reading |
| Single-pass tool use | One model call, one tool call, one response. No loop at all | The task needs exactly one lookup, which covers most "chatbot with search" features |
| Self-critique | An extra grading step between Observe and Repeat | Output quality matters more than latency and you have a rubric you can check against |
The loop in Section 5 is the general case. If your task fits single-pass tool use, use single-pass tool use. A loop you don't need is a cost you don't need.
Nine things to check before you let a loop off the leash.
If you can't answer number 9 without guessing, you don't have an agent yet. You have a loop.
The definitions used throughout this series, so nothing shifts meaning between days.
| Term | Definition |
|---|---|
| Pass | One complete trip through Plan, Act, Observe |
| Iteration | The same thing. Used interchangeably in most codebases |
| Plan | The decision about the next single step toward the goal |
| Act | Executing exactly one tool call chosen during Plan |
| Observe | Reading what the tool returned and adding it to history |
| Observation | A single tool result, success or failure, as the model will see it |
| Trace | The ordered record of every plan, action and observation in a run |
| Accumulation | The growth of known facts across passes, which is what makes it a spiral |
| State | Everything carried from one pass to the next, whether transcript or object |
Day 04: what a tool actually is. The Act step, opened up. Schemas, descriptions, why a badly described tool breaks an otherwise correct loop, and what "the model called the wrong tool" really means.
The 30 days run roughly like this: foundations first (what an agent is, the loop, tools), then capability (memory, context, retrieval), then reliability (evaluation, failure modes, observability), then production (cost, safety, deployment).
MewCP works on the production end of this.

Day 03 of 30 Days of AI Agents · @mewcp_ai
| Any rule that ends the loop. There are four, in Section 3 |
| Budget | A ceiling on passes, time, tokens or cost |
| Escalation | Handing control to a human, returning partial work alongside the question |