jatin.blog ~ $
$ cat ai-engineering/planning-vs-reactive-agents.md

Planning Agents vs Reactive Agents

How ReAct, plan-and-execute, ReWOO, and search-based planning trade cost for control.

Jatin Bansal@blog:~/ai-engineering$ open planning-vs-reactive-agents

Reactive agents choose the next action after each observation. Planning agents create a multi-step artifact, then execute it and revise it when needed. Reactive control fits exploratory tasks whose observations change the path. Planning pays off when the sequence is predictable enough to amortize a planner call across many steps.

What separates a planner from a reactor

Both architectures are agents in Simon Willison’s working sense; they run tools in a loop to achieve a goal. The difference is where the decision-making lives.

  • Reactive agents decide what to do next at each step. The ReAct loop is the archetype: emit one thought, one action, observe the result, then think again from the updated context. The agent has no commitment beyond the current step; it can swerve on the basis of the most recent observation.
  • Planning agents commit to a multi-step plan up front, then execute against it. The plan is a typed artifact: usually a JSON list of steps; produced by one expensive call to a strong model. Execution is mechanical: dispatch each step, splice the result back, move on.

The reactive agent does online reflection inside the action sequence; the planning agent does whole-task reflection before any action. Both are real strategies. They have different cost curves, different failure modes, and different sweet spots. Production systems converge on a hybrid where the boundary between the two shifts depending on how confident the agent is about what comes next.

The four points on the curve

Agent architectures lie on an axis from “decide each step at runtime” to “decide all steps before any action”:

  1. Pure ReAct. One LLM call per step. Maximally adaptive, maximally chatty. The default for chat agents and exploratory work. Covered in detail in the agent loop article.

  2. Plan-and-execute (sequential). One planner call up front emits a JSON list of typed steps; an executor (often a cheaper model, sometimes raw code) runs the steps in order. After each step or on failure, an optional re-planner can revise the remaining plan. LangChain’s canonical writeup is the practical reference; the research lineage is Plan-and-Solve (Wang et al., ACL 2023).

  3. ReWOO (reasoning without observation). The planner emits the entire plan including placeholders for observations. The executor runs the tool calls, often in parallel, and a final “Solver” call synthesizes the answer. The planner never sees the observations; the solver sees them together. ReWOO (Xu et al., 2023) trades adaptability for token efficiency: one planner call plus N tool calls plus one solver call replaces N×2 ReAct calls on tasks whose plans tolerate unexpected results.

  4. Tree-of-Thoughts (ToT). Multiple candidate plans (or partial plans) are generated and scored; the agent explores the tree with backtracking, much like a chess engine’s search. The Tree-of-Thoughts paper reports lifting Game-of-24 success from 4% (chain-of-thought) to 74%, but at multi-call cost per step that only pays off when the task has high branching and a cheap evaluator (a unit test, a constraint check, a verifier model). Production ToT is rare; production uses of ToT for hard sub-problems inside a larger agent are increasingly common.

Two patterns deserve flags but live one chapter away from the planner/reactor decision proper:

  • Reflexion. Reflexion (Shinn et al., NeurIPS 2023) is post-hoc reflection: after a trial fails, the agent writes a verbal “what went wrong” note to memory and tries again. It can wrap either a reactive or a planning agent. The dedicated reflection article picks up reflection as a first-class memory operation; the Generative Agents importance-threshold trigger, the salient-question-then-evidence-anchored-insight pipeline, and the self-reinforcing-error failure mode that bites both Reflexion-style and Generative-Agents-style implementations.
  • Anthropic’s workflow patterns. Building effective agents separates prompt chaining, routing, parallelization, orchestrator-workers, and evaluator-optimizer. Each workflow can be reactive or planned, depending on where it invokes the model.

When planning beats reacting

Planning works best when each step has low uncertainty. It is most useful when:

  • The step sequence is largely deterministic given the goal. Bulk migrations, ETL jobs, structured reports, “scrape these 50 URLs and produce a CSV”: the model knows the steps from the goal alone. Reacting at every step burns model calls re-deriving what the planner already knows.
  • Steps are weakly coupled. If step 5’s input doesn’t strongly depend on step 4’s output, the executor can use a smaller, cheaper model per step (the model-routing article covers the cost-tier mechanics). The planner’s whole-task reasoning is too expensive to repeat per step; the executor’s narrow-step reasoning is cheap. Plan-and-execute is the architecture that exploits this asymmetry.
  • The cost of replanning is low. When a step fails and the plan needs revision, how expensive is it to re-plan? If the re-planner is small and the failure is localized, replanning is cheap and you get adaptability back. If re-planning needs the strong model and re-derives most of the plan, you’ve lost ReWOO’s advantage.

