12. Agent Planning
Planning allows the agent to think before acting. Rather than blindly following the first tool call suggestion, a planner breaks the task into steps and maps them to tool calls before executing anything.
Simple planning prompt
def plan_task(task: str, model) -> list:
"""Decompose a task into steps before execution"""
planning_prompt = (
"You are a planner. Break down the following task into a numbered "
"list of steps. Each step should be achievable with a single tool call.\n\n"
f"Task: {task}\n\n"
"Output format:\n1. [Step description]\n2. [Step description]\netc."
)
plan_response = model.chat([{"role": "user", "content": planning_prompt}])
steps = []
for line in plan_response.content.split("\n"):
if line.strip() and line[0].isdigit():
steps.append(line.split(".", 1)[1].strip())
return steps
def execute_with_plan(task: str, tools: list, model):
steps = plan_task(task, model)
print(f"Planned {len(steps)} steps")
for i, step in enumerate(steps):
print(f"Step {i+1}: {step}")
# Route step to appropriate tool based on description
response = model.chat([
{"role": "system", "content": f"Execute this step: {step}"},
{"role": "user", "content": step}
], tools=[t.to_openai_schema() for t in tools])
if response.message.tool_calls:
for call in response.message.tool_calls:
result = tools[call.function.name].invoke(**call.function.arguments)
print(f"Result: {result[:200]}")
Planning vs. ReAct
ReAct reasons step-by-step while executing, using each observation to inform the next step. Planning reasons once upfront and produces a fixed roadmap before action. The two approaches have different strengths:
| Aspect | ReAct | Planning |
|---|---|---|
| Adaptability | High—reacts to observations | Low—follows a fixed plan |
| Overhead | Low—never plans ahead | Medium—extra LLM call upfront |
| Best for | Open-ended exploration | Structured, multi-step tasks |
| Failure mode | Wanders off topic | Plan becomes stale mid-execution |
Plan revision
Plans should be revisable. After each step, check whether the result matches the expected outcome and update the remaining plan:
def execute_with_revisions(task: str, tools: list, model, max_revisions: int = 3):
remaining_steps = plan_task(task, model)
executed = 0
for revision in range(max_revisions):
for step in remaining_steps[:]:
response = execute_step(step, tools, model)
if is_outcome_unacceptable(response):
# Revise remaining steps based on failure
new_steps = replan_from_failure(task, remaining_steps, model)
remaining_steps = new_steps
else:
remaining_steps.remove(step)
executed += 1
if not remaining_steps:
break
return executed
Input a complex research task like "Compare the GDP per capita of Japan and Germany for the last 5 years." Run the planner separately and compare its output to a ReAct agent handling the same task. Count the number of tool calls and total turns for each approach.