From Zero to AI

Lesson 5.2: Planning and Reasoning

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Implement planning strategies for complex agent tasks
  2. Use chain-of-thought prompting to improve reasoning
  3. Build agents that decompose goals into actionable steps
  4. Handle plan adaptation when initial approaches fail

Why Planning Matters

Simple agents react to each step without considering the bigger picture. This works for straightforward tasks but fails for complex problems.

Consider this task: "Research the top 3 AI companies, compare their products, and write a summary report."

Without Planning:

Step 1: Search "top AI companies"
Step 2: Read first result
Step 3: Search "company products"
Step 4: Read result
... (agent gets lost in details, forgets the goal)

With Planning:

Plan:
1. Identify top 3 AI companies (OpenAI, Anthropic, Google)
2. For each company:
   a. Research main products
   b. Note key features
3. Compare products across companies
4. Write summary report

Now executing step 1...

Planning keeps the agent focused and organized.


The Plan-and-Execute Pattern

This pattern separates planning from execution:

┌─────────────────────────────────────────────────────────────────┐
│                     PLAN-AND-EXECUTE                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐         ┌──────────┐         ┌──────────┐        │
│  │   Goal   │────────▶│  Planner │────────▶│   Plan   │        │
│  └──────────┘         └──────────┘         └────┬─────┘        │
│                                                  │              │
│                                                  ▼              │
│                                          ┌─────────────┐       │
│                                          │  Executor   │       │
│                                          └──────┬──────┘       │
│                                                 │               │
│         ┌───────────────────────────────────────┤               │
│         │                                       │               │
│         ▼                                       ▼               │
│  ┌─────────────┐                        ┌─────────────┐        │
│  │  Execute    │                        │   Replan    │        │
│  │  Step 1     │                        │  if needed  │        │
│  └──────┬──────┘                        └─────────────┘        │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                               │
│  │  Execute    │                                               │
│  │  Step 2     │                                               │
│  └──────┬──────┘                                               │
│         │                                                       │
│         ▼                                                       │
│       ...                                                       │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                               │
│  │   Result    │                                               │
│  └─────────────┘                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Implementing a Planning Agent

Here is a complete implementation of a planning agent:

import OpenAI from 'openai';

interface PlanStep {
  id: number;
  description: string;
  status: 'pending' | 'in_progress' | 'completed' | 'failed';
  result?: string;
}

interface Plan {
  goal: string;
  steps: PlanStep[];
}

interface Tool {
  name: string;
  description: string;
  parameters: Record<string, unknown>;
  execute: (args: Record<string, unknown>) => Promise<string>;
}

class PlanningAgent {
  private client: OpenAI;
  private model: string;
  private tools: Tool[];
  private plan: Plan | null = null;

  constructor(client: OpenAI, model: string, tools: Tool[]) {
    this.client = client;
    this.model = model;
    this.tools = tools;
  }

  async run(goal: string): Promise<string> {
    // Phase 1: Create the plan
    this.plan = await this.createPlan(goal);
    console.log('Plan created:', this.plan);

    // Phase 2: Execute each step
    for (const step of this.plan.steps) {
      step.status = 'in_progress';
      console.log(`Executing step ${step.id}: ${step.description}`);

      try {
        const result = await this.executeStep(step);
        step.result = result;
        step.status = 'completed';
        console.log(`Step ${step.id} completed:`, result);
      } catch (error) {
        step.status = 'failed';
        console.log(`Step ${step.id} failed:`, error);

        // Try to replan
        const shouldContinue = await this.handleFailure(step);
        if (!shouldContinue) {
          return `Failed at step ${step.id}: ${step.description}`;
        }
      }
    }

    // Phase 3: Synthesize final result
    return this.synthesizeResult();
  }