ReAct remains a better fit when:

  • The task is exploratory. Open-ended research, debugging, “figure out why X is failing.” Each observation genuinely changes what to do next; a plan made before the first observation is fiction. ReAct’s adaptivity is the point.
  • Tool results vary in shape. If the executor has to make non-trivial decisions about how to interpret each result, you’ve snuck the planner back into the executor. Just run ReAct.
  • The task is short. For 1-3 step tasks, the planner overhead doesn’t amortize. The single extra LLM call of a planner-then-executor pipeline is a 33% latency penalty on a 3-step task; it’s a 3% penalty on a 30-step task.

A useful diagnostic: if your planner emits a fixed plan and execution always follows it without revision, you’re not running an agent; you’re running a workflow. That’s not a criticism; it’s a clarification. Anthropic’s “Building effective agents” draws this line explicitly. Workflows are simpler, cheaper, and easier to test. Reach for an agent only when the dynamism is critical.

The cost of replanning

The planning vs reacting trade-off boils down to one quantity: the expected cost of replanning, multiplied by the probability that you’ll have to.

Let:

  • Cp = cost of a single planner call (large model, whole-task reasoning)
  • Ce = cost of a single executor step (small model, narrow reasoning)
  • Cr = cost of ReAct step (large model, narrow + whole-task reasoning amortized)
  • N = number of steps in the task
  • p = probability that the plan needs revision after some step (the “branch miss rate”)
  • k = average step count at which the revision fires (replanning loses partial progress)

Approximate total costs:

  • ReAct: N · Cr
  • Plan-and-execute, no revision: Cp + N · Ce
  • Plan-and-execute, with revision probability p at step k: Cp + N · Ce + p · (Cp + (N - k) · Ce)

The plan-and-execute path wins when Cp + N · Ce + p · Cp · (something) < N · Cr. With Cr ≈ 3 · Ce (a typical large/small model gap) and Cp ≈ 5 · Ce, the break-even is somewhere around N > 4 with p < 0.3. The numbers shift with your provider’s prices and your model split, but the shape doesn’t: planning pays off as N grows and as p shrinks. The mistake is to use planning on a task with high p; you collect the planner overhead and the replanning penalty and lose to ReAct on both ends.

This is exactly the branch-prediction calculation a CPU runs: speculate when the prediction is likely right, fall back to in-order execution when branches are unpredictable. Tasks where p is structurally high (exploratory research, debugging, chat) live in the unpredictable regime and want ReAct. Tasks with low p (bulk transformations, well-defined data pipelines) live in the predictable regime and want plan-and-execute.

Code: a plan-and-execute agent in Python

A migration-assistant pattern. The planner emits a typed Plan; the executor processes each step with a cheaper model; a re-planner fires if a step fails. Install: pip install anthropic pydantic. Uses the Anthropic SDK and Pydantic for the plan schema; same pattern as the structured-output article.

python
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
import json, time
from typing import Literal
from pydantic import BaseModel, Field
from anthropic import Anthropic

client = Anthropic()

# --- typed plan schema (the planner emits this; the executor consumes it) ---
class PlanStep(BaseModel):
    id: int
    action: Literal["read_file", "transform", "write_file", "run_tests"]
    args: dict
    depends_on: list[int] = Field(default_factory=list)  # for parallel execution

class Plan(BaseModel):
    goal: str
    steps: list[PlanStep]

PLANNER_MODEL = "claude-opus-4-7"
EXECUTOR_MODEL = "claude-haiku-4-5"   # cheaper executor; pick what fits your stack

# --- planner: one large-model call, structured output via tool use ---
PLAN_TOOL = {
    "name": "submit_plan",
    "description": "Submit the multi-step plan for the migration task.",
    "input_schema": Plan.model_json_schema(),
}

