Lesson 5 - Guided Project: A Planning, Reasoning Atlas
On this page
- Welcome to the Guided Project
- The Shape: Three Patterns, One Run
- Stage 1: Decompose the Request
- Stage 2: Execute Each Step — with ReAct Where It Needs Facts
- Stage 3: Draft the Itinerary
- Stage 4: Reflect and Revise
- Stage 5: Wire It Together
- The Verified Run
- Practice Exercises
- Summary
- Next Steps
- Continue Building Your Skills
Welcome to the Guided Project
Across this module you built the three reasoning patterns one at a time: decomposition (Lesson 2) plans the steps before acting, ReAct (Lesson 3) reasons and acts in an adaptive loop, and reflection (Lesson 4) critiques and revises after a draft. Each was a small, self-contained orchestration on top of the agent loop you already had. Now you’ll put all three on a single agent and watch them work together. Atlas — the travel planner you’ve been building all course — is going to take one genuinely hard request:
“Plan a 3-day autumn trip to Kyoto for a vegetarian traveler, staying under $100/day.”
That request has interacting constraints — season, budget, and a dietary restriction — exactly the kind of multi-step problem a bare loop wanders on. By the end, Atlas will decompose it into ordered steps, reason and act through the step that needs real cost data, draft an itinerary, then reflect on that draft and revise it when it catches a missed constraint. Three patterns, one run, verified end to end.
By the end of this project, you will be able to:
- Compose decomposition, ReAct, and reflection into one agent without rewriting any of them
- Decompose a constrained request into ordered steps, then execute each in turn
- Use a ReAct sub-step to ground a step in a real tool result
- Reflect on the finished draft and revise it until it satisfies every constraint
We’ll build it in stages, reusing the exact functions you wrote earlier in the module. Let’s give Atlas all three.
The Shape: Three Patterns, One Run
Before any code, picture how the patterns layer. They aren’t rivals — they sit at different moments of the task: decomposition runs first (plan the steps), ReAct runs during each step that needs facts (reason → act → observe), and reflection runs last (check and fix the result). Stack them and you get one pipeline:
- Decompose the request into an ordered list of steps.
- Execute each step — using a small ReAct loop for steps that need tool results, so the plan is grounded in real data, not guesses.
- Draft the itinerary from what the steps found.
- Reflect: critique the draft against the traveler’s constraints and revise until it passes.
Each stage is a function you’ve already written. The guided project is mostly wiring, which is the point: once you have the patterns, combining them is straightforward.
Stage 1: Decompose the Request
Start with the planning pass from Lesson 2. Atlas asks the model to turn the goal into a short, ordered list of steps — nothing is done yet, it’s just deciding what to do.
def decompose(client, goal, *, system, model="claude-haiku-4-5"):
r = client.messages.create(
model=model, max_tokens=512, system=system,
messages=[{"role": "user",
"content": f"Break this goal into 2-5 short ordered steps, "
f"one per line, no numbering:\n{goal}"}])
txt = "".join(b.text for b in r.content if b.type == "text")
return [ln.strip("-• ").strip() for ln in txt.splitlines() if ln.strip()]For our request, the plan comes back as three steps: confirm the season fits, check daily costs against the budget, then draft the itinerary. Notice the order matters — you want to know the budget is workable before you commit to an itinerary, and Atlas planned it that way.
Stage 2: Execute Each Step — with ReAct Where It Needs Facts
Some steps are pure reasoning (“does autumn in Kyoto fit?”), but one — “check daily costs against the budget” — needs a fact Atlas doesn’t have. That’s where ReAct earns its place: for a fact-dependent step, Atlas reasons about what it needs, calls a tool, observes the result, and concludes from it. This is the Lesson 3 loop, scoped to a single step:
def react_step(client, step, *, system, tools, tool_functions,
model="claude-haiku-4-5", max_steps=4):
messages = [{"role": "user", "content": f"Carry out this step: {step}"}]
for _ in range(max_steps):
response = client.messages.create(
model=model, max_tokens=512, system=system, tools=tools, messages=messages)
messages.append({"role": "assistant", "content": response.content})
thought = "".join(b.text for b in response.content if b.type == "text").strip()
if response.stop_reason != "tool_use":
return thought
results = []
for block in response.content:
if block.type != "tool_use":
continue
obs = str(tool_functions[block.name](**block.input))
print(f" [ReAct] {block.name}({block.input}) -> {obs}")
results.append({"type": "tool_result", "tool_use_id": block.id, "content": obs})
messages.append({"role": "user", "content": results})
return "Stopped: step limit."It’s the same reason-act-observe loop from Lesson 3 — when the step needs no tool, the model just returns its thought and the loop exits on the first turn; when it does, the model calls the tool, sees the observation, and reasons again. Here’s the simple cost tool the budget step uses:
COSTS = {"Kyoto": "hostel ~$35/night, meals ~$25/day"}
def lookup_cost(city): return COSTS.get(city, "unknown")
tools = [{"name": "lookup_cost", "description": "Typical daily costs for a city",
"input_schema": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}}]Stage 3: Draft the Itinerary
With the steps executed and their findings collected, Atlas writes a first draft. This is a plain model call over everything the steps turned up:
notes = "; ".join(f"{s}: {r}" for s, r in findings)
dr = client.messages.create(
model="claude-haiku-4-5", max_tokens=512, system=system,
messages=[{"role": "user",
"content": f"Using these findings, write the itinerary:\n{notes}"}])
draft = "".join(b.text for b in dr.content if b.type == "text")The draft will be plausible — three days, real Kyoto sights, within budget. But plausible isn’t the same as correct, and a drafting pass focused on “write a nice itinerary” can quietly drop a constraint. That’s exactly what reflection is for.
Stage 4: Reflect and Revise
The last stage is the Lesson 4 reflect-and-revise loop. Atlas critiques its own draft against the original request; if the critique returns exactly OK it ships, otherwise it revises using the critique and checks again — bounded by max_revisions so it can’t spin forever:
def reflect_revise(client, task, draft, *, system, model="claude-haiku-4-5", max_revisions=2):
for rnd in range(1, max_revisions + 1):
c = client.messages.create(
model=model, max_tokens=256, system=system,
messages=[{"role": "user",
"content": f"Task:\n{task}\n\nDraft:\n{draft}\n\n"
f"Critique against the task. If it satisfies every "
f"constraint reply exactly OK, else list fixes."}])
critique = "".join(b.text for b in c.content if b.type == "text").strip()
print(f"Critique {rnd}: {critique}")
if critique == "OK":
return {"final": draft, "revisions": rnd - 1}
r = client.messages.create(
model=model, max_tokens=512, system=system,
messages=[{"role": "user",
"content": f"Task:\n{task}\n\nDraft:\n{draft}\n\n"
f"Critique:\n{critique}\n\nRewrite to fix it."}])
draft = "".join(b.text for b in r.content if b.type == "text")
print(f"Revision {rnd}: {draft}")
return {"final": draft, "revisions": max_revisions}This is the stage that catches the trap. The draft put sushi and wagyu dinners on the itinerary — fine on budget and season, but the traveler is vegetarian. The critique pass, looking at the draft against the task rather than trying to write a nice plan, spots the miss and Atlas revises.
Stage 5: Wire It Together
Now the whole agent. plan_trip is just the four stages in sequence — decompose, execute each step with ReAct, draft, reflect — each one a function you’ve already built:
def plan_trip(client, request, *, system, tools, tool_functions):
steps = decompose(client, request, system=system) # 1. plan
findings = []
for step in steps: # 2. execute
result = react_step(client, step, system=system,
tools=tools, tool_functions=tool_functions)
findings.append((step, result))
notes = "; ".join(f"{s}: {r}" for s, r in findings) # 3. draft
dr = client.messages.create(
model="claude-haiku-4-5", max_tokens=512, system=system,
messages=[{"role": "user",
"content": f"Using these findings, write the itinerary:\n{notes}"}])
draft = "".join(b.text for b in dr.content if b.type == "text")
return reflect_revise(client, request, draft, system=system) # 4. reflectAnd the call that runs it:
request = ("Plan a 3-day autumn trip to Kyoto for a vegetarian traveler, "
"staying under $100/day.")
system = "You are Atlas. Plan, reason before tool calls, and check your work."
out = plan_trip(client, request, system=system,
tools=tools, tool_functions={"lookup_cost": lookup_cost})
print(out["final"])The Verified Run
Here is the full run, verified end to end against the agent loop you built (printing added at each stage so you can watch the patterns fire):
== Decompose ==
Step 1: Confirm season and weather fit
Step 2: Check daily costs against the budget
Step 3: Draft a 3-day itinerary
== Execute steps (ReAct where a step needs facts) ==
-> Autumn in Kyoto is mild with fall foliage — a good fit.
[ReAct] lookup_cost({'city': 'Kyoto'}) -> hostel ~$35/night, meals ~$25/day
-> ~$60/day before sights — comfortably under $100/day.
-> Day 1 temples, Day 2 Arashiyama bamboo grove, Day 3 Nishiki Market.
== Draft itinerary ==
Draft: Day 1: temples, sushi dinner. Day 2: Arashiyama, wagyu dinner. Day 3: Nishiki Market, ramen.
== Reflect & revise against constraints ==
Critique 1: Budget and season are fine, but the traveler is vegetarian and the sushi and wagyu dinners are not. Swap them for vegetarian meals.
Revision 1: Day 1: temples, vegetarian kaiseki. Day 2: Arashiyama, tofu hot pot. Day 3: Nishiki Market, vegetarian ramen. All under $100/day.
Critique 2: OK
Final (1 revision): Day 1: temples, vegetarian kaiseki. Day 2: Arashiyama, tofu hot pot. Day 3: Nishiki Market, vegetarian ramen. All under $100/day.Watch each pattern do its job. Decomposition turned one hard request into three ordered steps. ReAct grounded the budget step in a real cost lookup — $60/day, under the cap — instead of guessing. The draft was plausible but slipped a non-vegetarian dinner past the constraint. And reflection caught it: the critique compared the draft to the task, flagged the sushi and wagyu, and the revision fixed them. Without reflection, Atlas would have confidently handed a vegetarian traveler a wagyu dinner. With all three patterns, it planned, grounded, and corrected itself.
This is the shape of a capable agent
Real agents rarely rely on a single pattern. They decompose a hard task so they don’t have to solve it all at once, reason and act through the steps that need real-world facts so the plan is grounded rather than guessed, and reflect on the result so a missed constraint gets caught before the user sees it. Each pattern is cheap to add — a prompt and a little wiring on top of the loop you already have — and together they turn “works on easy questions” into “handles genuinely hard, constrained tasks.” That’s the jump from a demo to an agent you’d actually rely on.
Practice Exercises
Exercise 1: Make reflection check the budget too
The critique caught the vegetarian miss, but what if a revision pushed a day over budget? Right now the budget only gets checked during the ReAct step, not in reflection. How would you make the critique enforce the $100/day cap as well?
Hint
The critique prompt already says “critique against the task,” and the task contains the budget — so the model can check it in principle. To make it reliable, spell the constraints out: change the critique prompt to “Check this draft against every constraint: vegetarian meals, under $100/day, autumn, 3 days. Reply exactly OK only if all hold, else list each violation.” Being explicit about the checklist makes the reflection pass far less likely to skip a constraint.
Exercise 2: Let a step re-plan
Decomposition commits to its plan up front. Suppose the cost lookup had come back over budget — the remaining “draft the itinerary” step is now planning a trip the traveler can’t afford. How could you let Atlas re-decompose when a step’s result invalidates the plan?
Hint
After each step, add a check: if the result contradicts a constraint (e.g. the cost exceeds the budget), call decompose again with the new information folded into the goal — “…but Kyoto is over budget, so suggest a cheaper alternative.” This is decomposition and ReAct’s adaptivity combined: plan up front, but re-plan when an observation breaks the plan. It’s exactly the gap between plan-then-execute and ReAct that Lessons 2 and 3 contrasted.
Exercise 3: Count the cost
This run made nine model calls — one to decompose, several to execute the steps, one to draft, and a critique/revise/critique for reflection. That’s a lot more than a bare loop. When is composing all three patterns worth it, and when would you drop one?
Hint
Each pattern buys reliability at the cost of calls and latency. For a simple, single-fact request, all three are overkill — a bare loop answers it in one call. Reach for decomposition when the task has interdependent steps, ReAct when steps need real-world facts, and reflection when the output has hard constraints that are expensive to get wrong (a dietary restriction, a budget, a legal requirement). For a low-stakes draft you might keep decomposition and ReAct but skip reflection; for a high-stakes constrained plan, the extra critique pass is cheap insurance.
Summary
You composed all three of this module’s patterns onto one agent and watched them work together on a genuinely hard, constrained request. Atlas decomposed the trip into ordered steps, used a ReAct sub-loop to ground the budget step in a real cost lookup, drafted an itinerary, and then reflected on that draft — catching a non-vegetarian dinner the drafting pass had let slip, and revising until the critique returned OK. The whole pipeline is just the four functions from Lessons 2 through 4 wired in sequence, running on the agent loop you built back in Module 2. The verified run shows the payoff plainly: a plan that is planned, grounded in facts, and self-corrected before the traveler ever sees it.
Key Concepts
- Patterns layer, they don’t compete — decompose before, ReAct during, reflect after; stack them into one pipeline.
- ReAct grounds the fact-dependent steps — a tool call inside a step replaces a guess with data.
- Reflection catches dropped constraints — a critique pass against the task finds misses the drafting pass glossed over.
- Composition is mostly wiring — once you have the patterns, combining them is sequencing functions, not new machinery.
Why This Matters
This is what a capable agent actually looks like: not one clever trick, but a few cheap reasoning structures layered onto a plain loop. Decomposition keeps hard tasks tractable, ReAct keeps the work grounded in reality, and reflection keeps the output honest about its constraints. You now know how to add each one and — just as important — when each is worth its cost. That judgment, more than any framework, is what separates an agent that demos well from one you’d put in front of real users with real constraints.
Next Steps
Back to Module Overview
Return to the Planning and Reasoning module overview
Back to Course Home
Return to the Building AI Agents in Python course overview
Continue Building Your Skills
Atlas can now take a hard, constrained request and plan it the way a careful human would — break it down, look things up, draft, then check the draft against what was actually asked and fix what’s wrong. You’ve built every piece from the agent loop up: tools, validation, memory, and now reasoning structure. With planning and reasoning in place, your agent is no longer just reacting one step at a time — it’s deliberating. That’s the foundation everything more advanced builds on.