AI EngineeringZero to ProductionHome·About·What’s new·Contact
Agents & Tools · Part 4.2 · Intermediate

Agent patterns

In 4.1 you built the basic agent loop: the model looks at a goal, calls a tool, reads the result, and repeats until done. That loop is the engine — but a bare loop is not a strategy. This chapter is about the named control-flow patterns that decide how the agent reasons: ReAct (think, act, observe, repeat), plan-and-execute (plan upfront, then run the plan), reflection (draft, critique, revise), and routing (classify first, then dispatch). You will also learn the error-handling and retry control flow that keeps any of these patterns from falling over when a tool misbehaves. Every lab runs offline against a deterministic fake decider so you can see the exact shape of the control flow.

⏱️ ~95 min🤖 Agents🎯 Intermediate🐍 Runnable Python
🤖 What runs here, and what a real model replacesEvery lab in this chapter runs right here, offline, with no key. The trick is that we replace the LLM with a tiny deterministic fake decider — a plain Python function that returns the same “thought” or “plan” every time. That lets you watch the control flow — the loop, the branch, the retry — without a network call. In production you swap that one function for a real model call (e.g. claude-opus-4-8); the surrounding control flow is identical. Any traces shown are illustrative, not benchmarks.

Learning objectives

  • Recall the basic agent loop from 4.1 and explain why it needs a reasoning structure on top.
  • Implement the ReAct pattern — interleave Thought → Action → Observation until the task is solved.
  • Implement plan-and-execute and say precisely how it differs from ReAct.
  • Add a reflection (self-critique) step that revises a first draft.
  • Build a router that classifies a request and dispatches to the right sub-flow.
  • Wrap tool calls in error handling with capped retries that feed the error back to the model.
  • Pick the right pattern for a given task instead of reaching for one reflex.
📋 Before you startThis page builds on: Agent & tool foundations. Jumping in? Skim those first — or open the recap.
30-second recap
  • Agent & tool foundations: The model asks to call a tool, your code runs it and returns the result, and the loop repeats until the model answers — with step caps so it can't run forever.

1 · Recap — the loop needs a strategy

Here is a failure you have almost certainly seen a demo agent commit. You give it a two-part task — “find the two busiest cities and add their populations” — and it confidently blurts a single answer that's wrong, because it tried to do everything in one leap instead of one step at a time. The loop from 4.1 gave the agent hands (it can call tools) but not a method for using them. This chapter is about the method.

Think of the difference between a kitchen full of equipment and a recipe. In 4.1 you built the kitchen: the knives, the stove, the pantry — the tools the agent can reach for. But hand a stocked kitchen to someone with no recipe and no order of operations and you get chaos, or a dish that's raw in the middle. A pattern is the recipe: it tells the cook when to chop, when to taste, when to start over. Same kitchen, wildly different dinner.

The common mistake here is to assume the model's raw intelligence is the strategy — that a smart enough model will just figure out the right sequence on its own. Sometimes it does. But “sometimes” is the enemy of production: an agent that solves a task nine times and quietly mangles it the tenth is worse than useless, because you can't trust any single run. Imposing an explicit control-flow structure is what turns “usually works” into “works because I can see exactly how it works.” The structure is also what you inspect when something goes wrong — you can't debug a black box, but you can debug a loop with a visible trace.

In 4.1 the agent loop was: give the model a goal and a set of tools; it emits a tool call; you run the tool and hand back the result; repeat until the model says it is done. That is the mechanism. What it does not tell you is how the model should decide what to do at each step — and for anything beyond one tool call, that structure is what separates a reliable agent from one that flails.

The patterns below are the well-known, publicly documented ways to impose that structure. They are not competing products; they are shapes of control flow. A production agent often composes several: route first, then run a plan, reflecting on the risky step, retrying any tool that fails.

Goal from user Reasoning pattern ReAct / planreflect / route Tools run + observe Result done or loop
🗺️ How to read this diagram

Read it straight across left to right, following the four arrows — a goal enters on the left and a result comes out on the right, with the pattern sitting in the middle.

  • The blue Goal box on the far left is what the user asks for (“from user”) — the input that starts everything.
  • The purple Reasoning pattern box is where this chapter lives: it names the choices — ReAct / plan / reflect / route — that decide how the agent thinks between goal and tools.
  • The arrows then reach the amber Tools box (“run + observe”) and the green Result box (“done or loop”) — the pattern drives the tools, and the outcome either finishes or cycles back.

