DRAFT Agentic Design Patterns - Planning

aiagentsplanninglangchainlanggraphtypescript
By sko X opus 4.19/20/202518 min read

Learn how to implement planning patterns that enable AI agents to decompose complex tasks, create multi-step strategies, and execute sophisticated workflows using TypeScript, LangChain, LangGraph, and Google Gemini models on Vercel's serverless platform.

Mental Model: The Construction Site Manager

Think of planning agents like a construction site manager overseeing a building project. The manager doesn't personally lay bricks or install plumbing—instead, they create a comprehensive plan, delegate tasks to specialized workers, monitor progress, and adjust strategies when issues arise. Similarly, planning agents decompose complex problems into manageable steps, orchestrate execution through specialized tools or sub-agents, and adapt plans based on intermediate results. This separation of planning from execution enables handling complexity that would overwhelm a single-pass approach.

Basic Example: Plan-and-Execute Agent

1. Define Agent State Types

// lib/planning/types.ts
import { z } from 'zod';
import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';

// Schema for a single plan step
export const PlanStepSchema = z.object({
  step: z.number(),
  action: z.string(),
  reasoning: z.string(),
  dependencies: z.array(z.number()).default([]),
  status: z.enum(['pending', 'in_progress', 'completed', 'failed']).default('pending'),
  result: z.string().optional(),
});

export type PlanStep = z.infer<typeof PlanStepSchema>;

// State definition using LangGraph annotations
export const PlanningAgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
  plan: Annotation<PlanStep[]>({
    reducer: (current, update) => update,
    default: () => [],
  }),
  currentStep: Annotation<number>({ 
    default: () => 0 
  }),
  executionResults: Annotation<Record<number, any>>({
    reducer: (current, update) => ({ ...current, ...update }),
    default: () => ({}),
  }),
  finalOutput: Annotation<string>({ 
    default: () => '' 
  }),
});

export type AgentState = typeof PlanningAgentState.State;

Defines strongly-typed state structure with plan steps, execution tracking, and message history using LangGraph's Annotation system for state management.

2. Create Planning Node

// lib/planning/nodes/planner.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { map, filter, sortBy } from 'es-toolkit';
import { AgentState, PlanStep } from '../types';

const plannerModel = new ChatGoogleGenerativeAI({
  model: 'gemini-2.5-pro',
  temperature: 0.1,
  maxOutputTokens: 8192,
}).withStructuredOutput({
  name: 'plan',
  description: 'Structured plan for task execution',
  parameters: {
    type: 'object',
    properties: {
      steps: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            step: { type: 'number' },
            action: { type: 'string' },
            reasoning: { type: 'string' },
            dependencies: { 
              type: 'array', 
              items: { type: 'number' } 
            },
          },
          required: ['step', 'action', 'reasoning'],
        },
      },
    },
    required: ['steps'],
  },
});

export async function plannerNode(state: AgentState): Promise<Partial<AgentState>> {
  const userMessage = state.messages[state.messages.length - 1];
  
  const systemPrompt = `You are a planning agent. Break down the user's request into clear, actionable steps.
  Each step should:
  1. Have a clear action to perform
  2. Include reasoning for why this step is necessary
  3. List any dependencies on previous steps (using step numbers)
  
  Ensure steps are ordered logically with dependencies respected.`;
  
  const response = await plannerModel.invoke([
    new SystemMessage(systemPrompt),
    userMessage,
  ]);
  
  // Process and validate steps using es-toolkit
  const processedSteps = map(
    response.steps,
    (step: any, index: number) => ({
      ...step,
      step: index + 1,
      status: 'pending' as const,
      dependencies: step.dependencies || [],
    })
  );
  
  // Sort by dependencies to ensure correct execution order
  const sortedSteps = sortBy(
    processedSteps,
    [(step: PlanStep) => step.dependencies.length, 'step']
  );
  
  return {
    plan: sortedSteps,
    currentStep: 0,
  };
}

Creates a planning node that analyzes user requests and generates structured, dependency-aware execution plans using Gemini 2.5 Pro for high-quality reasoning.

3. Create Execution Node

// lib/planning/nodes/executor.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
import { filter, find, every, map as mapArray } from 'es-toolkit';
import { AgentState, PlanStep } from '../types';

