Module 9 of 12 · Open Source Track
Planning & Task Decomposition

Teach your agent to think before it acts. Five concepts — why complex tasks need a plan, routing requests before you plan, breaking goals into sub-tasks, running them as a dependency graph, and picking the right tools per step.

Track 4: Agent Architectures ⏱ ~16 min read 9 / 12
Concept 1 of 5

Plan the whole goal before acting

A raw ReAct loop is great for 2–3 tool calls, but on a big goal — "research 3 competitors, compare pricing, draft a report" — it makes locally sensible but globally messy choices, backtracks, and loses the thread.

Planning is a layer on top of ReAct: the agent first analyzes the entire goal and produces a structured plan of sub-tasks with dependencies, then executes it. On benchmarks, ReAct-only completes complex tasks ~40% of the time; adding a planning step pushes that to ~80%.

Concept 1 of 5

Build IKEA without the instructions

BEFORE: Imagine assembling IKEA furniture without reading the manual — you open the box, see 47 pieces, and just start connecting things that look like they fit.

PAIN: You attach a panel upside down, realize at step 15 you skipped a bracket at step 3, and end up with leftover screws. That's a ReAct-only agent: each move is locally reasonable, the whole build is a mess, because it never made a picture of the finished thing.

MAPPING: Planning is reading the instructions first. The agent breaks the goal into ordered phases, marks what depends on what, then follows the plan tracking progress — turning "try things and hope" into "understand, plan, execute."

Concept 1 of 5

Planning sits on top of ReAct

  1. Analyze the goalRead the full request and decide it's complex enough to need a structured plan.
  2. Generate the planOne model call produces a list of sub-tasks, each with its dependencies — the "what to do".
  3. Execute per sub-taskReAct handles the "how" of each leaf task, one at a time or in parallel.
  4. Adapt if neededA plan is a hypothesis: if a sub-task fails or surprises you, re-plan rather than force the old plan.
Concept 1 of 5

Pseudocode

FUNCTION run(goal):
  IF is_simple(goal):
    RETURN answer_directly(goal)

  plan = model.make_plan(goal)   # sub-tasks + deps

  FOR task IN plan.ready_tasks():
    result = react_execute(task)
    plan.mark_done(task, result)
    IF result.failed:
      plan = re_plan(goal, plan)  # adapt

  RETURN synthesize(plan.results)
Concept 1 of 5

Misconceptions

"Every request needs a plan."
No. "What's the weather?" or a single tool call gets slower with planning overhead. Route simple requests straight through — that's the next concept.
"The plan must be perfect before execution."
Plans are hypotheses, not contracts. Rigid plan-following is as bad as no plan — plan upfront, then re-plan when a sub-task surprises you.

Planning is a layer on top of ReAct: analyze the goal, build a sub-task plan, then execute it. It roughly doubles completion rates on complex tasks.

The plan says "what"; ReAct handles "how". Adapt the plan instead of clinging to it.

Concept 2 of 5

Route before you plan

Intent classification is a lightweight first pass that decides what kind of request this is and whether it even needs planning. One model call returns an intent label, a complexity score, and the key entities.

Based on that, your code routes: a simple question goes straight to a direct answer, a single-step task goes to a ReAct loop, and a complex multi-step task triggers the full planning pipeline. It's a router, not a filter — nothing is blocked, everything just takes the cheapest correct path.

Concept 2 of 5

The ER triage nurse

BEFORE: Imagine a hospital where every patient — paper cut or chest pain — gets the same full workup: blood tests, MRI, specialist consult.

PAIN: The paper-cut patient wastes hours and thousands of dollars, while the chest-pain patient waits behind them. Without routing, "What's 2+2?" runs through decomposition, DAG creation, and tool discovery — burning tokens and adding seconds for a 200ms task.

MAPPING: The classifier is the triage nurse: paper cut → band-aid station (direct answer), sprained ankle → minor procedure (single tool call), chest pain → full workup (planning pipeline). Each request gets the right level of care.

Concept 2 of 5

One call, three fields, one route

  1. Classify intentLabel the request: simple_question, single_tool, multi_step_task, or creative_generation.
  2. Score complexityRate it low / medium / high, and extract the key entities from the text.
  3. Decide needs_planningCombine intent + complexity into a yes/no on whether to plan.
  4. Route the requestSend it down the matching path — direct answer, ReAct, or full planning.
Concept 2 of 5

Which path does this take?

IF intent = simple_question:
  → answer directly (one call)