In short: The reasoning pattern is the layer between the goal and the tools — it's what turns raw tool access into a method for using it.

The distinction to hold ontoThe loop (4.1) is how the agent acts. A pattern (this chapter) is how the agent thinks inside that loop. You will layer robust tool design (4.3), reliability and guardrails (4.4), and full production systems (4.5) on top of these patterns later.

2 · ReAct — reason, act, observe, repeat

Watch how a careful person actually solves an unfamiliar problem and you'll never see them sprint to the answer. They take one step, look at what happened, and let that result shape the next step. You look up a train time, then — knowing it — decide whether to book the hotel or keep searching. ReAct is that same discipline, made explicit for an agent: think a little, act once, look at the result, and only then decide what to do next.

ReAct (Reason + Act) is the workhorse pattern for multi-step reasoning. Instead of guessing the whole answer at once, the model alternates between a Thought (private reasoning about what to do next), an Action (a concrete tool call), and an Observation (the tool's result). It reads the observation, thinks again, and repeats until it can answer. The interleaving is the point: each action is chosen in light of what the previous one returned.

The word doing the heavy lifting is interleave. A model left to reason in one long monologue will happily invent a fact, build three more inferences on top of it, and present a fluent, confident, wrong answer — because nothing ever checked its assumptions against reality. ReAct breaks that spell by forcing the model to act and observe between thoughts. Each observation is a small collision with the real world: the tool returns 2,100,000, or it returns an error, and either way the next thought has to reckon with what actually came back rather than what the model wished were true. That grounding is why ReAct is the default reach for open-ended tasks.

The classic mistake beginners make is collapsing Thought and Action into one blurred move — asking the model to “think about the population and also fetch it” in a single turn. When you do that you lose the observation step, and with it the whole benefit: the model never pauses to read what the tool returned before committing to the next action. Keep the three beats separate and named. The other trap is forgetting the loop can run indefinitely — a confused model may thought-and-act in circles — which is why the bound (max_steps) below is not decoration but a safety belt. Think, act, observe, and only then think again: the rhythm is the pattern.

Thought what next? Action call a tool Observation read result Answer when done
🗺️ How to read this diagram

Read it left to right, one box at a time — each arrow is a beat in the cycle, and the last box only appears once the loop has enough to finish.

  • The purple Thought box starts the cycle — “what next?” — the model's private reasoning about which move to make.
  • It flows into the amber Action box (“call a tool”) and then the blue Observation box (“read result”): the model does exactly one thing and then looks at what actually came back.
  • The green Answer box is the exit, reached “when done” — until then the observation feeds the next Thought, so the three left-hand boxes repeat before the agent ever reaches it.

In short: Think, act, observe, repeat — ReAct interleaves reasoning and tool calls so each next step is chosen in light of what the last one returned.

Here is a full ReAct loop solving a 2-step task — look up two facts, then combine them. The fake_model() stands in for the LLM: given the running trace, it returns the next Thought+Action. In production this one function becomes a model call; the loop around it does not change.

python · react loop (runnable — click ▶ Open in terminal)
react.py# ReAct: interleave Thought -> Action -> Observation until we can answer.
# The 'model' is a deterministic fake so this runs offline; swap it for a real
# LLM call and the control flow is IDENTICAL.

TOOLS = {
    'population': lambda city: {'Paris': 2_100_000, 'Lyon': 500_000}[city],
}

def fake_model(trace):
    """Return the next 'Thought' + 'Action' given the trace so far.
    A real LLM replaces this; it reads the same trace and emits the same shape."""
    obs = [line for line in trace if line.startswith('Observation:')]
    if len(obs) == 0:
        return ('Thought: I need the population of Paris first.',
                ('population', 'Paris'))
    if len(obs) == 1:
        return ('Thought: Now I need the population of Lyon.',
                ('population', 'Lyon'))
    return ('Thought: I have both; I can answer now.', ('finish', None))

def react(goal, max_steps=6):
    trace = [f'Goal: {goal}']
    facts = []
    for step in range(max_steps):
        thought, (tool, arg) = fake_model(trace)
        trace.append(thought)
        if tool == 'finish':
            total = sum(facts)
            trace.append(f'Answer: combined population = {total:,}')
            break
        trace.append(f'Action: {tool}({arg!r})')
        result = TOOLS[tool](arg)
        facts.append(result)
        trace.append(f'Observation: {result:,}')
    return trace

for line in react('total population of Paris and Lyon'):
    print(line)
Goal: total population of Paris and Lyon
Thought: I need the population of Paris first.
Action: population('Paris')
Observation: 2,100,000
Thought: Now I need the population of Lyon.
Action: population('Lyon')
Observation: 500,000
Thought: I have both; I can answer now.
Answer: combined population = 2,600,000

Notice the loop never assumes how many steps it takes — it keeps going until the model emits finish, bounded by max_steps. That decide-as-you-go quality is ReAct's strength: the agent adapts to what each observation reveals. Its cost is that it thinks step by step, so it can wander if the model is weak or the tools are noisy.

Why the trace mattersThe full Thought/Action/Observation trace is the model's working memory — you pass it back in every step so the next decision is informed. Keeping that trace clean and inspectable is also how you debug an agent later (trajectory evaluation lives in 4.4).

3 · Plan-and-execute — decide the whole path first

Some journeys you improvise; others you map before you leave. Wandering a new city on foot, you decide each turn as you go — that's ReAct. But when you're driving a delivery route with twelve fixed stops, you don't rediscover the road at every corner; you plan the whole route once, then just follow it. When a task's shape is known in advance, re-deciding every step is wasted effort — and every one of those re-decisions, for an agent, is another model call you pay for.

Plan-and-execute flips the order. The model first writes a plan — an explicit, ordered list of steps — and only then executes each step in turn. Where ReAct decides the next move after every observation, plan-and-execute commits to the path upfront. That makes the agent cheaper and more predictable for tasks whose shape is known, at the cost of adaptability if reality diverges from the plan.

The real payoff is more than saving model calls. A plan is legible: because the whole path exists as data before anything runs, you can print it, log it, show it to a human for approval, or diff two runs against each other. That's a property ReAct simply can't offer — in ReAct the “plan” only exists in hindsight, scattered across the trace. When a workflow is repeatable and the stakes of a wrong step are high (touching production, spending money, emailing a customer), being able to see and gate the plan before execution is worth a great deal.

The mistake to avoid is treating a plan as a promise the world is obliged to keep. Reality diverges: an order list comes back empty, an API you planned to call is down, step three assumed something step two disproved. An agent that marches through a now-invalid plan produces confidently wrong results — the brittleness this pattern trades for its predictability. The mature answer, shown in the tip after the lab, is to re-plan: execute the plan, but the moment a step's outcome contradicts an assumption, hand control back to the planner to revise what's left. That hybrid is where most production systems actually live — plan for the predictable spine, adapt at the joints.

ReAct · decide → run, one step at a time decide run 1 decide run 2 a model call before every step Plan-and-execute · plan once, then run the whole plan Plan (once) 1→2→3 upfront run 1 run 2 run 3 Result Where the deciding happens. ReAct spends a model call to re-decide before every action; plan-and-execute pays for one planning call, then runs the fixed steps with no further deciding — cheaper and predictable when the path is known.
🗺️ How to read this diagram

Read it top-to-bottom as two strips: the top strip is ReAct, the bottom strip is plan-and-execute. Both accomplish the same multi-step task; the difference is when the model is asked to think.

  • The top strip alternates purple decide boxes and amber run boxes — a model call, then an action, then another model call before the next action. Each arrow crosses back through “decide”.
  • The bottom strip has a single blue Plan (once) box that lays out steps 1→2→3 upfront, followed by the amber run boxes flowing straight into the green Result with no more deciding between them (grey arrows = pure execution).
  • The payoff is the missing boxes: plan-and-execute has one deciding box instead of one per step — fewer model calls, and the whole path exists as data before anything runs, so you can inspect or approve it.

In short: ReAct re-decides before every step (adaptable, more model calls); plan-and-execute decides once then executes (cheaper, predictable) — pick by whether the path is known ahead of time.

python · plan and execute (runnable — click ▶ Open in terminal)
plan_execute.py# Plan-and-execute: model writes an ordered plan, then we run each step.
# fake_planner() is deterministic; a real LLM would generate the plan text.

STEP_TOOLS = {
    'fetch_orders': lambda: [12, 7, 31],
    'sum_orders':   lambda xs: sum(xs),
    'format':       lambda n: f'Total orders this week: {n}',
}

def fake_planner(goal):
    """Return an ordered plan (list of step names). A real LLM writes this."""
    return ['fetch_orders', 'sum_orders', 'format']

def execute(goal):
    plan = fake_planner(goal)
    print('PLAN:')
    for i, step in enumerate(plan, 1):
        print(f'  {i}. {step}')
    print('EXECUTE:')
    state = None
    for step in plan:
        if step == 'fetch_orders':
            state = STEP_TOOLS[step]()
        elif step == 'sum_orders':
            state = STEP_TOOLS[step](state)
        elif step == 'format':
            state = STEP_TOOLS[step](state)
        print(f'  ran {step} -> {state!r}')
    return state

print('RESULT:', execute('report this week\'s order total'))
PLAN:
  1. fetch_orders
  2. sum_orders
  3. format
EXECUTE:
  ran fetch_orders -> [12, 7, 31]
  ran sum_orders -> 50
  ran format -> 'Total orders this week: 50'
RESULT: Total orders this week: 50
ReActPlan-and-execute
When decisions happenAfter every observation (as you go).All upfront, before any tool runs.
Best forOpen-ended, exploratory tasks where the next step depends on the last result.Known, repeatable workflows with a predictable shape.
WeaknessCan wander; more model calls (one per step).Brittle if reality diverges from the plan; may need a re-plan step.
Hybrids are commonReal systems often re-plan: execute the plan, but if a step fails or an observation contradicts an assumption, hand control back to the planner to revise the remaining steps. That blends plan-and-execute's predictability with ReAct's adaptability.

4 · Reflection — draft, critique, revise

Nobody sends the first draft. A good writer bangs out a rough version, then reads it back with a colder eye — “this paragraph doesn't answer the question, this claim has no support” — and fixes what they find. The first pass is for getting something down; the second is for getting it right. Reflection gives an agent that same second pass, and it's often the difference between an answer that looks plausible and one that's actually correct.

Reflection (self-critique) adds a quality gate: after producing a first attempt, the model critiques its own output against the requirements, then revises. It is the agent equivalent of “read your work before you hand it in.” You reach for it when correctness matters more than latency — the extra pass costs time and tokens but catches mistakes a single shot would ship.

Why does critiquing work at all, when the same model wrote the flawed draft? Because judging is easier than generating. Producing a complete, correct answer in one shot forces the model to juggle every requirement at once; but handed a finished draft and asked the narrower question “does this mention the cause and the fix?”, it only has to check, not create. Splitting the hard job into write then grade against a checklist plays to a real strength — the same reason a proofreader catches errors the author's own eye slid past. The critique step is most powerful when you give it explicit requirements to check against, rather than a vague “make it better.”

The failure mode that bites people is the loop that never ends. If the critic is fussy — or worse, if revising to satisfy one requirement quietly breaks another — the draft ping-pongs forever, and every round is more tokens and more latency on the meter. That's why reflection always ships with a hard max_rounds cap and a clean exit the moment the critique comes back empty, as the warning after the lab insists. The second, subtler mistake is reflecting on everything: reflection earns its cost only when correctness genuinely outranks speed. For a throwaway summary it's overkill; for a customer-facing refund decision it's cheap insurance.

Draft first attempt Critique find the flaw Revise fix it
🗺️ How to read this diagram

Read it left to right, following the arrows: a first attempt is judged, then repaired — the same output passing through three named stages.

  • The blue Draft box on the left is the model's first attempt — the “first attempt” sub-label — produced before any checking happens.
  • The arrow leads to the amber Critique box in the middle, whose job is to “find the flaw” — it inspects the draft and names what's wrong or missing rather than accepting it.
  • The final arrow reaches the green Revise box — “fix it” — which applies the critique to produce a better version, the payoff of the whole loop.

In short: Reflection is draft → critique → revise — the model checks its own work and repairs it instead of shipping the first attempt.

python · reflection loop (runnable — click ▶ Open in terminal)
reflection.py# Reflection: draft -> critique -> revise. All three 'model' calls are faked
# deterministically here; a real LLM replaces each, control flow unchanged.

REQUIRED = ['cause', 'fix']   # a good incident summary must mention both

def fake_draft(task):
    return 'The checkout service went down.'   # incomplete on purpose

def fake_critique(text):
    """Return a list of missing requirements. A real LLM judges the draft."""
    missing = []
    if 'queue' not in text.lower():
        missing.append('cause')
    if 'restart' not in text.lower() and 'scale' not in text.lower():
        missing.append('fix')
    return missing

def fake_revise(text, missing):
    add = {'cause': ' The queue backed up.',
           'fix': ' Drain the queue and scale the consumer group to fix it.'}
    for m in missing:
        text += add[m]
    return text

def reflect(task, max_rounds=3):
    draft = fake_draft(task)
    print(f'draft: {draft}')
    for r in range(max_rounds):
        missing = fake_critique(draft)
        if not missing:
            print(f'round {r}: looks complete, stopping.')
            break
        print(f'round {r}: missing {missing} -> revising')
        draft = fake_revise(draft, missing)
    return draft

print('final:', reflect('summarize the checkout incident'))
draft: The checkout service went down.
round 0: missing ['cause', 'fix'] -> revising
round 1: looks complete, stopping.
final: The checkout service went down. The queue backed up. Drain the queue and scale the consumer group to fix it.
Cap the reflection loopSelf-critique can loop forever if the critic is never satisfied — always bound it with a max_rounds and stop when the critique comes back clean. A reflection loop with no cap is a common way to burn tokens (and money) on an agent that never returns.

5 · Routing — classify first, then dispatch

Call any large company and the first thing you hit is a menu — press 1 for billing, 2 for tech support, 3 for everything else. It feels bureaucratic, but it's doing something smart: instead of training every operator to handle every possible question, it sends your call to someone who only needs to know one domain deeply. Routing gives an agent that same front desk, and it's how you stop one bloated prompt from trying to be an expert at everything at once.

When an agent faces heterogeneous requests — a billing question, a bug report, a how-to — you do not want one giant prompt trying to handle everything. Routing puts a lightweight classifier step first: it reads the request, picks the right sub-flow (or tool, or specialized model), and dispatches to it. Each downstream handler stays small and focused, which makes the whole system easier to reason about and cheaper to run.

The trap is the one-mega-prompt agent: a single instruction block stuffed with “if it's about billing do this, unless it's a bug in which case do that, but for general questions…” It works in the demo and rots in production — every new case makes the prompt longer, the handlers interfere with each other, and you can't test any one behaviour in isolation because they all share the same tangled context. Routing cuts that knot. Because each handler is a small, independent unit, you can test it alone, give it its own tools, its own tuned prompt, even its own cheaper or more capable model — a trivial FAQ handler doesn't need your most expensive model, and a delicate billing flow can have one. The classifier stays deliberately thin; the intelligence lives in the focused handlers behind it.

One thing to keep honest: routing only shifts the problem, it doesn't erase it. A misclassification sends the request to the wrong expert, and a handler confidently answering the wrong question can be worse than an honest “I'm not sure.” So the classifier deserves real care — a sensible default bucket for the ambiguous cases (note the general fallback in the lab), and, in higher-stakes systems, a way to escalate or ask a clarifying question rather than force every request into a box. Route confidently, but design for the request that fits no clean category.

Request any kind Classifier pick the intent billing handler own tools + prompt bug handler own tools + prompt general (fallback) the ambiguous bucket One thin classifier, many focused handlers. The classifier only picks an intent; each handler stays small and independently testable, with its own tools, prompt, and even its own model. The green general bucket catches whatever fits no clean category.
🗺️ How to read this diagram

Scan it left to right: a request enters on the left, the classifier in the middle sorts it, and it exits into exactly one handler on the right.

  • The blue Request box is any incoming message — a billing question, a bug report, a how-to — all arriving through one door.
  • The purple Classifier is the only shared, always-run step; it does one cheap job — read the request and name its intent — then fans out along the three arrows.
  • The amber handlers (billing, bug) are the focused experts, each with its own tools and prompt so it can be tested in isolation; the green general handler is the deliberate fallback for requests that match no clear bucket.

In short: Keep the classifier thin and the handlers focused — routing turns one bloated do-everything prompt into small, independently testable units, with a fallback for the ambiguous cases.

python · router (runnable — click ▶ Open in terminal)
router.py# Routing: a classifier picks the handler, then we dispatch to it.
# fake_route() is deterministic; a real LLM (or a small classifier) replaces it.

def fake_route(request):
    """Classify the request into an intent. A real model does this step."""
    r = request.lower()
    if any(w in r for w in ('refund', 'charge', 'invoice')):
        return 'billing'
    if any(w in r for w in ('error', 'crash', 'broken', 'down')):
        return 'bug'
    return 'general'

HANDLERS = {
    'billing': lambda q: f'[billing] looking up your account for: {q}',
    'bug':     lambda q: f'[bug] opening a ticket for: {q}',
    'general': lambda q: f'[general] answering: {q}',
}

def agent(request):
    intent = fake_route(request)
    return intent, HANDLERS[intent](request)

for req in ['I want a refund on invoice 88',
            'the checkout page is down',
            'what are your hours?']:
    intent, reply = agent(req)
    print(f'{intent:8} <- {req!r}')
    print(f'         {reply}')
billing  <- 'I want a refund on invoice 88'
         [billing] looking up your account for: I want a refund on invoice 88
bug      <- 'the checkout page is down'
         [bug] opening a ticket for: the checkout page is down
general  <- 'what are your hours?'
         [general] answering: what are your hours?

The router itself is trivial control flow — classify, then look up a handler. The value is architectural: each handler is a small, testable unit, and you can give each one its own tools, prompt, or even a cheaper model. A real classifier is usually a quick model call or a small trained classifier; the dispatch table stays exactly as shown.

6 · Error handling & retries in the loop

The demo works flawlessly; the production version falls over in an hour. The reason is almost never the reasoning — it's that a tool hiccuped. The network blipped, an API returned a 429, a database was mid-failover for two seconds. In a notebook you never see these; at real volume they're constant. An agent that assumes its tools always succeed is an agent that assumes it lives in the demo, and the world it actually runs in is messier than that.

Tools fail. A call raises an exception, times out, or returns junk that does not parse. A naive agent crashes or, worse, feeds the garbage forward. The robust move is to catch the failure, feed the error back to the model as an observation, and let it retry — bounded by a maximum attempt count so a persistently broken tool cannot loop forever.

The phrase “feed the error back” is where an agent does something a plain retry loop can't. Ordinary software retries blindly — same call, same arguments, hope the blip passed. An agent can do better: hand the model the actual error text as an observation and it can reason about it. Told “invalid date format: expected YYYY-MM-DD”, a capable model reformats its argument and the second attempt succeeds — it fixed the cause, not just re-rolled the dice. That's why the error message matters as much as the retry itself; a vague “request failed” teaches the model nothing, while a precise one turns a failure into a hint.

Two mistakes sink naive retry code. The first is no cap: an unbounded retry against a genuinely broken tool — deleted resource, wrong credentials — is an infinite loop that quietly drains your token budget until someone notices the bill, which is exactly why max_attempts below is mandatory, not optional. The second, subtler one is retrying things that will never succeed. Retries are medicine for transient failures — a timeout, a rate limit, a momentary blip. They do nothing for terminal ones: an auth failure or a 404 is wrong on attempt one and wrong on attempt three, so retrying just burns attempts before failing anyway. The skill is telling the two apart and giving up immediately on the terminal kind, as the callout after the lab spells out.

Call tool attempt Error? exception / junk Feed error back as observation Retry (capped) if attempts left Give up when cap hit
🗺️ How to read this diagram

Read it left to right as one control-flow chain — a tool call at the start, and two possible endings depending on whether the retries run out.

  • The blue Call tool box is the attempt; it flows into the amber Error? box, which checks whether the result was an exception or junk.
  • On failure the red Feed error back box hands the error to the model “as observation,” and the purple Retry (capped) box loops the attempt again — but only “if attempts left.”
  • The green Give up box on the far right is the safety exit — reached “when cap hit,” so a genuinely broken tool stops instead of looping forever.

In short: Catch the failure, feed the error back, and retry up to a cap — the cap is what keeps a broken tool from becoming an infinite loop.

python · retry wrapper (runnable — click ▶ Open in terminal)
retries.py# Capped retries: catch the failure, feed the error back, try again up to a limit.
# The 'tool' here fails the first two times, then succeeds -- deterministic, no
# randomness or clock. A real agent feeds the error text back to the LLM.

class ToolError(Exception):
    pass

def flaky_tool(state):
    """Fails until it has been nudged twice; then returns a value."""
    if state['attempts'] < 2:
        raise ToolError(f"transient failure #{state['attempts'] + 1}")
    return 'OK: fetched 42 records'

def call_with_retries(tool, max_attempts=3):
    state = {'attempts': 0}
    last_error = None
    for attempt in range(1, max_attempts + 1):
        try:
            result = tool(state)
            print(f'attempt {attempt}: success -> {result}')
            return result
        except ToolError as e:
            last_error = str(e)
            print(f'attempt {attempt}: caught {last_error!r}; feeding back + retrying')
            state['attempts'] += 1        # what the model would learn from the observation
    print(f'giving up after {max_attempts} attempts; last error: {last_error!r}')
    return None

call_with_retries(flaky_tool)
attempt 1: caught 'transient failure #1'; feeding back + retrying
attempt 2: caught 'transient failure #2'; feeding back + retrying
attempt 3: success -> OK: fetched 42 records

The cap is not optional. Without max_attempts a tool that is genuinely broken (bad credentials, a deleted resource) turns your agent into an infinite loop. Feeding the error text back matters too: a good model reads “invalid date format” and fixes its argument on the next try instead of repeating the same mistake.

Retries are not a fix for a bad toolRetrying masks transient failures (a blip, a rate limit). It does not fix a tool that is wrong every time — that just wastes attempts. Distinguish retryable errors (timeouts, 429s) from terminal ones (auth failure, not-found) and give up immediately on the terminal kind. Tool design that surfaces clear, machine-readable errors is covered in 4.3.

7 · Which pattern when

The most common mistake with these patterns isn't misusing one — it's reaching for the fanciest one by reflex. Engineers who've just learned ReAct wrap every task in a reasoning loop; teams enamoured with reflection critique-and-revise a one-line lookup. It feels sophisticated and it's almost always wrong. The best agent for a job is the one with the least structure that still makes it reliable — every extra pattern is more model calls, more latency, and more surface area for something to go sideways.

The patterns are not ranked — each fits a shape of problem. Reach for the simplest one that covers the task, and compose them only when the task genuinely needs it.

Here's a way to hold them all in your head. Match the pattern to what the task looks like, not to what's impressive. If one tool answers the question, you don't need a pattern at all — just call it (that's the basic loop from 4.1). If the next step depends on the last result, you need to adapt as you go: that's ReAct. If the path is known and repeatable, decide it once: plan-and-execute. If getting it wrong is expensive, add a quality gate: reflection. If the requests come in different flavours, sort them at the door: routing. And whenever any of these touches a flaky tool, wrap that call in retries — that one is orthogonal, a seatbelt you can add to any of the others.