const executorModel = new ChatGoogleGenerativeAI({
  model: 'gemini-2.5-flash',  // Faster, cheaper model for execution
  temperature: 0,
  maxOutputTokens: 2048,
});

export async function executorNode(state: AgentState): Promise<Partial<AgentState>> {
  const { plan, currentStep, executionResults } = state;
  
  // Find next executable step
  const executableStep = find(plan, (step: PlanStep) => {
    // Step must be pending and all dependencies completed
    if (step.status !== 'pending') return false;
    
    const dependenciesMet = every(
      step.dependencies,
      (depId: number) => {
        const depStep = find(plan, (s: PlanStep) => s.step === depId);
        return depStep?.status === 'completed';
      }
    );
    
    return dependenciesMet;
  });
  
  if (!executableStep) {
    // No more steps to execute
    return {
      finalOutput: generateSummary(plan, executionResults),
    };
  }
  
  // Execute the step
  const contextFromDeps = executableStep.dependencies
    .map(depId => {
      const result = executionResults[depId];
      return result ? `Step ${depId} result: ${result}` : '';
    })
    .filter(Boolean)
    .join('\n');
  
  const executionPrompt = `Execute the following action:
  Action: ${executableStep.action}
  Reasoning: ${executableStep.reasoning}
  ${contextFromDeps ? `\nContext from previous steps:\n${contextFromDeps}` : ''}
  
  Provide a concise result of the action execution.`;
  
  const result = await executorModel.invoke([
    new SystemMessage('You are an execution agent. Perform the requested action and return the result.'),
    new HumanMessage(executionPrompt),
  ]);
  
  // Update plan and results using es-toolkit
  const updatedPlan = mapArray(plan, (step: PlanStep) =>
    step.step === executableStep.step
      ? { ...step, status: 'completed' as const, result: result.content as string }
      : step
  );
  
  return {
    plan: updatedPlan,
    executionResults: {
      [executableStep.step]: result.content,
    },
    currentStep: currentStep + 1,
  };
}

function generateSummary(plan: PlanStep[], results: Record<number, any>): string {
  const completedSteps = filter(plan, (s: PlanStep) => s.status === 'completed');
  return `Completed ${completedSteps.length} steps successfully. 
Final results: ${JSON.stringify(results, null, 2)}`;
}

Executes individual plan steps using Gemini 2.5 Flash for cost efficiency, managing dependencies and accumulating results for subsequent steps.

4. Build the Planning Graph

// lib/planning/graph.ts
import { StateGraph, END, MemorySaver } from '@langchain/langgraph';
import { AgentState } from './types';
import { plannerNode } from './nodes/planner';
import { executorNode } from './nodes/executor';
import { every } from 'es-toolkit';

export function createPlanningGraph() {
  const workflow = new StateGraph<AgentState>({
    stateSchema: AgentState,
  });
  
  // Add nodes
  workflow.addNode('planner', plannerNode);
  workflow.addNode('executor', executorNode);
  
  // Define conditional edges
  workflow.addConditionalEdges(
    'executor',
    (state: AgentState) => {
      // Check if all steps are completed using es-toolkit
      const allCompleted = every(
        state.plan,
        step => step.status === 'completed' || step.status === 'failed'
      );
      
      return allCompleted ? 'end' : 'continue';
    },
    {
      end: END,
      continue: 'executor',
    }
  );
  
  // Set up flow
  workflow.setEntryPoint('planner');
  workflow.addEdge('planner', 'executor');
  
  return workflow.compile({
    checkpointer: new MemorySaver(), // For serverless, use external storage in production
  });
}

Assembles the planning workflow with conditional execution logic, enabling iterative step execution until all tasks complete.

5. API Route for Planning Agent

