ESC

Type to search the knowledge base.

ReAct vs Plan-and-Execute

ReAct interleaves thought and tools. Plan-and-execute writes the plan first. Pick based on tool cost, reversibility, and how often the world changes mid-run.

intermediate5 min read
  • agents
  • planning
  • react

You planned a 12-step itinerary. Step 2 is sold out. Steps 3–12 are now fiction you will still try to book. That is why a ReAct agent exists: think, act, look at the world, then think again. Plan-and-execute writes the checklist first. It is faster when the world sits still. It is a trap when a tool can fail or a seat can vanish.

Neither pattern is a personality. They are control flow. Pick them the way you pick a loop versus a batch job: cost of a tool call, whether you can undo it, and how often observations invalidate the rest of the list.

Key takeaways

  • ReAct interleaves thought, tool, observation until a stop condition.
  • Plan-and-execute drafts every step up front, then runs the list.
  • Prefer ReAct when tools are fallible, expensive to reverse, or the data changes mid-run.
  • Prefer a plan when steps are cheap, local, and a human should review the checklist.
  • Hybrid: plan phases, ReAct inside a phase.

ReAct agent vs plan-and-execute

The think, act, observe loop is the runtime. ReAct and plan-and-execute are two ways to schedule it. ReAct, from Yao et al., keeps the next action conditional on the last observation. Plan-and-execute spends one (or a few) completions on a structured plan, then walks it.

type ToolResult = { ok: boolean; data: unknown; error?: string };

type PlanStep = { id: string; tool: string; args: Record<string, unknown> };

export async function reactLoop(opts: {
  goal: string;
  maxSteps: number;
  think: (trace: string) => Promise<{ kind: 'act'; tool: string; args: Record<string, unknown> } | { kind: 'stop'; answer: string }>;
  act: (tool: string, args: Record<string, unknown>) => Promise<ToolResult>;
}): Promise<string> {
  let trace = `Goal: ${opts.goal}\n`;
  for (let i = 0; i < opts.maxSteps; i++) {
    const decision = await opts.think(trace);
    if (decision.kind === 'stop') return decision.answer;
    const observation = await opts.act(decision.tool, decision.args);
    trace += `act ${decision.tool} ${JSON.stringify(decision.args)}\nobs ${JSON.stringify(observation)}\n`;
    if (!observation.ok && observation.error === 'sold_out') {
      trace += 'Plan is stale. Re-plan from this observation.\n';
    }
  }
  throw new Error('max steps');
}

export async function planThenExecute(opts: {
  plan: (goal: string) => Promise<PlanStep[]>;
  act: (step: PlanStep) => Promise<ToolResult>;
  goal: string;
}): Promise<ToolResult[]> {
  const steps = await opts.plan(opts.goal);
  const out: ToolResult[] = [];
  for (const step of steps) {
    const result = await opts.act(step);
    out.push(result);
    if (!result.ok) break; // failure: leftover steps are invalid
  }
  return out;
}

The failure comment is the product difference. ReAct can insert a new thought after sold_out. A naive plan runner keeps walking.

Which loop should you ship?

ReAct Plan-and-execute
Control flow Next tool depends on last observation Checklist first, then walk
Tokens More model turns; each turn is smaller One fat plan, then cheaper steps (or no model per step)
Recoverability High if you feed errors back Low unless you re-plan on failure
Reviewability Trace is a diary Plan is a document a human can edit
Failure mode Loops, “let me try again” Stale plan, overconfident sequence

Use ReAct when:

  • Inventory, calendars, tickets, or prices can change between steps.
  • A tool is costly to reverse (charge, email, deploy).
  • You do not know the next lookup until you see the last one.

Use plan-and-execute when:

  • The tools are local and cheap (read_file, lint).
  • You want a human to approve the list before any step runs. Pair with human in the loop on the plan, not on every micro-tool.
  • The task is “run these migrations in order” and step 2 cannot invalidate step 5’s meaning, only its success.

Token use is not a slogan. ReAct pays for extra completions and extra observations stuffed into context. A plan pays for a long first completion and then risks throwing it away. Cap both. See agent failure modes for the unbounded “try again” bill.

Hybrid: plan the phases, ReAct the messy one

You rarely need a religion. A travel bot can plan phases (research → book → confirm) and ReAct inside book, where seats disappear. A coding bot can plan files to touch, then ReAct while tests are red. That outer list is what a coding-agent loop should show the user before edits start.

type Phase = 'research' | 'book' | 'confirm';

export async function hybridTravel(goal: string) {
  const phases: Phase[] = ['research', 'book', 'confirm'];
  const notes: string[] = [];
  for (const phase of phases) {
    const answer = await reactLoop({
      goal: `${phase}: ${goal}\nPrior notes: ${notes.join(' | ')}`,
      maxSteps: phase === 'book' ? 8 : 4,
      think: plannerFor(phase),
      act: toolsFor(phase),
    });
    notes.push(`${phase}: ${answer}`);
  }
  return notes.at(-1);
}

Keep phase tools disjoint. Research should not charge a card. Confirm should not search flights again unless book reported a failure. Disjoint allowlists do more than a paragraph of “you are a careful travel agent.”

This is the same instinct as a frontend event loop: one turn of work, then look at the queues. If you care how JS actually schedules that, the event loop writeup is the non-AI version of the same idea.

Interview answer (out loud)

“A ReAct agent interleaves thought, tool, and observation so a failed or stale step can change the next action. Plan-and-execute writes the checklist first, which is easy to review and easy to invalidate. I pick ReAct when the world moves or tools are hard to undo. I pick a plan when steps are cheap and I want a human to edit the list. Hybrid is plan the phases, ReAct inside the noisy phase, with a max-step cap on both.”

Further reading (primary sources)

Related guides