def plan(goal: str) -> Plan:
    resp = client.messages.create(
        model=PLANNER_MODEL,
        max_tokens=4096,
        tools=[PLAN_TOOL],
        tool_choice={"type": "tool", "name": "submit_plan"},   # force the call
        messages=[{"role": "user", "content": f"Produce a step-by-step plan to: {goal}"}],
    )
    tool_block = next(b for b in resp.content if b.type == "tool_use")
    return Plan.model_validate(tool_block.input)

# --- executor: one small-model call per step (or raw code where deterministic) ---
def execute_step(step: PlanStep, state: dict) -> dict:
    # Deterministic actions short-circuit the LLM entirely.
    if step.action == "read_file":
        return {"text": open(step.args["path"]).read()}
    if step.action == "write_file":
        open(step.args["path"], "w").write(step.args["content"])
        return {"ok": True}
    if step.action == "run_tests":
        # subprocess.run(...) in real life; stubbed here
        return {"passed": True}

    # The transform action is the only one that needs the model.
    resp = client.messages.create(
        model=EXECUTOR_MODEL,
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": f"Transform per spec.\n\nInput:\n{step.args['input']}\n\nSpec:\n{step.args['spec']}",
        }],
    )
    return {"output": "".join(b.text for b in resp.content if b.type == "text")}

# --- the orchestrator: plan once, execute sequentially, replan on failure ---
def run(goal: str, *, max_replans=2, deadline_s=600.0):
    started = time.monotonic()
    current_plan = plan(goal)
    state: dict = {}
    replans = 0

    while True:
        for step in current_plan.steps:
            if time.monotonic() - started > deadline_s:
                raise TimeoutError(f"deadline exceeded at step {step.id}")
            try:
                result = execute_step(step, state)
                state[step.id] = result
                # progress check: tests failed? trigger replan
                if step.action == "run_tests" and not result.get("passed", True):
                    raise RuntimeError(f"tests failed at step {step.id}")
            except Exception as e:
                if replans >= max_replans:
                    raise RuntimeError(f"replan budget exhausted: {e}")
                replans += 1
                # Replan from the failed step forward, with the failure as context.
                current_plan = replan(goal, current_plan, step.id, str(e), state)
                break   # restart the for-loop with the new plan
        else:
            return state   # ran to completion without a failure-triggered break

def replan(goal: str, prior: Plan, failed_at: int, error: str, state: dict) -> Plan:
    """One re-planner call. Pass enough state for the planner to revise meaningfully."""
    summary = json.dumps({k: v for k, v in state.items() if k < failed_at})
    resp = client.messages.create(
        model=PLANNER_MODEL,
        max_tokens=4096,
        tools=[PLAN_TOOL],
        tool_choice={"type": "tool", "name": "submit_plan"},
        messages=[{
            "role": "user",
            "content": (
                f"Revise the plan for goal: {goal}\n"
                f"Original plan:\n{prior.model_dump_json(indent=2)}\n"
                f"Step {failed_at} failed with: {error}\n"
                f"Prior step outputs:\n{summary}\n"
                f"Emit a new plan that picks up from step {failed_at}."
            ),
        }],
    )
    tool_block = next(b for b in resp.content if b.type == "tool_use")
    return Plan.model_validate(tool_block.input)

The planner uses forced tool choice to return a typed plan. The executor handles deterministic actions without a model call and uses the model only for transformations. Replanning is capped at two attempts; repeated replanning indicates that the task is too uncertain for this control flow.

Further reading

  • Anthropic: Building effective agents: workflow and agent patterns including prompt chaining, routing, parallelization, orchestrator-workers, and evaluator-optimizer.
  • LangChain: “Plan-and-Execute Agents”; the canonical engineering writeup on the planner/executor split, with reference implementations for plan-and-execute, ReWOO, and LLMCompiler. The clearest single source on the in-between architectures named in this article.
  • Tree-of-Thoughts (Yao et al., NeurIPS 2023): the paper that demonstrated deliberate search over candidate plans, with the eye-popping Game-of-24 result (4% → 74%). The cleanest reference for when an agent should plan multiple candidates before committing to one.
  • LLMCompiler (Kim et al., ICML 2024): the DAG-emitting planner with parallel execution, reporting 3.7× latency speedup and 6.7× cost reduction over ReAct on tasks with exposable dependency structure. The clearest argument that “agent” and “make” can be the same thing.