// app/api/planning/route.ts
import { createPlanningGraph } from '@/lib/planning/graph';
import { HumanMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';
import { v4 as uuidv4 } from 'uuid';

export const runtime = 'nodejs';
export const maxDuration = 300;

export async function POST(req: Request) {
  try {
    const { message, sessionId = uuidv4() } = await req.json();
    
    const graph = createPlanningGraph();
    
    // Stream events for real-time updates
    const encoder = new TextEncoder();
    const stream = new TransformStream();
    const writer = stream.writable.getWriter();
    
    (async () => {
      try {
        const events = graph.stream(
          {
            messages: [new HumanMessage(message)],
          },
          {
            configurable: { thread_id: sessionId },
            streamMode: 'values',
          }
        );
        
        for await (const event of events) {
          // Send plan updates to frontend
          if (event.plan) {
            await writer.write(
              encoder.encode(`data: ${JSON.stringify({
                type: 'plan_update',
                plan: event.plan,
                currentStep: event.currentStep,
              })}\n\n`)
            );
          }
          
          // Send final output
          if (event.finalOutput) {
            await writer.write(
              encoder.encode(`data: ${JSON.stringify({
                type: 'complete',
                output: event.finalOutput,
              })}\n\n`)
            );
          }
        }
      } catch (error) {
        console.error('Streaming error:', error);
        await writer.write(
          encoder.encode(`data: ${JSON.stringify({
            type: 'error',
            error: 'Processing failed',
          })}\n\n`)
        );
      } finally {
        await writer.close();
      }
    })();
    
    return new Response(stream.readable, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      },
    });
  } catch (error) {
    console.error('Planning API error:', error);
    return NextResponse.json(
      { error: 'Failed to process request' },
      { status: 500 }
    );
  }
}

Exposes the planning agent via streaming API endpoint, providing real-time updates as the agent progresses through plan execution.

6. Frontend Component with React Query

// components/PlanningInterface.tsx
'use client';

import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { map } from 'es-toolkit';

interface PlanStep {
  step: number;
  action: string;
  reasoning: string;
  status: 'pending' | 'in_progress' | 'completed' | 'failed';
  result?: string;
}

export default function PlanningInterface() {
  const [input, setInput] = useState('');
  const [plan, setPlan] = useState<PlanStep[]>([]);
  const [output, setOutput] = useState('');
  
  const executePlan = useMutation({
    mutationFn: async (message: string) => {
      const response = await fetch('/api/planning', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message }),
      });
      
      if (!response.ok) throw new Error('Failed to execute plan');
      if (!response.body) throw new Error('No response body');
      
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              
              if (data.type === 'plan_update') {
                setPlan(data.plan);
              } else if (data.type === 'complete') {
                setOutput(data.output);
              }
            } catch (e) {
              console.error('Parse error:', e);
            }
          }
        }
      }
    },
  });
  
  const getStatusBadge = (status: string) => {
    const badges = {
      pending: 'badge-ghost',
      in_progress: 'badge-info',
      completed: 'badge-success',
      failed: 'badge-error',
    };
    return badges[status as keyof typeof badges] || 'badge-ghost';
  };
  
  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">Planning Agent</h2>
        
        <form onSubmit={(e) => {
          e.preventDefault();
          executePlan.mutate(input);
        }}>
          <div className="form-control">
            <textarea
              className="textarea textarea-bordered h-24"
              placeholder="Describe a complex task..."
              value={input}
              onChange={(e) => setInput(e.target.value)}
              disabled={executePlan.isPending}
            />
          </div>
          
          <div className="card-actions justify-end mt-4">
            <button 
              type="submit" 
              className="btn btn-primary"
              disabled={!input || executePlan.isPending}
            >
              {executePlan.isPending ? (
                <>
                  <span className="loading loading-spinner"></span>
                  Planning & Executing...
                </>
              ) : 'Create Plan & Execute'}
            </button>
          </div>
        </form>
        
        {plan.length > 0 && (
          <div className="mt-6">
            <h3 className="font-bold mb-2">Execution Plan:</h3>
            <ul className="steps steps-vertical">
              {map(plan, (step) => (
                <li 
                  key={step.step}
                  className={`step ${step.status === 'completed' ? 'step-primary' : ''}`}
                >
                  <div className="text-left">
                    <div className="flex items-center gap-2">
                      <span className="font-semibold">{step.action}</span>
                      <span className={`badge badge-sm ${getStatusBadge(step.status)}`}>
                        {step.status}
                      </span>
                    </div>
                    <p className="text-sm opacity-70">{step.reasoning}</p>
                    {step.result && (
                      <div className="mt-2 p-2 bg-base-200 rounded text-sm">
                        {step.result}
                      </div>
                    )}
                  </div>
                </li>
              ))}
            </ul>
          </div>
        )}
        
        {output && (
          <div className="alert alert-success mt-4">
            <span>{output}</span>
          </div>
        )}
        
        {executePlan.isError && (
          <div className="alert alert-error mt-4">
            <span>Failed to execute plan. Please try again.</span>
          </div>
        )}
      </div>
    </div>
  );
}