The last idea is the one that turns a list of patterns into real engineering: they compose. A production agent is rarely “a ReAct agent” or “a routing agent” — it's a stack of patterns, each earning its place. A support system might route to a billing sub-agent, which runs a plan, drops into ReAct for the one open-ended step, reflects before it commits a refund, and retries every tool call underneath. Each layer is there because that part of the task demanded it, not because the pattern was available. The judgment you're building is knowing which layers a given task actually needs — and, just as importantly, which it doesn't.

The task what shape? one tool answers it → basic loop (4.1) next step depends on last → ReAct path known + repeatable → plan-and-execute correctness > speed → reflection many kinds of request → routing retries wrap any Match the pattern to the task's shape. Read down the branches, pick the simplest one that fits; retries sits off to the side because it's orthogonal — a seatbelt you wrap around any tool call, under any pattern.
🗺️ How to read this diagram

Start at the blue The task box on the left and follow the branch whose label matches what your task actually looks like — each arrow leads to the pattern to reach for.

  • The four branches fan out from the task by its shape: one tool suffices → the plain basic loop; each step depends on the last → ReAct; a known repeatable path → plan-and-execute; correctness outranks speed → reflection; many kinds of request → routing.
  • The green box is the “no pattern needed” case — reach for it first; the purple boxes are the four named patterns, ordered by increasing structure.
  • The red retries box sits apart, past the dashed line, because it is orthogonal: it wraps any tool call under any of the patterns rather than being an alternative to them.

