DRAFT Agentic Design Patterns - Planning
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.