IF intent = single_tool:
  → run a short ReAct loop

IF intent = multi_step_task
   OR complexity = high:
  → full planning pipeline

IF parsing / intent is unclear:
  → default needs_planning = TRUE

Over-planning a simple task wastes a few hundred tokens; under-planning a complex one fails the whole task. So when in doubt, plan.

Concept 2 of 5

Misconceptions

"Classification must be 100% accurate."
It doesn't. A false positive (unnecessary planning) costs a few hundred tokens; a false negative (skipped planning) fails the task. Bias the fallback toward planning.
"More intent categories = better routing."
More categories raise misclassification risk. Start with 3–4 broad intents and split them later once you see which requests actually fail.

Intent classification is a cheap router that decides whether to plan at all. It saves the planning tax on the 60–70% of requests that are simple.

Keep categories few, and default to planning when unsure — false negatives are the expensive ones.

Concept 3 of 5

Break the goal into sub-tasks

Task decomposition uses the model to turn a vague goal into a hierarchy of concrete sub-tasks, each with explicit dependencies. Each leaf task should be small enough to finish in 1–2 tool calls.

Three strategies: top-down (split the goal into phases, then phases into actions), bottom-up (start from atomic actions and group them), and iterative (plan only the next step, useful when later steps depend on earlier results). Top-down is the common default.

Concept 3 of 5

The project manager who breaks it down

BEFORE: A manager gets "Launch the new website" and just starts — no breakdown, no timeline. They bounce between copy, DNS, and the homepage, never finishing anything.

PAIN: An agent handed a raw complex goal has the same problem: it can't tell where to start, how to track progress, or when it's done. It makes headway on one aspect, then loses that context switching to another.

MAPPING: A good manager splits "launch" into concrete tasks — finalize mockups, migrate content, configure DNS, load-test, go-live — each specific with clear inputs and outputs. Decomposition does the same: the model generates a tree where each leaf is a 1–2 tool-call action.

Concept 3 of 5

Goal to task tree

  1. Split into phasesThe model breaks the goal into 3–5 high-level phases.
  2. Expand each phaseDecompose phases into specific leaf actions (one per competitor, etc.).
  3. Add dependenciesMark which tasks must finish before others can start.
  4. Assign toolsTag each leaf with the tools it will need (search, extract, compare).
Concept 3 of 5

Pseudocode

plan = model.decompose(goal)

# plan is a list of tasks, each:
#   id, description,
#   dependencies: [other ids],
#   tools: [tool names]

FOR task IN plan:
  assert task.is_atomic()   # 1-2 tool calls
  assert task.deps_exist()  # no dangling ids

# aim for 5-10 leaf tasks total —
# too many = coordination overhead.
Concept 3 of 5

Misconceptions

"Finer decomposition is always better."
No. 5–10 sub-tasks succeed ~85% of the time; 15+ ultra-fine ones drop to ~60% as coordination overhead dominates. Aim for 1–2 tool-call leaves.
"Any breakdown covers the goal."
Too-narrow tasks leave coverage gaps; too-broad ones overwhelm a single step. The leaves together must cover the entire original goal.

Decomposition turns a goal into a hierarchy of atomic, dependency-linked sub-tasks. Right granularity is a 1–2 tool-call leaf.

Target 5–10 leaves that collectively cover the whole goal — not 15+ fragments.

Concept 4 of 5

Run sub-tasks as a dependency graph

Sub-tasks aren't all sequential — researching three competitors can happen at once, but the report can't start until all research is done. A DAG (Directed Acyclic Graph) captures exactly that: nodes are tasks, edges are dependencies (A→B means A must finish first).

The executor works in waves: start every task with no unmet dependencies in parallel; as each finishes, release the tasks it unblocked. "Acyclic" means no loops — a cycle (A needs B, B needs A) would deadlock, so you validate the graph before running it.

Concept 4 of 5

Cooking a multi-dish dinner

BEFORE: Imagine cooking roast chicken, mashed potatoes, salad, and dessert strictly one step at a time — peel potatoes, then wait; chop salad, then wait; preheat oven, then wait. Total: 3 hours.

PAIN: Sequential execution wastes time on independent work. If Asana and Monday.com each take 5 seconds, running them one after another costs 10s; running them together costs 5s. Across 8 sub-tasks, parallelism can cut wall-clock time 40–60%.