  private async createPlan(goal: string): Promise<Plan> {
    const toolDescriptions = this.tools.map((t) => `- ${t.name}: ${t.description}`).join('\n');

    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content: `You are a planning assistant. Given a goal, create a step-by-step plan to achieve it.

Available tools:
${toolDescriptions}

Respond with a JSON object containing:
{
  "goal": "the original goal",
  "steps": [
    { "id": 1, "description": "step description" },
    { "id": 2, "description": "step description" },
    ...
  ]
}

Keep plans concise (3-7 steps). Each step should be actionable.`,
        },
        {
          role: 'user',
          content: `Create a plan for: ${goal}`,
        },
      ],
      response_format: { type: 'json_object' },
    });

    const content = response.choices[0].message.content || '{}';
    const planData = JSON.parse(content);

    return {
      goal: planData.goal || goal,
      steps: planData.steps.map((s: { id: number; description: string }) => ({
        ...s,
        status: 'pending' as const,
      })),
    };
  }

  private async executeStep(step: PlanStep): Promise<string> {
    const completedSteps = this.plan!.steps.filter((s) => s.status === 'completed')
      .map((s) => `Step ${s.id}: ${s.description}\nResult: ${s.result}`)
      .join('\n\n');

    const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
      {
        role: 'system',
        content: `You are an execution agent. Execute the given step using available tools.

Goal: ${this.plan!.goal}

${completedSteps ? `Completed steps:\n${completedSteps}\n` : ''}

Current step: ${step.description}

Use tools as needed to complete this step. When done, provide a clear summary of what was accomplished.`,
      },
      {
        role: 'user',
        content: `Execute step ${step.id}: ${step.description}`,
      },
    ];

    // Run execution loop
    for (let i = 0; i < 5; i++) {
      const response = await this.client.chat.completions.create({
        model: this.model,
        messages,
        tools: this.tools.map((tool) => ({
          type: 'function' as const,
          function: {
            name: tool.name,
            description: tool.description,
            parameters: tool.parameters,
          },
        })),
      });

      const message = response.choices[0].message;
      messages.push(message);

      if (!message.tool_calls || message.tool_calls.length === 0) {
        return message.content || 'Step completed';
      }

      // Execute tools
      for (const toolCall of message.tool_calls) {
        const tool = this.tools.find((t) => t.name === toolCall.function.name);
        if (!tool) continue;

        const args = JSON.parse(toolCall.function.arguments);
        const result = await tool.execute(args);

        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: result,
        });
      }
    }

    return 'Step execution completed';
  }

  private async handleFailure(failedStep: PlanStep): Promise<boolean> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content:
            'You are a planning assistant. A step in the plan has failed. Decide whether to retry, skip, or abort.',
        },
        {
          role: 'user',
          content: `The step "${failedStep.description}" failed. 
          
Should we:
1. Retry the step
2. Skip and continue
3. Abort the plan

Respond with just the number (1, 2, or 3).`,
        },
      ],
    });

    const decision = response.choices[0].message.content?.trim();

    if (decision === '1') {
      // Retry
      failedStep.status = 'pending';
      return true;
    } else if (decision === '2') {
      // Skip
      failedStep.status = 'completed';
      failedStep.result = 'Skipped due to failure';
      return true;
    }

    // Abort
    return false;
  }

  private async synthesizeResult(): Promise<string> {
    const stepResults = this.plan!.steps.map(
      (s) => `Step ${s.id}: ${s.description}\nResult: ${s.result || 'N/A'}`
    ).join('\n\n');

    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content:
            'Synthesize the results of the completed plan into a clear, comprehensive response.',
        },
        {
          role: 'user',
          content: `Goal: ${this.plan!.goal}\n\nStep Results:\n${stepResults}\n\nProvide a final summary.`,
        },
      ],
    });

    return response.choices[0].message.content || 'Plan completed.';
  }
}

Chain-of-Thought Reasoning

Chain-of-thought (CoT) prompting improves reasoning by asking the model to think step by step.

Without CoT:

Q: A store has 45 apples. They sell 12 and receive a shipment of 30.
   How many apples do they have now?
A: 63 apples

With CoT:

Q: A store has 45 apples. They sell 12 and receive a shipment of 30.
   How many apples do they have now? Think step by step.
A: Let me work through this:
   1. Starting apples: 45
   2. After selling 12: 45 - 12 = 33
   3. After receiving 30: 33 + 30 = 63
   So the store has 63 apples.

Both get the same answer here, but CoT significantly improves accuracy on complex problems.


Implementing Chain-of-Thought

Add CoT to agent reasoning:

class ReasoningAgent {
  private client: OpenAI;
  private model: string;

  constructor(client: OpenAI, model: string) {
    this.client = client;
    this.model = model;
  }

