[SL_CORE]
< CD ..||7 min read

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.

[ai-agents][reasoning][architecture][production]

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.