React component that visualizes plan creation and execution in real-time, showing step progress and results using DaisyUI components.

Advanced Example: ReAct Agent with Tree of Thoughts

1. Additional Dependencies Already Installed

// All dependencies from project setup are already available:
// @langchain/google-genai - Google AI models
// @langchain/langgraph - Stateful workflows  
// es-toolkit, es-toolkit/compat - Functional utilities
// zod - Schema validation
// @tanstack/react-query - Data fetching

No additional installations needed - all required packages are pre-installed in the project setup.

// lib/advanced-planning/types.ts
import { z } from 'zod';
import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';

// Thought node for tree structure
export const ThoughtNodeSchema = z.object({
  id: z.string(),
  content: z.string(),
  score: z.number(),
  depth: z.number(),
  parentId: z.string().nullable(),
  children: z.array(z.string()).default([]),
  isTerminal: z.boolean().default(false),
  metadata: z.record(z.any()).optional(),
});

export type ThoughtNode = z.infer<typeof ThoughtNodeSchema>;

// Enhanced state with tree exploration
export const TreeAgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
  thoughtTree: Annotation<Map<string, ThoughtNode>>({
    reducer: (current, update) => new Map([...current, ...update]),
    default: () => new Map(),
  }),
  currentNodeId: Annotation<string | null>({ 
    default: () => null 
  }),
  bestPath: Annotation<string[]>({
    reducer: (current, update) => update,
    default: () => [],
  }),
  explorationBudget: Annotation<number>({ 
    default: () => 10 
  }),
  iterationCount: Annotation<number>({
    reducer: (current, update) => current + update,
    default: () => 0,
  }),
});

export type TreeState = typeof TreeAgentState.State;

Defines tree-based exploration state supporting branching thought processes with scoring and depth tracking for sophisticated reasoning.

2. Implement ReAct Pattern with Tree Exploration

// lib/advanced-planning/nodes/react-tree.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { SystemMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { maxBy, filter, map, sortBy, take } from 'es-toolkit';
import { v4 as uuidv4 } from 'uuid';
import { TreeState, ThoughtNode } from '../types';

const reasoningModel = new ChatGoogleGenerativeAI({
  model: 'gemini-2.5-pro',
  temperature: 0.7,
  maxOutputTokens: 8192,
}).withStructuredOutput({
  name: 'thought_expansion',
  description: 'Generate multiple reasoning paths',
  parameters: {
    type: 'object',
    properties: {
      thoughts: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            reasoning: { type: 'string' },
            action: { type: 'string' },
            confidence: { type: 'number' },
          },
        },
      },
    },
  },
});

export async function reactTreeNode(state: TreeState): Promise<Partial<TreeState>> {
  const { thoughtTree, currentNodeId, explorationBudget, iterationCount } = state;
  
  // Check exploration budget
  if (iterationCount >= explorationBudget) {
    return selectBestPath(state);
  }
  
  // Get current context
  const currentNode = currentNodeId ? thoughtTree.get(currentNodeId) : null;
  const context = buildContext(state, currentNode);
  
  // Generate multiple thought branches
  const thoughtPrompt = `Given the current context, generate 3 different reasoning paths.
  Each path should:
  1. Provide unique reasoning approach
  2. Suggest a specific action
  3. Estimate confidence (0-1) in the approach
  
  Context: ${context}
  
  Current thought: ${currentNode?.content || 'Initial state'}`;
  
  const response = await reasoningModel.invoke([
    new SystemMessage('You are a reasoning agent exploring multiple solution paths.'),
    new HumanMessage(thoughtPrompt),
  ]);
  
  // Create new thought nodes
  const newNodes = new Map<string, ThoughtNode>();
  const parentDepth = currentNode?.depth || 0;
  
  for (const thought of response.thoughts) {
    const nodeId = uuidv4();
    const node: ThoughtNode = {
      id: nodeId,
      content: `${thought.reasoning} → ${thought.action}`,
      score: thought.confidence * (1 / (parentDepth + 1)), // Decay score with depth
      depth: parentDepth + 1,
      parentId: currentNodeId,
      children: [],
      isTerminal: false,
      metadata: { action: thought.action },
    };
    
    newNodes.set(nodeId, node);
    
    // Update parent's children
    if (currentNode) {
      currentNode.children.push(nodeId);
      thoughtTree.set(currentNodeId!, {
        ...currentNode,
        children: currentNode.children,
      });
    }
  }
  
  // Select next node to explore using es-toolkit (highest score)
  const nextNode = maxBy(
    Array.from(newNodes.values()),
    (node: ThoughtNode) => node.score
  );
  
  return {
    thoughtTree: new Map([...thoughtTree, ...newNodes]),
    currentNodeId: nextNode?.id || null,
    iterationCount: 1,
  };
}

