What planning actually buys you

Take a concrete goal: migrate this service’s database and update all callers. It has dependencies (schema before data before cutover), parallelizable parts, verification points, and failure branches. An agent that only ever picks a locally-plausible next action produces locally-plausible nonsense: it updates a caller before the schema exists, skips verification because nothing in the immediate context demanded it, or loses track of which callers it already touched.

Planning makes that structure explicit and therefore inspectable. The decomposition names the phases and their order; a progress record says what is done; the agent reasons about the task as a whole rather than as a sequence of disconnected reactions. The second, underrated payoff is debuggability. A reactive trace is a pile of tool calls you must reconstruct a narrative from; a plan is a data structure, so when the output is wrong you can point at the step that produced the wrong thing and replay it. Planning does not only make an agent more capable — it makes its failures legible, which in production is often the larger win.

Advertisement

ReAct: deliberation smeared through the loop

The default behaviour of a tool-using LlmAgent is ReAct: the model reasons about what it needs, emits a tool call, the runtime executes it and feeds the result back, and the model reasons again with that result in context, repeating until it answers with no further calls. Deliberation is not a separate phase — it is embedded in every step. There is no artifact called ‘the plan’ anywhere in the system.

That is a genuine strength. ReAct is maximally adaptive: every decision is made with the freshest possible information, so a surprising tool result is absorbed immediately instead of colliding with a commitment made five steps ago. It has no plan to become stale, no replanning machinery to build, and no planning tokens to pay for. For search, lookup, question-answering, and most tool-routing, it is simply the right answer.

Its failure mode is equally characteristic. Because the agent optimises one step at a time, it drifts on long horizons: it pursues a promising thread until the original goal has quietly fallen out of the context window, revisits work it already did, or converges on a locally sensible answer that never satisfies the actual request.

Advertisement

Plan-then-execute: commit to a shape before you act

The alternative is to make the plan a first-class artifact. The agent reads the goal, produces an explicit ordered list of steps before touching a tool, then executes them. The decomposition is generated once with the whole goal in view and no tool noise in the context — exactly the condition under which models produce their most coherent structure.

Three things follow. The plan is inspectable before it is expensive: you can log it, show it to the user, or gate execution behind an approval, none of which is possible when the plan exists only implicitly. Execution gets cheaper per step, because each step runs with a tight context rather than the whole accumulated conversation. And progress becomes measurable: three of seven steps done is a real number you can display and resume from.

The cost is brittleness. A plan is a set of assumptions written down before any of them were tested, and reality routinely violates them — a tool errors, a dataset is not shaped as assumed, an API returns an empty list. An agent that marches through a stale plan produces confident garbage. Plan-then-execute is therefore never complete without replanning; that pair, not the plan alone, is the actual pattern.

Picking a mode — and the hybrid that usually wins

The honest framing is not ‘which is better’ but ‘how much structure does this task have, and how early can I know it.’

SignalPrefer ReActPrefer plan-then-execute
Step countA handfulMany, long horizon
DependenciesOrder barely mattersReal prerequisites
KnowabilityNext step depends on the last resultShape derivable from the goal
Cost per stepCheap, reversibleExpensive or side-effecting
OversightNone neededA human should see it first
ResumabilityOne turn, doneMust survive restarts

Most real systems land in the middle, and the middle has a name: plan the coarse structure, react within each step. The agent generates five phases, then executes phase two with an ordinary reason–act loop free to call three tools in whatever order the results suggest. You get the global coherence of a plan and the local adaptivity of ReAct, spending planning tokens only where the decomposition is genuinely uncertain. And a third option beats both when it applies: if the steps are the same every time, do not plan at all — encode them as a SequentialAgent or LoopAgent and let the composition be the plan, free, deterministic, and impossible for a model to skip a step in.

The planner slot: what ADK actually injects

ADK exposes planning as a planner field on LlmAgent, populated from google.adk.planners. A planner is not an orchestrator running beside the agent; it is a pair of hooks around the model call. Before the request goes out the planner contributes planning instructions to it; when the response returns the planner post-processes the parts, separating deliberation from the content that should be surfaced or acted upon.

Two implementations ship with the kit. BuiltInPlanner delegates to the model’s native thinking capability by passing a ThinkingConfig — you are not prompting the model into a planning format, you are switching on reasoning it already knows how to do and paying for it in thinking tokens. PlanReActPlanner takes the prompt-engineering route for models without native thinking: it instructs the model to emit its plan, its reasoning, and its actions as separately delimited blocks, then parses those blocks apart so the runtime acts on the actions and treats the rest as deliberation.

from google.adk.agents import LlmAgent
from google.adk.planners import BuiltInPlanner
from google.genai import types