  async reason(problem: string): Promise<string> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content: `You are a reasoning assistant. When solving problems:

1. First, understand what is being asked
2. Break down the problem into smaller parts
3. Solve each part step by step
4. Verify your reasoning
5. Provide the final answer

Always show your thinking process before giving the answer.`,
        },
        {
          role: 'user',
          content: problem,
        },
      ],
    });

    return response.choices[0].message.content || '';
  }

  async reasonWithStructure(problem: string): Promise<{
    understanding: string;
    steps: string[];
    verification: string;
    answer: string;
  }> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content: `You are a reasoning assistant. Analyze problems systematically.

Respond with a JSON object:
{
  "understanding": "What the problem is asking",
  "steps": ["Step 1 reasoning", "Step 2 reasoning", ...],
  "verification": "Check if the reasoning is correct",
  "answer": "The final answer"
}`,
        },
        {
          role: 'user',
          content: problem,
        },
      ],
      response_format: { type: 'json_object' },
    });

    return JSON.parse(response.choices[0].message.content || '{}');
  }
}

Usage:

const agent = new ReasoningAgent(client, 'gpt-4o');

const result = await agent.reasonWithStructure(
  'If a train travels at 60 mph for 2.5 hours, then at 45 mph for 1.5 hours, what is the total distance traveled?'
);

console.log('Understanding:', result.understanding);
console.log('Steps:', result.steps);
console.log('Verification:', result.verification);
console.log('Answer:', result.answer);

Output:

Understanding: Calculate total distance from two segments with different speeds
Steps: [
  "Segment 1: 60 mph × 2.5 hours = 150 miles",
  "Segment 2: 45 mph × 1.5 hours = 67.5 miles",
  "Total: 150 + 67.5 = 217.5 miles"
]
Verification: 150 + 67.5 = 217.5Answer: 217.5 miles

Goal Decomposition

Complex goals need to be broken into manageable sub-goals:

interface Goal {
  description: string;
  subGoals: Goal[];
  isAtomic: boolean;
}

class GoalDecomposer {
  private client: OpenAI;
  private model: string;

  constructor(client: OpenAI, model: string) {
    this.client = client;
    this.model = model;
  }

  async decompose(goal: string, depth: number = 0): Promise<Goal> {
    if (depth > 3) {
      return { description: goal, subGoals: [], isAtomic: true };
    }

    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content: `You are a goal decomposition assistant. 

Given a goal, determine:
1. Is this goal atomic (can be done in a single action)? 
2. If not atomic, what are the sub-goals needed?

Respond with JSON:
{
  "isAtomic": boolean,
  "subGoals": ["sub-goal 1", "sub-goal 2", ...] // empty if atomic
}`,
        },
        {
          role: 'user',
          content: `Decompose this goal: ${goal}`,
        },
      ],
      response_format: { type: 'json_object' },
    });

    const data = JSON.parse(response.choices[0].message.content || '{}');

    if (data.isAtomic || !data.subGoals?.length) {
      return { description: goal, subGoals: [], isAtomic: true };
    }

    const subGoals = await Promise.all(
      data.subGoals.map((sg: string) => this.decompose(sg, depth + 1))
    );

    return {
      description: goal,
      subGoals,
      isAtomic: false,
    };
  }

  printGoalTree(goal: Goal, indent: number = 0): void {
    const prefix = '  '.repeat(indent);
    const marker = goal.isAtomic ? '[x]' : '[-]';
    console.log(`${prefix}${marker} ${goal.description}`);
    goal.subGoals.forEach((sg) => this.printGoalTree(sg, indent + 1));
  }
}

Usage:

const decomposer = new GoalDecomposer(client, 'gpt-4o');

const goalTree = await decomposer.decompose('Build a personal website with a blog');

decomposer.printGoalTree(goalTree);

Output:

[-] Build a personal website with a blog
  [-] Set up project infrastructure
    [x] Choose a web framework (Next.js, Astro, etc.)
    [x] Initialize the project
    [x] Set up version control
  [-] Design the website
    [x] Create wireframes for main pages
    [x] Choose color scheme and typography
    [x] Design responsive layouts
  [-] Implement the blog
    [x] Create blog post data model
    [x] Build blog listing page
    [x] Build individual post page
    [x] Add markdown support
  [x] Deploy the website

Adaptive Planning

Plans often need adjustment based on execution results:

class AdaptivePlanningAgent {
  private client: OpenAI;
  private model: string;
  private tools: Tool[];

  constructor(client: OpenAI, model: string, tools: Tool[]) {
    this.client = client;
    this.model = model;
    this.tools = tools;
  }