function buildContext(state: TreeState, currentNode: ThoughtNode | null): string {
  if (!currentNode) {
    return state.messages[state.messages.length - 1]?.content as string || '';
  }
  
  // Build path from root to current using es-toolkit
  const path: ThoughtNode[] = [];
  let node: ThoughtNode | null = currentNode;
  
  while (node) {
    path.unshift(node);
    node = node.parentId ? state.thoughtTree.get(node.parentId) || null : null;
  }
  
  return map(path, (n: ThoughtNode) => n.content).join(' → ');
}

function selectBestPath(state: TreeState): Partial<TreeState> {
  const { thoughtTree } = state;
  
  // Find terminal nodes or deepest nodes using es-toolkit
  const allNodes = Array.from(thoughtTree.values());
  const terminalNodes = filter(allNodes, (n: ThoughtNode) => 
    n.isTerminal || n.children.length === 0
  );
  
  // Select best terminal node
  const bestNode = maxBy(terminalNodes, (n: ThoughtNode) => n.score);
  
  if (!bestNode) {
    return { bestPath: [] };
  }
  
  // Reconstruct path
  const path: string[] = [];
  let current: ThoughtNode | null = bestNode;
  
  while (current) {
    path.unshift(current.id);
    current = current.parentId ? thoughtTree.get(current.parentId) || null : null;
  }
  
  return { bestPath: path };
}

Implements tree-based exploration combining ReAct pattern with Tree of Thoughts using Gemini 2.5 Pro, enabling systematic exploration of multiple reasoning paths.

3. Action Execution with Reflection

// lib/advanced-planning/nodes/reflective-executor.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { find } from 'es-toolkit';
import { TreeState, ThoughtNode } from '../types';

// Define tools with schemas
const searchTool = new DynamicStructuredTool({
  name: 'search',
  description: 'Search for information',
  schema: z.object({
    query: z.string(),
  }),
  func: async ({ query }) => {
    // Simulate search
    return `Search results for "${query}": [relevant information]`;
  },
});

const calculateTool = new DynamicStructuredTool({
  name: 'calculate',
  description: 'Perform calculations',
  schema: z.object({
    expression: z.string(),
  }),
  func: async ({ expression }) => {
    // Use math.js or similar for real calculations
    try {
      // Safe evaluation for demo purposes
      const result = Function('"use strict"; return (' + expression + ')')();
      return `Calculation result: ${result}`;
    } catch (e) {
      return `Calculation error: ${e}`;
    }
  },
});

const tools = [searchTool, calculateTool];

