A plan is the map a ReAct agent lacks. This post gives the agent one, and the pleasing part is that it needs no new machinery at all.
A plan is just a tool the agent calls. It writes down the tasks, tracks their status, and updates them as it goes, and because the plan sits in the context, every later step can see it.
When a plan is worth the cost
Planning is not free. Writing and updating a plan costs model calls, so it earns its place only on the tasks that need it.
Plan when the task is genuinely multi-step. A question that needs several pieces of research combined, where you cannot see the whole route from the start, is exactly what a plan is for. "How many hours to reach the Moon at a marathon pace?" needs a record, a distance, a conversion, and a calculation over all three.
Skip planning when the task is one or two steps. "What is the capital of France?" needs no plan, and writing one is pure overhead. The decision is the same one from the very start of this series: match the machinery to the task.
A plan is a tool
The agent records its plan by calling a tool, the same mechanism it uses for everything else.
class Task(BaseModel):
description: str
status: Literal["pending", "in_progress", "done"] = "pending"
@tool
def write_plan(tasks: list[str]) -> str:
"""Record a plan as an ordered list of tasks to work through."""
plan = [Task(description=t) for t in tasks]
context.state["plan"] = plan # the plan lives in the run's state
return render_plan(plan)
Read where the plan goes: into the run's state, which means it rides in the context from here on. The agent does not have to remember its plan. It re-reads it every step, because it is right there in front of the model, the same way a developer keeps the spec open in a tab.
Updating as it goes
A plan that never changes is a to-do list the agent ignores. The value is in updating it: mark a task done, mark the next one in progress, so the plan always reflects where the agent actually is.
@tool
def update_task(index: int, status: str) -> str:
"""Update one task's status as work progresses."""
context.state["plan"][index].status = status
return render_plan(context.state["plan"])
Now the plan is a live document. After each meaningful step the agent updates it, and the next step begins by reading a plan that shows exactly what is done and what remains. That is the difference between a plan and a wish: a plan gets checked off.
The plan lives in the context. The agent reads it every step and checks tasks off as it goes.
Watching it plan
Give the agent the Moon question and it plans before it searches.
Action: write_plan([
"Find Kipchoge's marathon world record time",
"Convert the record to a speed in km/h",
"Find the Earth-Moon closest-approach distance",
"Divide the distance by the speed for the time",
])
Action: update_task(0, "in_progress")
Action: search_web("Kipchoge marathon world record")
Observe: 2:01:09 (Berlin 2022)
Action: update_task(0, "done")
...
The agent stopped, decomposed the problem, and wrote the route down before taking a single step. Every step after that begins by reading the plan, so it always knows which of the four tasks it is on and what is left. Compare that with the reactive version, which would have searched, then wondered what to do with the result.
Keep the first version simple
A planning tool can grow elaborate: multiple plans at once, partial updates, dependency graphs between tasks. Reach for that only when a task demands it. The simple single-list version above handles most work.
The one extension worth knowing early is completion criteria. "Research Kipchoge's record" is vague, and the agent cannot tell when it is done. "Confirm Kipchoge's marathon record time and the distance unit" is checkable. A task the agent can verify as complete is a task it will not half-finish and mark done, which is exactly the early-victory failure planning is meant to prevent.
| Version | What it holds | Reach for it when |
|---|---|---|
| Simple | One ordered list of tasks with statuses | Most multi-step tasks |
| Completion criteria | Each task with a checkable "done when" | The agent declares tasks done too early |
| Multiple plans | Several plans, sub-plans, dependencies | Genuinely complex, branching work |
Start with one checked-off list. Add completion criteria and structure only when the task demands it.
What to take from this
- A plan needs no new machinery. It is a tool the agent calls to record tasks, and the plan lives in the context, so every step sees it.
- The value is in updating it. Mark tasks done and in-progress so the plan stays a live map, checked off, not a wish written once and ignored.
- Keep the first version a single list, and add completion criteria when the agent finishes tasks too early. Match the plan's complexity to the task.
A plan sets direction. It does not, on its own, notice when direction has gone wrong. The agent can still follow a good plan straight into a failed search and keep going. The next post adds the habit that catches that: reflection.