Agentic AI

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.

JrCodex·7 min read

Jr Codex Agentic AI Notes

Level: Intermediate Prerequisites: Module 3, Chapter 2: Short-Term Memory Time to complete: ~20 minutes


Table of Contents

  1. Why Decompose at All
  2. The Plan as Data
  3. Generating a Plan
  4. Dependencies and Execution Order
  5. How Far to Decompose
  6. 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.parsed
Two 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 plan
Failure 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

  1. In the competitor-research failure, no individual step was wrong. What exactly was missing?
  2. Why do LLM planners default to linear dependency chains, and what does that cost?
  3. 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