  async run(goal: string): Promise<string> {
    let plan = await this.createPlan(goal);
    const results: Array<{ step: string; result: string }> = [];

    while (plan.steps.length > 0) {
      const currentStep = plan.steps[0];
      console.log(`Executing: ${currentStep}`);

      try {
        const result = await this.executeStep(currentStep, results);
        results.push({ step: currentStep, result });
        plan.steps.shift(); // Remove completed step

        // Check if we need to replan based on result
        const needsReplan = await this.checkNeedsReplan(goal, results, plan);
        if (needsReplan) {
          console.log('Replanning based on new information...');
          plan = await this.replan(goal, results);
        }
      } catch (error) {
        console.log(`Step failed: ${error}`);
        plan = await this.replan(goal, results);
      }
    }

    return this.synthesize(goal, results);
  }

  private async checkNeedsReplan(
    goal: string,
    results: Array<{ step: string; result: string }>,
    currentPlan: { steps: string[] }
  ): Promise<boolean> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content:
            'You are a planning evaluator. Based on execution results, determine if the current plan still makes sense or needs adjustment.',
        },
        {
          role: 'user',
          content: `Goal: ${goal}

Completed steps and results:
${results.map((r) => `- ${r.step}: ${r.result}`).join('\n')}

Remaining plan:
${currentPlan.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}

Does the plan need adjustment? Respond with just "yes" or "no".`,
        },
      ],
    });

    return response.choices[0].message.content?.toLowerCase().includes('yes') || false;
  }

  private async replan(
    goal: string,
    completedResults: Array<{ step: string; result: string }>
  ): Promise<{ steps: string[] }> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content: `You are a planning assistant. Create a new plan considering what has already been done.

Respond with JSON: { "steps": ["step 1", "step 2", ...] }`,
        },
        {
          role: 'user',
          content: `Goal: ${goal}

Already completed:
${completedResults.map((r) => `- ${r.step}: ${r.result}`).join('\n')}

What steps remain to achieve the goal?`,
        },
      ],
      response_format: { type: 'json_object' },
    });

    return JSON.parse(response.choices[0].message.content || '{"steps":[]}');
  }

  private async createPlan(goal: string): Promise<{ steps: string[] }> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content:
            'Create a step-by-step plan. Respond with JSON: { "steps": ["step 1", "step 2", ...] }',
        },
        {
          role: 'user',
          content: `Create a plan for: ${goal}`,
        },
      ],
      response_format: { type: 'json_object' },
    });

    return JSON.parse(response.choices[0].message.content || '{"steps":[]}');
  }

  private async executeStep(
    step: string,
    previousResults: Array<{ step: string; result: string }>
  ): Promise<string> {
    // Implementation similar to previous examples
    return `Completed: ${step}`;
  }

  private async synthesize(
    goal: string,
    results: Array<{ step: string; result: string }>
  ): Promise<string> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [
        {
          role: 'system',
          content: 'Synthesize the execution results into a final response.',
        },
        {
          role: 'user',
          content: `Goal: ${goal}\n\nResults:\n${results.map((r) => `- ${r.step}: ${r.result}`).join('\n')}`,
        },
      ],
    });

    return response.choices[0].message.content || 'Task completed.';
  }
}

Planning Best Practices

1. Keep Plans Manageable

Too many steps overwhelm the agent. Aim for 3-7 high-level steps.

2. Allow for Flexibility

Plans are hypotheses. Build in mechanisms to adjust when reality differs.

3. Validate Steps

After each step, verify it actually succeeded before moving on.

4. Include Checkpoints

For long tasks, add explicit verification points in the plan.

5. Set Resource Limits

Limit iterations, API calls, and execution time to prevent runaway costs.


Key Takeaways

  1. Planning separates strategy from execution - think first, then act
  2. Chain-of-thought reasoning improves accuracy on complex problems
  3. Goal decomposition breaks complex objectives into manageable parts
  4. Adaptive planning adjusts to unexpected results and failures
  5. Plans are not fixed - build agents that can replan when needed

Practice Exercise

Build a planning agent that can:

  1. Accept a research topic
  2. Create a plan with 3-5 research steps
  3. Execute each step (simulated or with real search)
  4. Adapt the plan if it discovers the topic needs different focus
  5. Produce a final summary

Test with: "Research the environmental impact of electric vehicles"


Resources

Resource Type Level
Chain of Thought Prompting Research Paper Intermediate
Plan-and-Solve Prompting Research Paper Advanced
Tree of Thoughts Research Paper Advanced

Next Lesson

In the next lesson, you will learn about multi-agent systems - how to coordinate multiple specialized agents to tackle complex problems.

Continue to Lesson 5.3: Multi-Agent Systems