Planning And Self Correction
Task Decomposition
A ReAct agent decides one step at a time. That works well up to a point and then stops working, for a specific reason.
Jr Codex Agentic AI Notes
Level: Intermediate Prerequisites: Module 3, Chapter 2: Short-Term Memory Time to complete: ~20 minutes
Table of Contents
- Why Decompose at All
- The Plan as Data
- Generating a Plan
- Dependencies and Execution Order
- How Far to Decompose
- Summary & Next Steps
1. Why Decompose at All
A ReAct agent decides one step at a time. That works well up to a point and then stops working, for a specific reason.
The Failure Without a Plan
─────────────────────────────────────────
Task: "Compare our top three competitors' pricing
and write a summary for the board."
A step-at-a-time agent:
Step 1 searches for competitors ✓
Step 2 researches competitor A deeply ✓
Step 3 keeps researching A ✓
Step 4 more on A's enterprise tier ✓
...
Step 12 context is full, B and C were
never touched, no summary exists
Nothing went WRONG at any individual step. Every
choice was locally reasonable. The task failed
because nothing tracked the SHAPE of the whole job.
─────────────────────────────────────────
What a Plan Buys
─────────────────────────────────────────
COVERAGE all three competitors are on the
list, so none is silently skipped
PROGRESS "2 of 5 done" is measurable — you
can detect stalling
BUDGETING steps and cost can be allocated per
sub-task rather than consumed
first-come
PARALLELISM independent sub-tasks can run
concurrently (Section 4)
RECOVERY a failed sub-task is retried alone,
not the whole run
─────────────────────────────────────────
2. The Plan as Data
The central design decision: the plan is a structured object your code owns, not prose the model holds in context.
from pydantic import BaseModel, Field
from typing import Literal
class Step(BaseModel):
id: str
description: str = Field(description="One concrete action, verifiable as done or not")
depends_on: list[str] = Field(default_factory=list)
status: Literal["pending", "running", "done", "failed", "skipped"] = "pending"
result: str | None = None
attempts: int = 0
class Plan(BaseModel):
goal: str
steps: list[Step]
def ready(self) -> list[Step]:
"""Steps whose dependencies are all satisfied."""
done = {s.id for s in self.steps if s.status == "done"}
return [s for s in self.steps
if s.status == "pending" and set(s.depends_on) <= done]
def is_complete(self) -> bool:
return all(s.status in ("done", "skipped") for s in self.steps)
def is_stuck(self) -> bool:
"""Nothing running, nothing ready, and not finished — a real state, not a bug."""
return (not self.is_complete()
and not self.ready()
and not any(s.status == "running" for s in self.steps))Why Structured Rather Than Prose
─────────────────────────────────────────
A prose plan in the context DRIFTS — it scrolls
into the low-attention middle and the model
gradually reinterprets it (Module 2, Chapter 1).
A structured plan is owned by your CODE:
- progress is computed, not claimed
- `is_stuck()` is detectable rather than
something you notice from a bad output
- it survives a restart (Module 6, Chapter 2)
- it renders compactly into the scratchpad every
step (Module 3, Chapter 2)
─────────────────────────────────────────
3. Generating a Plan
PLANNER_PROMPT = """Break this goal into concrete steps.
Rules:
- Each step must be ONE action, checkable as done or not done.
- Use depends_on ONLY for genuine ordering constraints. Steps that
could run at the same time must NOT depend on each other.
- 3-8 steps. If you need more, the steps are too small.
- Do not include "review the results" or "summarise" as separate steps
unless the output is a deliverable in its own right.
Available tools:
{tools}
GOAL: {goal}"""
def make_plan(client, goal, tools) -> Plan:
return client.chat.completions.parse(
model="gpt-4o",
messages=[{"role": "user", "content": PLANNER_PROMPT.format(
goal=goal, tools=render_tool_summaries(tools))}],
response_format=Plan, temperature=0,
).choices[0].message.parsedTwo Rules Doing the Heavy Lifting
─────────────────────────────────────────
"USE depends_on ONLY FOR GENUINE CONSTRAINTS"
Models default to a linear chain where every
step depends on the previous one. That destroys
all parallelism for no reason. Saying so
explicitly recovers it.
"SHOW THE AVAILABLE TOOLS"
A planner that cannot see the tools produces
steps the executor cannot perform — the most
common cause of an immediately-stuck plan.
─────────────────────────────────────────
4. Dependencies and Execution Order
Modelling dependencies as a graph rather than a list is what makes parallelism and partial recovery possible.
The Same Task, Two Shapes
─────────────────────────────────────────
LINEAR (what a model produces by default)
s1 ──► s2 ──► s3 ──► s4 ──► s5
5 sequential steps. s2 waits on s1 for no reason.
GRAPH (what it should be)
s1 ──┬──► s2 ──┐
├──► s3 ──┼──► s5
└──► s4 ──┘
s2, s3, s4 are INDEPENDENT — run them together.
Depth 3 instead of 5, and a failure in s3 does
not block s2 or s4.
─────────────────────────────────────────
import asyncio
async def execute_plan(agent, plan: Plan, max_parallel=3):
sem = asyncio.Semaphore(max_parallel)
async def run_step(step: Step):
async with sem:
step.status = "running"
step.attempts += 1
try:
step.result = await agent.run_subtask(step.description, plan)
step.status = "done"
except Exception as e:
step.status = "failed"
step.result = f"{type(e).__name__}: {e}"
while not plan.is_complete():
batch = plan.ready()
if not batch:
if plan.is_stuck():
return handle_stuck(plan) # re-plan or escalate — Chapter 2
await asyncio.sleep(0.1) # something is still running
continue
await asyncio.gather(*(run_step(s) for s in batch)) # the READY set, together
return planFailure Is Localised
─────────────────────────────────────────
When s3 fails, s2 and s4 have already succeeded
and keep their results. Only s3 retries, and only
s5 is blocked.
Compare a linear chain, where a failure at step 3
discards steps 4 and 5 and often the whole run.
This alone justifies the graph.
─────────────────────────────────────────
5. How Far to Decompose
Both directions fail, and the failures look different.
TOO COARSE
─────────────────────────────────────────
"Research all competitors"
One step that is really twelve. No visible
progress, no parallelism, and if it fails you
learn nothing about where.
TOO FINE
─────────────────────────────────────────
"Open the browser" · "Type the query" ·
"Read the first result" · "Note the price"
Planning overhead swamps the work. The plan itself
fills the context. And you have re-implemented the
agent loop badly — this is what the ReAct step is
FOR.
THE RIGHT GRAIN
─────────────────────────────────────────
One step = one thing an agent can do in a SHORT
ReAct loop of roughly 2-5 tool calls.
✓ "Find competitor A's published pricing tiers"
✓ "Summarise the three pricing models found"
Each is verifiable, independently retryable, and
large enough that planning it was worth doing.
─────────────────────────────────────────
The Two-Level Structure
─────────────────────────────────────────
PLAN LEVEL what must happen, and in what order
(this chapter)
STEP LEVEL a short ReAct loop that figures out
HOW (Module 2, Chapter 2)
The plan handles the long horizon the model is bad
at. The ReAct loop handles the short horizon it is
good at. Each layer does what it is suited to —
which is the whole point of decomposition.
─────────────────────────────────────────
6. Summary & Next Steps
Key Takeaways
- Step-at-a-time agents fail on multi-part tasks not through bad individual decisions but because nothing tracks the shape of the whole job.
- The plan should be a structured object owned by your code, so progress is computed rather than claimed and stuck states are detectable.
- Model dependencies as a graph, not a chain — this recovers parallelism and localises failure to the step that failed.
- Decompose to steps a short ReAct loop can complete: the plan covers the long horizon the model is weak at, the loop covers the short horizon it is strong at.
Concept Check
- In the competitor-research failure, no individual step was wrong. What exactly was missing?
- Why do LLM planners default to linear dependency chains, and what does that cost?
- What are the two distinct failure modes of decomposing too finely?
Next Chapter
→ Chapter 2: Planning Patterns
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Agentic AI Index