In short: Pick the least structure that makes the task reliable — basic loop first, then the one pattern that fits its shape — and wrap flaky tool calls in retries regardless of which you chose.

SituationReach forWhy
One tool answers itBasic loop (4.1)No structure needed — call the tool, return the result.
Multi-step reasoning, next step depends on the lastReActInterleaving thought/action/observation lets the agent adapt to each result.
A known, repeatable workflowPlan-and-executeCommitting to the path upfront is cheaper and more predictable than deciding each step.
Correctness matters more than speedReflectionA critique-and-revise pass catches mistakes a single shot would ship.
Many different kinds of requestRoutingClassify first so each handler stays small, focused, and independently testable.
Any pattern touching flaky toolsRetries (wrap the tool)Catch, feed the error back, retry with a cap — orthogonal to the pattern above it.
They stackThese are not either/or. A support agent might route to a billing sub-agent, which runs a plan, executes each step with ReAct when a step is open-ended, reflects before sending a refund, and wraps every tool call in retries. The skill is choosing the least structure that makes the task reliable.
✓ Knowledge check

You are building an agent for a fixed nightly report with the same three steps every time. ReAct or plan-and-execute — and why?

Show answer
Plan-and-execute. The workflow is known and repeatable, so committing to the ordered plan upfront is cheaper (fewer model calls) and more predictable than re-deciding the next step after every observation. ReAct's adapt-as-you-go strength is wasted when the path never changes.
✓ Knowledge check