agent = LlmAgent(
    name="analyst",
    model="gemini-2.5-pro",
    instruction="Answer multi-step analysis questions using the tools.",
    planner=BuiltInPlanner(
        thinking_config=types.ThinkingConfig(include_thoughts=True),
    ),
    tools=[query_tickets, cluster_themes],
)

If neither fits, BasePlanner is subclassable: implement the instruction-building and response-processing halves yourself and you have a domain-specific planning strategy in the same slot.

Writing a decomposition that survives contact with tools

Plan quality is mostly step quality, and models produce far better steps when the instruction says what a good step looks like. Three properties do nearly all the work.

A step must be verifiable. ‘Analyse the data’ has no done-condition; ‘produce a per-theme volume count for Q3 and write it to state['theme_counts']’ can be checked. Make every step name its output. A step must be executable with the tools that exist. Models cheerfully plan steps requiring capabilities the agent lacks, so put the tool inventory in the planning context and require each step to map to a tool or to plain reasoning. A step must be bounded — a step that is really a project needs either splitting or a genuine sub-plan with its own depth limit.

Two anti-patterns follow. Over-decomposition — fifteen steps for something a reason–act loop does in three — multiplies model calls without adding coherence. And speculative branching, where the plan hedges on facts execution has not established yet, means you are planning too far ahead: stop at the unknown and let replanning resume once the answer is in hand.

The plan lives in state, not in the transcript

A plan that exists only as text the model once emitted is a plan you will lose. Long tasks compact their context, sessions resume hours later, processes restart, and a summarised conversation silently drops the ordering detail that made the plan a plan. Treat the plan instead as data in session state: steps, their status, and the artifacts each produced, written through output_key or tool_context.state.

def record_step(step_id: str, status: str, result_key: str, tool_context):
    """Mark a plan step done/failed and where its output landed."""
    plan = tool_context.state.get("plan", [])
    for s in plan:
        if s["id"] == step_id:
            s["status"] = status
            s["result_key"] = result_key
    tool_context.state["plan"] = plan   # reassign so the delta is persisted
    return {"remaining": [s["id"] for s in plan if s["status"] == "pending"]}

Note the reassignment: ADK persists state through the event deltas a tool or callback produces, so mutating a nested object in place and never writing the key back is a classic way to lose progress. Once the plan is in state, the agent answers ‘what is next’ by reading rather than re-deriving it from a possibly-summarised history — and resumability, an accurate progress indicator, and a durable debugging record all fall out of the same structure.

Replanning when execution diverges from the plan

Replanning converts a brittle plan into a robust one, and the first design decision is what counts as divergence. Three triggers cover most cases: a step failed outright; a step succeeded but returned something that invalidates a later step’s premise (the tickets were not categorised the way the clustering step assumed); or new information makes the remaining plan the wrong plan.

The response should be graded, not all-or-nothing. A transient tool error deserves a retry. A single bad step deserves repair — rewrite that step, keep the rest. Only a broken premise deserves a full replan of the remaining steps. Collapsing all three into ‘regenerate everything’ is the most common way replanning becomes a cost centre.

Two invariants keep replanning honest. The goal is immutable — replanning revises the route, never the destination. An agent that quietly relaxes the objective until the plan succeeds is the worst failure mode here, because it reports success; pin the original goal in state and re-inject it verbatim on every replan. And completed work is preserved: replan the remaining steps from current state. Finally, count replans — two or three on a hard task is healthy, ten means thrashing, and the right move then is to stop and escalate.

Reflection: a critic loop that improves the output

Replanning fixes the route; reflection fixes the product. After the agent produces work, a separate evaluation step judges it against explicit criteria and either accepts it or sends it back for revision. In ADK the natural shape is a LoopAgent holding a producer and a critic, with max_iterations as the hard ceiling and escalation as the early exit.

from google.adk.agents import LoopAgent

def accept_draft(tool_context):
    """Called by the critic when the draft meets every criterion."""
    tool_context.actions.escalate = True   # break out of the LoopAgent
    return {"verdict": "accepted"}

review_loop = LoopAgent(
    name="draft_and_review",
    sub_agents=[reviser, critic],
    max_iterations=3,
)

Reflection lives or dies on the criteria. A critic told to ‘check the quality’ always finds something and loops to the ceiling every time, burning three times the tokens for cosmetic edits. A critic given a checklist — does every theme have a recommendation, is every quantitative claim traceable to a retrieved number — returns an actionable verdict and terminates the moment the checklist passes. Keep the critic’s context narrow, the artifact and the criteria rather than the whole conversation, so it judges the work instead of sympathising with the reasoning that produced it.