MAPPING: A DAG is your cooking schedule: it says what can happen at once (chop while the oven preheats) and what must wait (can't frost a cake before it bakes). The executor starts everything unblocked and releases waiting tasks as dependencies clear.

Concept 4 of 5

Execution in waves

  1. Find the rootsTasks with no dependencies start immediately, in parallel.
  2. Run a waveDispatch all currently-ready tasks together.
  3. Release the unblockedAs each task finishes, mark any task whose deps are now all satisfied as ready.
  4. Validate acyclicBefore running, a topological check catches cycles; if found, ask the model to regenerate the plan.
Concept 4 of 5

Pseudocode

WHILE not all_done(dag):
  ready = [ t FOR t IN dag
            IF deps_done(t)
            AND not t.done ]

  IF ready is empty:
    raise "cycle or stuck"

  run_in_parallel(ready)   # one wave
  FOR t IN ready:
    mark_done(t)

# cap parallelism (~3-5) to respect
# API rate limits.
Concept 4 of 5

Misconceptions

"More parallelism is always better."
Each parallel task eats rate-limit capacity. 10 tasks against a 5/min limit isn't 10x — it bottlenecks. 3–5 parallel is the sweet spot for most tiers.
"If one sub-task fails, the whole DAG fails."
Not if you handle it. A good executor supports partial completion: if ClickUp research fails, still compare Asana and Monday and note the gap.

A DAG runs independent sub-tasks in parallel waves while respecting dependencies. It saves ~35% wall-clock time and prevents out-of-order execution.

Validate the graph is acyclic, cap parallelism to your rate limit, and allow partial completion.

Concept 5 of 5

Give each sub-task only its tools

When you hand the model 20+ tool definitions on every call, two things break: tool-selection accuracy drops (noticeable above 5–7 tools), and you waste context on descriptions the model won't use.

Dynamic tool discovery picks a relevant subset at runtime — usually the 3–5 tools that match the current sub-task — and injects only those. Fewer, more relevant tools means faster, cheaper, and more accurate decisions.

Concept 5 of 5

The contractor's tool truck

BEFORE: Imagine a contractor who hauls their entire tool truck — plumbing, electrical, carpentry, landscaping — into every job.

PAIN: To find a wrench they sort through 200 tools; the clutter slows them and they sometimes grab the wrong one because too many look alike. Giving the model 20 tool defs (100–200 tokens each) is the same: 2,000–4,000 tokens of noise per call, and worse picks.

MAPPING: Discovery is the contractor sizing up the job first — "this is plumbing, I need pipe wrench, cutter, Teflon tape" — and grabbing just those. The agent matches the sub-task to its inventory and injects only the 3–4 relevant tools.

Concept 5 of 5

Match sub-task to tools

  1. Describe the sub-taskTake the current leaf task's description as the query.
  2. Search the inventoryRank tools by relevance — embedding similarity, or a category map.
  3. Select the top fewKeep the 3–5 best matches; drop the rest.
  4. Inject only thosePut just the selected tool definitions into this sub-task's prompt.
Concept 5 of 5

Embeddings or a category map?

IF tools < 5-7 total:
  → just send them all

IF 5-15 tools, stable set:
  → category map
     (task type → tool subset)

IF 15+ tools, evolving fast:
  → embedding similarity search

# selection accuracy beats
# availability: 4 right > 20 noisy

Embedding search adds 50–100ms and a vector store; a category map is faster and easier to debug for smaller sets.

Concept 5 of 5

Misconceptions

"Embeddings always beat a category map."
Not below ~15 tools. A simple category map is faster, easier to debug, and just as accurate for small stable sets. Use embeddings when the inventory is large and changing.
"Tool descriptions don't matter with discovery."
They matter more. Matching relies on them — "does stuff with data" won't match anything. Describe inputs, outputs, and purpose clearly.

Dynamic tool discovery injects only the few tools relevant to each sub-task. It cuts token cost ~80% and lifts selection accuracy from ~85% to ~95%.

Send all tools under ~7; category-map mid-size sets; embed only large, evolving inventories.

Test the core insight

One question per concept. Tap to reveal the answer.

Open the full module on desktop

The desktop version has you build an intent classifier, a task decomposer that emits a dependency graph, and a wave-based DAG executor with dynamic tool discovery — wired together into a working planning agent against local Mistral/Ollama, in Python and Node.

Open M13 on Desktop → Planning · intent routing · decomposition · DAG · tool discovery · ~70 min

Module 9 of 12 · Open Source Track · Agent Architectures