export async function reflectiveExecutor(state: TreeState): Promise<Partial<TreeState>> {
  const { thoughtTree, currentNodeId } = state;
  
  if (!currentNodeId) return {};
  
  const currentNode = thoughtTree.get(currentNodeId);
  if (!currentNode?.metadata?.action) return {};
  
  const action = currentNode.metadata.action as string;
  
  // Execute action
  let result: string;
  try {
    // Parse action and execute appropriate tool
    const toolMatch = action.match(/(\w+)\((.*)\)/);
    if (toolMatch) {
      const [, toolName, args] = toolMatch;
      const tool = find(tools, (t) => t.name === toolName);
      
      if (tool) {
        // Parse arguments safely
        const parsedArgs = args ? { [toolName === 'search' ? 'query' : 'expression']: args.replace(/['"]/g, '') } : {};
        result = await tool.func(parsedArgs);
      } else {
        result = `Unknown tool: ${toolName}`;
      }
    } else {
      result = 'No valid action to execute';
    }
  } catch (error) {
    result = `Execution error: ${error}`;
  }
  
  // Reflect on result using Gemini Flash
  const reflectionModel = new ChatGoogleGenerativeAI({
    model: 'gemini-2.5-flash',
    temperature: 0,
    maxOutputTokens: 1024,
  });
  
  const reflection = await reflectionModel.invoke([
    new SystemMessage('Evaluate if this result successfully addresses the goal.'),
    new HumanMessage(`Action: ${action}\nResult: ${result}\nDoes this achieve our objective?`),
  ]);
  
  // Update node with reflection
  const updatedNode: ThoughtNode = {
    ...currentNode,
    isTerminal: reflection.content?.includes('success') || reflection.content?.includes('achieved') || false,
    metadata: {
      ...currentNode.metadata,
      result,
      reflection: reflection.content,
    },
  };
  
  thoughtTree.set(currentNodeId, updatedNode);
  
  return {
    thoughtTree,
  };
}

Executes actions from thought nodes with reflection using Gemini 2.5 Flash to evaluate success, enabling the agent to learn from execution outcomes.

4. Orchestration Graph with Parallel Exploration

// lib/advanced-planning/graph.ts
import { StateGraph, END } from '@langchain/langgraph';
import { filter } from 'es-toolkit';
import { TreeState, ThoughtNode } from './types';
import { reactTreeNode } from './nodes/react-tree';
import { reflectiveExecutor } from './nodes/reflective-executor';

export function createAdvancedPlanningGraph() {
  const workflow = new StateGraph<TreeState>({
    stateSchema: TreeState,
  });
  
  // Add nodes
  workflow.addNode('think', reactTreeNode);
  workflow.addNode('act', reflectiveExecutor);
  workflow.addNode('select_best', selectBestPathNode);
  
  // Conditional routing
  workflow.addConditionalEdges(
    'think',
    (state: TreeState) => {
      // Check if we should continue exploring or execute
      const { iterationCount, explorationBudget } = state;
      
      if (iterationCount >= explorationBudget) {
        return 'select';
      }
      
      // Check if current node needs action
      const currentNode = state.currentNodeId 
        ? state.thoughtTree.get(state.currentNodeId)
        : null;
      
      return currentNode?.metadata?.action ? 'execute' : 'explore';
    },
    {
      explore: 'think',
      execute: 'act',
      select: 'select_best',
    }
  );
  
  workflow.addConditionalEdges(
    'act',
    (state: TreeState) => {
      const currentNode = state.currentNodeId 
        ? state.thoughtTree.get(state.currentNodeId)
        : null;
      
      return currentNode?.isTerminal ? 'end' : 'continue';
    },
    {
      end: END,
      continue: 'think',
    }
  );
  
  workflow.addEdge('select_best', END);
  workflow.setEntryPoint('think');
  
  return workflow.compile();
}

async function selectBestPathNode(state: TreeState): Promise<Partial<TreeState>> {
  const { thoughtTree, bestPath } = state;
  
  if (bestPath.length === 0) {
    return { finalOutput: 'No solution found within exploration budget' };
  }
  
  // Compile results from best path using es-toolkit
  const pathNodes = filter(
    bestPath.map(id => thoughtTree.get(id)),
    (node): node is ThoughtNode => node !== undefined
  );
  
  const solution = pathNodes
    .map(node => ({
      step: node.content,
      result: node.metadata?.result || 'N/A',
    }));
  
  return {
    finalOutput: JSON.stringify(solution, null, 2),
  };
}

Orchestrates complex reasoning with conditional branching between thinking, acting, and path selection phases for optimal problem-solving.

5. Performance Optimization with Caching

// lib/advanced-planning/cache.ts
import { hash } from 'es-toolkit/compat';
import { isEqual } from 'es-toolkit';

interface CacheEntry {
  result: any;
  timestamp: number;
  ttl: number;
}

// For Vercel serverless, use in-memory cache or external service like Redis
// This is a simplified in-memory version for demonstration
class InMemoryCache {
  private cache = new Map<string, CacheEntry>();
  
  get(key: string): any | null {
    const entry = this.cache.get(key);
    if (!entry) return null;
    
    if (Date.now() - entry.timestamp > entry.ttl * 1000) {
      this.cache.delete(key);
      return null;
    }
    
    return entry.result;
  }
  
  set(key: string, value: any, ttl: number): void {
    this.cache.set(key, {
      result: value,
      timestamp: Date.now(),
      ttl,
    });
  }
  
  delete(key: string): void {
    this.cache.delete(key);
  }
}

export class PlanningCache {
  private readonly prefix = 'planning:';
  private readonly defaultTTL = 3600; // 1 hour
  private store = new InMemoryCache();
  
  async get(key: string): Promise<any | null> {
    try {
      const cacheKey = this.generateKey(key);
      return this.store.get(cacheKey);
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  }
  
  async set(key: string, value: any, ttl?: number): Promise<void> {
    try {
      const cacheKey = this.generateKey(key);
      this.store.set(cacheKey, value, ttl || this.defaultTTL);
    } catch (error) {
      console.error('Cache set error:', error);
    }
  }
  
  private generateKey(input: string): string {
    // Use hash from es-toolkit/compat for consistent key generation
    const hashed = hash(input);
    return `${this.prefix}${hashed}`;
  }
  
  // Semantic similarity caching for similar queries
  async findSimilar(
    query: string,
    threshold: number = 0.85
  ): Promise<any | null> {
    try {
      // In production, use vector similarity search with embeddings
      // This is a simplified token overlap version
      const queryTokens = new Set(query.toLowerCase().split(' '));
      
      // Check cached queries for similarity
      for (const [key, entry] of (this.store as any).cache) {
        if (!key.startsWith(this.prefix)) continue;
        
        // Extract original query from metadata if stored
        const similarity = this.calculateSimilarity(query, key);
        if (similarity > threshold) {
          return entry.result;
        }
      }
      
      return null;
    } catch (error) {
      console.error('Similarity search error:', error);
      return null;
    }
  }
  
  private calculateSimilarity(query1: string, query2: string): number {
    // Simple Jaccard similarity for demonstration
    const set1 = new Set(query1.toLowerCase().split(' '));
    const set2 = new Set(query2.toLowerCase().split(' '));
    
    const intersection = new Set([...set1].filter(x => set2.has(x)));
    const union = new Set([...set1, ...set2]);
    
    return intersection.size / union.size;
  }
}

// Export singleton instance for serverless
export const planningCache = new PlanningCache();

Implements intelligent caching with TTL and semantic similarity matching using es-toolkit utilities to reduce redundant LLM calls and improve response times.

6. Frontend Visualization of Tree Exploration

// components/TreeVisualization.tsx
'use client';

import { useEffect, useRef } from 'react';
import { ThoughtNode } from '@/lib/advanced-planning/types';

interface TreeVisualizationProps {
  thoughtTree: Map<string, ThoughtNode>;
  currentNodeId: string | null;
  bestPath: string[];
}

export default function TreeVisualization({
  thoughtTree,
  currentNodeId,
  bestPath,
}: TreeVisualizationProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  
  useEffect(() => {
    if (!canvasRef.current) return;
    
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    
    // Clear canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // Calculate positions for nodes
    const positions = calculateTreeLayout(thoughtTree);
    
    // Draw edges
    thoughtTree.forEach((node) => {
      const nodePos = positions.get(node.id);
      if (!nodePos) return;
      
      node.children.forEach((childId) => {
        const childPos = positions.get(childId);
        if (!childPos) return;
        
        ctx.beginPath();
        ctx.moveTo(nodePos.x, nodePos.y);
        ctx.lineTo(childPos.x, childPos.y);
        
        // Highlight best path
        if (bestPath.includes(node.id) && bestPath.includes(childId)) {
          ctx.strokeStyle = '#10b981';
          ctx.lineWidth = 3;
        } else {
          ctx.strokeStyle = '#6b7280';
          ctx.lineWidth = 1;
        }
        
        ctx.stroke();
      });
    });
    
    // Draw nodes
    thoughtTree.forEach((node) => {
      const pos = positions.get(node.id);
      if (!pos) return;
      
      ctx.beginPath();
      ctx.arc(pos.x, pos.y, 20, 0, 2 * Math.PI);
      
      // Color based on state
      if (node.id === currentNodeId) {
        ctx.fillStyle = '#3b82f6'; // Current node - blue
      } else if (bestPath.includes(node.id)) {
        ctx.fillStyle = '#10b981'; // Best path - green
      } else if (node.isTerminal) {
        ctx.fillStyle = '#f59e0b'; // Terminal - orange
      } else {
        ctx.fillStyle = '#e5e7eb'; // Default - gray
      }
      
      ctx.fill();
      ctx.strokeStyle = '#1f2937';
      ctx.lineWidth = 2;
      ctx.stroke();
      
      // Draw score
      ctx.fillStyle = '#1f2937';
      ctx.font = '12px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(node.score.toFixed(2), pos.x, pos.y);
    });
  }, [thoughtTree, currentNodeId, bestPath]);
  
  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h3 className="card-title">Thought Tree Exploration</h3>
        <canvas
          ref={canvasRef}
          width={800}
          height={400}
          className="border border-base-300 rounded-lg"
        />
        <div className="flex gap-4 mt-4 text-sm">
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-blue-500 rounded-full"></div>
            <span>Current</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-green-500 rounded-full"></div>
            <span>Best Path</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-orange-500 rounded-full"></div>
            <span>Terminal</span>
          </div>
        </div>
      </div>
    </div>
  );
}

function calculateTreeLayout(
  thoughtTree: Map<string, ThoughtNode>
): Map<string, { x: number; y: number }> {
  const positions = new Map<string, { x: number; y: number }>();
  const levelCounts = new Map<number, number>();
  
  // Count nodes per level
  thoughtTree.forEach((node) => {
    const count = levelCounts.get(node.depth) || 0;
    levelCounts.set(node.depth, count + 1);
  });
  
  // Calculate positions
  const levelIndices = new Map<number, number>();
  thoughtTree.forEach((node) => {
    const levelIndex = levelIndices.get(node.depth) || 0;
    const levelCount = levelCounts.get(node.depth) || 1;
    
    const x = (800 / (levelCount + 1)) * (levelIndex + 1);
    const y = 50 + node.depth * 100;
    
    positions.set(node.id, { x, y });
    levelIndices.set(node.depth, levelIndex + 1);
  });
  
  return positions;
}

Visualizes the thought tree exploration in real-time, showing current node, best path, and terminal nodes with interactive canvas rendering.

Conclusion

Planning patterns transform AI agents from simple responders into sophisticated problem-solvers capable of handling complex, multi-step tasks. By implementing Plan-and-Execute for structured workflows and combining ReAct with Tree of Thoughts for exploratory reasoning, you can build agents that match or exceed human-level planning capabilities.

The key insights from these implementations:

  • Separation of concerns between planning and execution enables using optimal models for each phase (Gemini 2.5 Pro for planning, Gemini 2.5 Flash for execution)
  • Tree exploration with scoring mechanisms finds better solutions than linear approaches
  • Caching and optimization make sophisticated planning economically viable in production
  • Real-time visualization helps users understand and trust agent decision-making
  • es-toolkit usage throughout ensures clean, functional code that's optimized for serverless environments

These patterns, running on Vercel's serverless platform with TypeScript, LangGraph, and Google Gemini models, provide the foundation for building production-ready planning agents that can decompose complexity, explore alternatives, and deliver reliable results at scale within the 777-second execution window. transform AI agents from simple responders into sophisticated problem-solvers capable of handling complex, multi-step tasks. By implementing Plan-and-Execute for structured workflows and combining ReAct with Tree of Thoughts for exploratory reasoning, you can build agents that match or exceed human-level planning capabilities.

The key insights from these implementations:

  • Separation of concerns between planning and execution enables using optimal models for each phase
  • Tree exploration with scoring mechanisms finds better solutions than linear approaches
  • Caching and optimization make sophisticated planning economically viable in production
  • Real-time visualization helps users understand and trust agent decision-making

These patterns, running on Vercel's serverless platform with TypeScript and LangGraph, provide the foundation for building production-ready planning agents that can decompose complexity, explore alternatives, and deliver reliable results at scale.