Agent Planning & Reasoning: From Plan-Execute to Loops
# AI agents that plan before acting complete complex tasks with fewer errors and lower cost. Learn plan-and-execute, tree-of-thought, and self-improvement loop patterns for production agents.
An agent that jumps straight to tool calls fails more often and costs more than one that reasons about the task first. Planning — figuring out what needs to happen before doing it — is the difference between an agent that stumbles through a task and one that completes it reliably. Here are the planning patterns that work in production.
Why Planning Matters
Without planning, agents:
- Call tools in the wrong order (read file before checking if it exists)
- Miss dependencies (try to update a record before creating the parent)
- Repeat work (search for the same thing three times)
- Go off-track after an unexpected tool result
With planning, agents decompose the goal before acting, track progress, and recover from failures systematically.
Pattern 1: Plan-and-Execute
The simplest effective planning pattern: generate a plan, then execute each step.
interface Plan {
goal: string;
steps: Array<{
id: number;
description: string;
toolNeeded?: string;
dependsOn: number[];
status: "pending" | "in_progress" | "done" | "failed";
result?: string;
}>;
}
async function planAndExecute(goal: string): Promise<string> {
// Phase 1: Plan
const planResponse = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1000,
messages: [{
role: "user",
content: `Create a step-by-step plan to: ${goal}
Available tools: search_web, read_file, write_file, run_code, call_api
Return JSON:
{
"steps": [
{
"id": 1,
"description": "what to do",
"toolNeeded": "tool name or null",
"dependsOn": []
}
]
}
Be specific and concrete. List dependencies between steps.`,
}],
});
const plan: Plan = {
goal,
steps: JSON.parse(planResponse.content[0].text).steps.map((s: any) => ({
...s,
status: "pending",
})),
};
// Phase 2: Execute
return await executePlan(plan);
}
async function executePlan(plan: Plan): Promise<string> {
const results: Record<number, string> = {};
while (plan.steps.some((s) => s.status === "pending")) {
// Find executable steps (deps satisfied, not blocked)
const ready = plan.steps.filter((s) =>
s.status === "pending" &&
s.dependsOn.every((dep) => plan.steps.find((p) => p.id === dep)?.status === "done")
);
if (ready.length === 0) break; // deadlock or all done
// Execute ready steps (in parallel if no mutual deps)
await Promise.all(
ready.map(async (step) => {
step.status = "in_progress";
const contextFromDeps = step.dependsOn
.map((dep) => `Step ${dep} result: ${results[dep]}`)
.join("\n");
try {
const result = await executeStep(step, contextFromDeps);
step.result = result;
results[step.id] = result;
step.status = "done";
} catch (err) {
step.status = "failed";
step.result = String(err);
}
})
);
}
// Synthesize final answer
const synthesis = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1000,
messages: [{
role: "user",
content: `Goal: ${plan.goal}
Steps completed:
${plan.steps.map((s) => `[${s.status}] Step ${s.id}: ${s.description}\nResult: ${s.result ?? "N/A"}`).join("\n\n")}
Synthesize a complete answer to the goal based on these results.`,
}],
});
return synthesis.content[0].text;
}
Pattern 2: ReAct Loop (Reason + Act)
ReAct interleaves reasoning and action — the agent thinks aloud before each tool call:
const REACT_SYSTEM = `You are an agent solving tasks using available tools.
For each action, follow this format exactly:
Thought: [reason about what to do next and why]
Action: [tool to use]
Action Input: [input for the tool as JSON]
After seeing a tool result, continue:
Thought: [reason about the result and what to do next]
Action: [next tool or "Finish"]
Action Input: [input or final answer]
Think step by step. Only use one tool at a time.`;
async function reactAgent(task: string, tools: Tool[]): Promise<string> {
const messages: Anthropic.MessageParam[] = [{
role: "user",
content: `Task: ${task}`,
}];
let iterations = 0;
const maxIterations = 15;
while (iterations < maxIterations) {
iterations++;
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1500,
system: REACT_SYSTEM,
messages,
});
const text = response.content[0].text;
messages.push({ role: "assistant", content: text });
// Parse action
const actionMatch = text.match(/Action:\s*(.+)\nAction Input:\s*({[^}]+}|.+)/);
if (!actionMatch) break;
const [, action, inputStr] = actionMatch;
if (action.trim() === "Finish") {
const answerMatch = inputStr.match(/"answer":\s*"([^"]+)"/);
return answerMatch?.[1] ?? inputStr;
}
// Execute tool
let toolResult: string;
try {
const input = JSON.parse(inputStr);
const result = await executeTool(action.trim(), input);
toolResult = JSON.stringify(result);
} catch (err) {
toolResult = `Error: ${err}`;
}
messages.push({
role: "user",
content: `Observation: ${toolResult}`,
});
}
return "Max iterations reached";
}
ReAct is excellent for debugging — the Thought steps are visible and tell you exactly why the agent took each action.
Pattern 3: Tree of Thoughts
For problems with multiple valid approaches, explore branches and pick the best:
interface ThoughtNode {
thought: string;
value: number; // 0–1 confidence score
children: ThoughtNode[];
terminal: boolean;
answer?: string;
}
async function treeOfThoughts(problem: string, branches = 3, depth = 3): Promise<string> {
async function expand(node: ThoughtNode, remainingDepth: number): Promise<void> {
if (remainingDepth === 0 || node.terminal) return;
// Generate candidate next thoughts
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 800,
messages: [{
role: "user",
content: `Problem: ${problem}
Current reasoning: ${node.thought}
Generate ${branches} different ways to continue solving this problem.
For each path, estimate confidence (0-1) that it leads to a correct solution.
Return JSON: {"paths": [{"thought": "...", "confidence": 0.8, "isTerminal": false, "answer": null}]}`,
}],
});
const { paths } = JSON.parse(response.content[0].text);
node.children = paths.map((p: any) => ({
thought: p.thought,
value: p.confidence,
children: [],
terminal: p.isTerminal,
answer: p.answer,
}));
// Only expand promising branches (beam search)
const promising = node.children
.sort((a, b) => b.value - a.value)
.slice(0, 2);
await Promise.all(promising.map((child) => expand(child, remainingDepth - 1)));
}
const root: ThoughtNode = { thought: "Starting analysis", value: 1, children: [], terminal: false };
await expand(root, depth);
// Find best terminal node
function findBest(node: ThoughtNode): ThoughtNode {
if (node.terminal || node.children.length === 0) return node;
const best = node.children.reduce((a, b) => a.value > b.value ? a : b);
return findBest(best);
}
const best = findBest(root);
return best.answer ?? best.thought;
}
Tree of Thoughts shines for math problems, strategy tasks, and any domain where there are multiple valid solution paths.
Pattern 4: Self-Improvement Loop
Agents that critique and improve their own outputs:
async function selfImprovingAgent(task: string, rounds = 3): Promise<string> {
// Initial attempt
let solution = await generateSolution(task);
for (let round = 0; round < rounds; round++) {
// Self-critique
const critique = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 500,
messages: [{
role: "user",
content: `Task: ${task}
Current solution:
${solution}
Critique this solution. What are the weaknesses, errors, or missing elements?
Return JSON: {
"score": 1-10,
"issues": ["issue1", ...],
"suggestions": ["suggestion1", ...],
"satisfactory": true/false
}`,
}],
});
const feedback = JSON.parse(critique.content[0].text);
if (feedback.satisfactory || feedback.score >= 9) break;
// Improve based on critique
const improvement = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 2000,
messages: [{
role: "user",
content: `Improve this solution based on the feedback.
Task: ${task}
Current solution: ${solution}
Issues: ${feedback.issues.join("; ")}
Suggestions: ${feedback.suggestions.join("; ")}
Provide an improved solution that addresses all issues.`,
}],
});
solution = improvement.content[0].text;
}
return solution;
}
Choosing a Planning Pattern
| Pattern | Best for | Cost | Latency | |---------|----------|------|---------| | Plan-and-Execute | Multi-step tasks with known structure | Medium | Medium | | ReAct | Open-ended research, debugging | Low–Medium | Medium | | Tree of Thoughts | Optimization, creative tasks, math | High | High | | Self-Improvement | Quality-critical single outputs | Medium | High |
For the QuantumSketch video pipeline at Shahriar Labs, we use Plan-and-Execute: generate a scene plan, then execute each scene in parallel. The parallel execution cuts latency by 70% vs. sequential.
FAQ
Does planning actually improve results? Yes — significantly. Plan-and-Execute with a clear task reduces tool call errors by 40–60% in our testing. The model makes better decisions when it has a map.
How much does planning add to cost? Planning adds 300–800 tokens per run (the plan generation). For a 3,000-token task execution, that's 10–25% overhead. The reduction in retry costs typically exceeds the planning overhead.
Can I let the agent modify its plan mid-execution?
Yes — expose a update_plan tool. If a step fails unexpectedly, the agent can revise remaining steps. This is called "adaptive planning" and handles real-world messiness better than rigid plans.
What's the max complexity for plan-and-execute? Plans with >10 steps get unwieldy — the model loses track of dependencies. Break plans with >10 steps into sub-plans managed by sub-agents.
Is ReAct the same as the agent loop I already have? Mostly — the key difference is the explicit "Thought:" step that makes reasoning visible. Without it, the model can call tools without articulating why, which makes debugging nearly impossible.