Why must a reflection loop and a retry loop both have a hard cap?

Show answer
Because both can run forever otherwise. A critic that is never satisfied keeps requesting revisions; a tool that is genuinely broken keeps raising the same error. A max_rounds / max_attempts cap guarantees the agent terminates and stops burning tokens and money on a task it cannot complete.

🪜 Practice — from one pattern to a composed agent beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Add a third fact to ReActBeginner

Extend react.py so the goal is the combined population of Paris, Lyon, and a third city. Add the city to the population tool and a matching branch in fake_model.

Show solution Add e.g. 'Nice': 340_000 to the tool dict, then add an if len(obs) == 2 branch in fake_model that returns a Thought+Action for Nice, and shift the finish branch to len(obs) == 3. The loop and everything else stay the same.
Exercise 2 · Make the plan re-planIntermediate

In plan_execute.py, make sum_orders fail on an empty list, and have execute call the planner again to insert a fetch_orders retry step before continuing.

Show solution Catch the failure inside the execute loop; on failure, call fake_planner again (or splice a recovery step into the remaining plan) and resume. This is the re-plan hybrid — plan-and-execute borrowing ReAct's adaptability.
Exercise 3 · Critique against real requirementsIntermediate

Change reflection.py so the critic checks the actual REQUIRED list (cause, fix) by mapping each requirement to a keyword, instead of hard-coding the two checks.

Show solution Build a dict like {'cause': 'queue', 'fix': 'scale'} and loop over REQUIRED, appending any requirement whose keyword is absent. Now adding a new requirement is a one-line change.
Exercise 4 · Route to a real sub-flowAdvanced

In router.py, make the bug handler run the ReAct loop from section 2 (or a small version of it) instead of returning a canned string, so routing dispatches to an actual pattern.

Show solution Import or inline a mini ReAct loop and call it from the bug handler. This is the real shape of composition: the router picks the sub-agent, and the sub-agent runs its own pattern with its own tools.
Exercise 5 · Distinguish retryable from terminal errorsExpert

Extend retries.py with a second exception type TerminalError (e.g. auth failure) that is not retried. Show that a terminal error gives up immediately while a transient one still retries.

Show solution Add class TerminalError(Exception), catch it in its own except that returns/raises without retrying, and keep the ToolError branch as the retryable path. Retrying an auth failure just wastes attempts — fail fast.
Exercise 6 · Compose all fourProfessional

Sketch (in code or clear pseudocode) a single agent that routes a request, runs a plan for one branch, uses ReAct for an open-ended step, and reflects before returning — with every tool call wrapped in retries. Explain where each pattern earns its keep.

Show solution Route at the top; each branch is a handler. A known branch calls execute(plan); an open-ended branch calls react(); wrap the risky output in a reflect() gate; wrap every tool in call_with_retries. Justify each: routing keeps handlers small, plan for predictability, ReAct for adaptability, reflection for quality, retries for resilience.

Context: A teammate has an agent stuck in a loop that keeps calling the same failing tool and never returns, and another that ships low-quality answers on important requests.

Your task: Write a short note (5–8 sentences) recommending which pattern(s) to apply to each problem and what control-flow change fixes it.

Requirements:

  • For the looping agent, name capped retries and the distinction between retryable and terminal errors.
  • For the quality problem, name reflection (draft → critique → revise) with a bounded max_rounds.
  • State clearly that the fake decider in these labs becomes a real model call and the control flow is unchanged.
  • Note that patterns compose, so both fixes can live in one agent.

💡 Hint: You don't need production code — this is about matching a symptom to the right pattern. Robust tool design and observability come in 4.3 and 4.4.

✓ Checkpoint — you can move on when you can…

  • Explain why the basic loop (4.1) needs a reasoning pattern layered on top.
  • Implement ReAct — Thought → Action → Observation, repeating until an answer, bounded by max steps.
  • Implement plan-and-execute and state exactly how it differs from ReAct (plan upfront vs decide-as-you-go).
  • Add a reflection loop (draft → critique → revise) with a hard cap on rounds.
  • Build a router that classifies a request and dispatches to a focused handler.
  • Wrap a tool in capped retries that feed the error back, and tell retryable from terminal failures.
  • Choose the least structure that makes a given task reliable — and compose patterns when it genuinely needs it.