DRAFT Agentic Design Patterns - Reflection

ailangchainlanggraphtypescriptreflectionagents
By sko X opus 4.19/20/202511 min read

Learn how to implement self-improving AI agents using the Reflection pattern with LangChain, LangGraph, and TypeScript on Vercel's serverless platform.

Mental Model: The Code Review Analogy

Think of the Reflection pattern like a pull request review process. When you submit code, a reviewer (the Critic agent) examines it, provides feedback, and you (the Producer agent) revise it based on that feedback. This cycle continues until the code meets quality standards or reaches a merge deadline. In AI agents, this same principle enables iterative self-improvement through structured feedback loops. Just as code reviews catch bugs and improve quality, reflection patterns help agents identify and correct their own mistakes, leading to more accurate and reliable outputs.

Basic Example: Self-Reflection Agent

1. Create the Reflection State Graph

// lib/agents/reflection-basic.ts
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { take } from "es-toolkit";

const ReflectionState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
  }),
  reflectionCount: Annotation<number>({
    reducer: (x, y) => y ?? x,
    default: () => 0,
  }),
});

const model = new ChatGoogleGenerativeAI({
  modelName: "gemini-2.5-flash",
  temperature: 0.7,
});

const generatePrompt = ChatPromptTemplate.fromMessages([
  ["system", "You are an expert essay writer. Generate a response to the user's request."],
  new MessagesPlaceholder("messages"),
]);

const reflectPrompt = ChatPromptTemplate.fromMessages([
  ["system", `You are a writing critic. Review the essay and provide specific, actionable feedback.
   If the essay is excellent, respond with only "APPROVED".
   Otherwise, list 2-3 specific improvements needed.`],
  new MessagesPlaceholder("messages"),
]);

Creates the basic state structure with message history and reflection counter. The state tracks both the conversation and how many reflection cycles have occurred.

2. Implement Generation and Reflection Nodes

// lib/agents/reflection-basic.ts (continued)
async function generateNode(state: typeof ReflectionState.State) {
  const chain = generatePrompt.pipe(model);
  const response = await chain.invoke({
    messages: state.messages
  });
  
  return {
    messages: [response],
  };
}

async function reflectNode(state: typeof ReflectionState.State) {
  const chain = reflectPrompt.pipe(model);
  const lastMessages = take(state.messages, -2); // Get last user message and AI response
  
  const critique = await chain.invoke({
    messages: lastMessages
  });
  
  return {
    messages: [new HumanMessage(`Feedback: ${critique.content}`)],
    reflectionCount: state.reflectionCount + 1,
  };
}

function shouldContinue(state: typeof ReflectionState.State) {
  const lastMessage = state.messages[state.messages.length - 1];
  
  // Stop if approved or max reflections reached
  if (lastMessage.content?.toString().includes("APPROVED") || 
      state.reflectionCount >= 3) {
    return END;
  }
  
  return "reflect";
}

The generation node creates initial content while the reflection node critiques it. The shouldContinue function implements stopping logic based on quality approval or iteration limits.

3. Build the Workflow Graph

// lib/agents/reflection-basic.ts (continued)
export function createReflectionAgent() {
  const workflow = new StateGraph(ReflectionState)
    .addNode("generate", generateNode)
    .addNode("reflect", reflectNode)
    .addEdge(START, "generate")
    .addConditionalEdges("generate", shouldContinue, {
      reflect: "reflect",
      [END]: END,
    })
    .addEdge("reflect", "generate");
  
  return workflow.compile();
}

Assembles the workflow with conditional edges that control the reflection loop flow.

4. Create API Route

// app/api/reflection/route.ts
import { createReflectionAgent } from "@/lib/agents/reflection-basic";
import { HumanMessage } from "@langchain/core/messages";
import { NextResponse } from "next/server";

export const runtime = "nodejs";
export const maxDuration = 60;

export async function POST(req: Request) {
  try {
    const { prompt } = await req.json();
    const agent = createReflectionAgent();
    
    const result = await agent.invoke({
      messages: [new HumanMessage(prompt)],
      reflectionCount: 0,
    });
    
    // Extract the final refined output
    const finalOutput = result.messages
      .filter((m: any) => m._getType() === "ai")
      .pop()?.content;
    
    return NextResponse.json({
      output: finalOutput,
      iterations: result.reflectionCount,
      messages: result.messages.map((m: any) => ({
        type: m._getType(),
        content: m.content,
      })),
    });
  } catch (error) {
    console.error("Reflection error:", error);
    return NextResponse.json(
      { error: "Reflection process failed" },
      { status: 500 }
    );
  }
}

Handles HTTP requests and manages the reflection agent execution with proper error handling.

5. Build Frontend Component

// components/ReflectionDemo.tsx
"use client";

import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { groupBy } from "es-toolkit";

interface ReflectionResult {
  output: string;
  iterations: number;
  messages: Array<{ type: string; content: string }>;
}

export default function ReflectionDemo() {
  const [prompt, setPrompt] = useState("");
  const [showProcess, setShowProcess] = useState(false);
  
  const reflection = useMutation({
    mutationFn: async (userPrompt: string) => {
      const res = await fetch("/api/reflection", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt: userPrompt }),
      });
      
      if (!res.ok) throw new Error("Reflection failed");
      return res.json() as Promise<ReflectionResult>;
    },
  });
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (prompt.trim()) {
      reflection.mutate(prompt);
    }
  };
  
  // Group messages by iteration for display
  const messagesByIteration = reflection.data?.messages
    ? groupBy(reflection.data.messages, (_, index) => 
        Math.floor(index / 2).toString()
      )
    : {};
  
  return (
    <div className="card w-full bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">Reflection Agent Demo</h2>
        
        <form onSubmit={handleSubmit}>
          <textarea
            className="textarea textarea-bordered w-full"
            placeholder="Enter your writing prompt..."
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            rows={3}
            disabled={reflection.isPending}
          />
          
          <div className="card-actions justify-between mt-4">
            <label className="label cursor-pointer">
              <span className="label-text mr-2">Show Process</span>
              <input
                type="checkbox"
                className="checkbox"
                checked={showProcess}
                onChange={(e) => setShowProcess(e.target.checked)}
              />
            </label>
            
            <button
              type="submit"
              className="btn btn-primary"
              disabled={reflection.isPending || !prompt.trim()}
            >
              {reflection.isPending ? (
                <>
                  <span className="loading loading-spinner"></span>
                  Reflecting...
                </>
              ) : "Generate"}
            </button>
          </div>
        </form>
        
        {reflection.data && (
          <div className="mt-6 space-y-4">
            <div className="stats shadow">
              <div className="stat">
                <div className="stat-title">Reflection Iterations</div>
                <div className="stat-value">{reflection.data.iterations}</div>
              </div>
            </div>
            
            {showProcess && (
              <div className="space-y-4">
                {Object.entries(messagesByIteration).map(([iter, msgs]) => (
                  <div key={iter} className="collapse collapse-arrow bg-base-200">
                    <input type="checkbox" />
                    <div className="collapse-title font-medium">
                      Iteration {parseInt(iter) + 1}
                    </div>
                    <div className="collapse-content">
                      {msgs.map((msg, idx) => (
                        <div key={idx} className={`chat chat-${msg.type === "ai" ? "end" : "start"}`}>
                          <div className={`chat-bubble ${msg.type === "human" ? "chat-bubble-primary" : ""}`}>
                            {msg.content}
                          </div>
                        </div>
                      ))}
                    </div>
                  </div>
                ))}
              </div>
            )}
            
            <div className="divider">Final Output</div>
            <div className="prose max-w-none">
              {reflection.data.output}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

Provides an interactive UI to demonstrate the reflection process with collapsible iteration views.

Advanced Example: Producer-Critic Architecture with Streaming

1. Define Producer and Critic Agents

// lib/agents/producer-critic.ts
import { StateGraph, END, START, Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { z } from "zod";
import { minBy, maxBy } from "es-toolkit";
import { StructuredOutputParser } from "@langchain/core/output_parsers";

const CritiqueSchema = z.object({
  score: z.number().min(0).max(100),
  approved: z.boolean(),
  issues: z.array(z.object({
    category: z.enum(["accuracy", "clarity", "completeness", "style"]),
    description: z.string(),
    severity: z.enum(["minor", "major", "critical"]),
  })),
  suggestions: z.array(z.string()),
});

const ProducerCriticState = Annotation.Root({
  task: Annotation<string>(),
  drafts: Annotation<string[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  critiques: Annotation<typeof CritiqueSchema._type[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  iteration: Annotation<number>({
    reducer: (_, y) => y,
    default: () => 0,
  }),
});

const producer = new ChatGoogleGenerativeAI({
  modelName: "gemini-2.5-pro",
  temperature: 0.7,
  maxOutputTokens: 2048,
});

const critic = new ChatGoogleGenerativeAI({
  modelName: "gemini-2.5-flash",
  temperature: 0.3,
});

Defines separate models for producer and critic roles with structured critique output schema.

2. Implement Producer Node with Context

// lib/agents/producer-critic.ts (continued)
async function producerNode(state: typeof ProducerCriticState.State) {
  const lastCritique = state.critiques[state.critiques.length - 1];
  
  let prompt = `Task: ${state.task}`;
  
  if (lastCritique) {
    const criticalIssues = lastCritique.issues
      .filter(i => i.severity === "critical")
      .map(i => `- ${i.description}`)
      .join("\n");
    
    prompt += `\n\nPrevious draft received feedback. Critical issues to address:\n${criticalIssues}`;
    prompt += `\n\nSuggestions for improvement:\n${lastCritique.suggestions.join("\n")}`;
    prompt += `\n\nGenerate an improved version addressing all feedback.`;
  } else {
    prompt += "\n\nGenerate a high-quality response.";
  }
  
  const response = await producer.invoke(prompt);
  
  return {
    drafts: [response.content as string],
    iteration: state.iteration + 1,
  };
}

Producer node incorporates previous critique feedback to generate improved drafts.

3. Implement Critic Node with Structured Output

// lib/agents/producer-critic.ts (continued)
async function criticNode(state: typeof ProducerCriticState.State) {
  const latestDraft = state.drafts[state.drafts.length - 1];
  const parser = StructuredOutputParser.fromZodSchema(CritiqueSchema);
  
  const prompt = `You are an expert critic. Evaluate this response for the task: "${state.task}"
  
Response to evaluate:
${latestDraft}

Provide a detailed critique following this JSON schema:
${parser.getFormatInstructions()}

Score 90+ means the response is excellent and approved.
Be thorough but constructive in your feedback.`;
  
  const response = await critic.invoke(prompt);
  const critique = await parser.parse(response.content as string);
  
  return {
    critiques: [critique],
  };
}

Critic provides structured feedback with scores, issues categorization, and improvement suggestions.

4. Advanced Routing Logic

// lib/agents/producer-critic.ts (continued)
function routingLogic(state: typeof ProducerCriticState.State) {
  const lastCritique = state.critiques[state.critiques.length - 1];
  
  // Early termination conditions
  if (!lastCritique) return "critic";
  
  if (lastCritique.approved || state.iteration >= 5) {
    return END;
  }
  
  // Adaptive routing based on critique severity
  const criticalCount = lastCritique.issues.filter(i => i.severity === "critical").length;
  
  if (criticalCount > 2 && state.iteration < 3) {
    // Major rewrite needed
    return "producer";
  } else if (lastCritique.score > 75) {
    // Minor refinements only
    return "producer";
  } else {
    // Standard iteration
    return "producer";
  }
}

export function createProducerCriticAgent() {
  const workflow = new StateGraph(ProducerCriticState)
    .addNode("producer", producerNode)
    .addNode("critic", criticNode)
    .addEdge(START, "producer")
    .addEdge("producer", "critic")
    .addConditionalEdges("critic", routingLogic, {
      producer: "producer",
      [END]: END,
    });
  
  return workflow.compile();
}

Implements sophisticated routing based on critique severity and iteration count.

5. Streaming API with Server-Sent Events

// app/api/producer-critic/route.ts
import { createProducerCriticAgent } from "@/lib/agents/producer-critic";
import { debounce } from "es-toolkit";

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

export async function POST(req: Request) {
  const { task } = await req.json();
  
  const encoder = new TextEncoder();
  const stream = new TransformStream();
  const writer = stream.writable.getWriter();
  
  // Debounced write to prevent overwhelming client
  const debouncedWrite = debounce(async (data: any) => {
    await writer.write(
      encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
    );
  }, 100);
  
  const agent = createProducerCriticAgent();
  
  (async () => {
    try {
      const eventStream = await agent.streamEvents(
        { task, drafts: [], critiques: [], iteration: 0 },
        { version: "v2" }
      );
      
      for await (const event of eventStream) {
        if (event.event === "on_chain_end" && event.name === "producer") {
          await debouncedWrite({
            type: "draft",
            iteration: event.data.output.iteration,
            content: event.data.output.drafts[event.data.output.drafts.length - 1],
          });
        }
        
        if (event.event === "on_chain_end" && event.name === "critic") {
          const critique = event.data.output.critiques[event.data.output.critiques.length - 1];
          await debouncedWrite({
            type: "critique",
            iteration: event.data.output.iteration,
            score: critique.score,
            approved: critique.approved,
            issues: critique.issues,
          });
        }
      }
      
      await writer.write(encoder.encode(`data: ${JSON.stringify({ type: "complete" })}\n\n`));
    } catch (error) {
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({ type: "error", error: String(error) })}\n\n`)
      );
    } finally {
      await writer.close();
    }
  })();
  
  return new Response(stream.readable, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    },
  });
}

Streams reflection events in real-time using server-sent events for progressive UI updates.

6. Advanced Frontend with Real-Time Visualization

// components/ProducerCriticDemo.tsx
"use client";

import { useState, useCallback } from "react";
import { useMutation } from "@tanstack/react-query";
import { partition, groupBy } from "es-toolkit";

interface StreamEvent {
  type: "draft" | "critique" | "complete" | "error";
  iteration?: number;
  content?: string;
  score?: number;
  approved?: boolean;
  issues?: Array<{
    category: string;
    description: string;
    severity: string;
  }>;
  error?: string;
}

export default function ProducerCriticDemo() {
  const [task, setTask] = useState("");
  const [events, setEvents] = useState<StreamEvent[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);
  
  const startReflection = useCallback(async () => {
    setEvents([]);
    setIsStreaming(true);
    
    try {
      const response = await fetch("/api/producer-critic", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ task }),
      });
      
      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      
      while (reader) {
        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 event = JSON.parse(line.slice(6));
              setEvents(prev => [...prev, event]);
              
              if (event.type === "complete" || event.type === "error") {
                setIsStreaming(false);
              }
            } catch {}
          }
        }
      }
    } catch (error) {
      console.error("Stream error:", error);
      setIsStreaming(false);
    }
  }, [task]);
  
  // Group events by iteration
  const [drafts, critiques] = partition(
    events.filter(e => e.type === "draft" || e.type === "critique"),
    e => e.type === "draft"
  );
  
  const iterations = groupBy(
    [...drafts, ...critiques],
    e => e.iteration?.toString() || "0"
  );
  
  const finalDraft = drafts[drafts.length - 1];
  const finalCritique = critiques[critiques.length - 1];
  
  return (
    <div className="container mx-auto p-4">
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">Producer-Critic Reflection System</h2>
          
          <div className="form-control">
            <textarea
              className="textarea textarea-bordered"
              placeholder="Describe your task..."
              value={task}
              onChange={(e) => setTask(e.target.value)}
              rows={3}
              disabled={isStreaming}
            />
          </div>
          
          <div className="card-actions justify-end mt-4">
            <button
              className="btn btn-primary"
              onClick={startReflection}
              disabled={isStreaming || !task.trim()}
            >
              {isStreaming ? (
                <>
                  <span className="loading loading-spinner"></span>
                  Processing...
                </>
              ) : "Start Reflection"}
            </button>
          </div>
        </div>
      </div>
      
      {events.length > 0 && (
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-6">
          {/* Iteration Timeline */}
          <div className="card bg-base-100 shadow">
            <div className="card-body">
              <h3 className="card-title text-lg">Reflection Process</h3>
              
              <ul className="timeline timeline-vertical">
                {Object.entries(iterations).map(([iter, iterEvents]) => {
                  const draft = iterEvents.find(e => e.type === "draft");
                  const critique = iterEvents.find(e => e.type === "critique");
                  
                  return (
                    <li key={iter}>
                      <div className="timeline-middle">
                        <svg
                          xmlns="http://www.w3.org/2000/svg"
                          viewBox="0 0 20 20"
                          fill="currentColor"
                          className="h-5 w-5"
                        >
                          <path
                            fillRule="evenodd"
                            d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
                            clipRule="evenodd"
                          />
                        </svg>
                      </div>
                      <div className="timeline-end timeline-box">
                        <div className="text-lg font-black">Iteration {iter}</div>
                        {critique && (
                          <div className="stats stats-horizontal shadow mt-2">
                            <div className="stat">
                              <div className="stat-title">Score</div>
                              <div className="stat-value text-2xl">{critique.score}</div>
                            </div>
                            <div className="stat">
                              <div className="stat-title">Status</div>
                              <div className={`stat-value text-2xl ${critique.approved ? "text-success" : "text-warning"}`}>
                                {critique.approved ? "✓" : "↻"}
                              </div>
                            </div>
                          </div>
                        )}
                        {critique?.issues && (
                          <div className="mt-2">
                            <p className="font-semibold">Issues Found:</p>
                            {critique.issues.map((issue, idx) => (
                              <div key={idx} className={`badge badge-${issue.severity === "critical" ? "error" : "warning"} gap-2 mr-1`}>
                                {issue.category}
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                      <hr />
                    </li>
                  );
                })}
              </ul>
            </div>
          </div>
          
          {/* Final Output */}
          <div className="card bg-base-100 shadow">
            <div className="card-body">
              <h3 className="card-title text-lg">Final Output</h3>
              
              {finalCritique?.approved && (
                <div className="alert alert-success">
                  <span>Output approved with score: {finalCritique.score}/100</span>
                </div>
              )}
              
              {finalDraft && (
                <div className="prose max-w-none">
                  <div className="mockup-code">
                    <pre><code>{finalDraft.content}</code></pre>
                  </div>
                </div>
              )}
              
              {isStreaming && (
                <div className="flex justify-center">
                  <span className="loading loading-dots loading-lg"></span>
                </div>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Creates a sophisticated UI with timeline visualization and real-time streaming updates.

7. Performance Optimization with Caching

// lib/cache/reflection-cache.ts
import { kv } from "@vercel/kv";
import { hash } from "es-toolkit/compat";

interface CacheEntry {
  task: string;
  output: string;
  score: number;
  timestamp: number;
}

export class ReflectionCache {
  private readonly ttl = 3600; // 1 hour
  
  async get(task: string): Promise<CacheEntry | null> {
    const key = `reflection:${hash(task)}`;
    const cached = await kv.get<CacheEntry>(key);
    
    if (cached && Date.now() - cached.timestamp < this.ttl * 1000) {
      return cached;
    }
    
    return null;
  }
  
  async set(task: string, output: string, score: number): Promise<void> {
    const key = `reflection:${hash(task)}`;
    const entry: CacheEntry = {
      task,
      output,
      score,
      timestamp: Date.now(),
    };
    
    await kv.set(key, entry, { ex: this.ttl });
  }
  
  async getSimilar(task: string, threshold = 0.8): Promise<CacheEntry[]> {
    // Implement semantic similarity search
    const allKeys = await kv.keys("reflection:*");
    const similar: CacheEntry[] = [];
    
    for (const key of allKeys) {
      const entry = await kv.get<CacheEntry>(key);
      if (entry) {
        // Simple similarity check (implement proper semantic similarity)
        const similarity = this.calculateSimilarity(task, entry.task);
        if (similarity > threshold) {
          similar.push(entry);
        }
      }
    }
    
    return similar;
  }
  
  private calculateSimilarity(a: string, b: string): number {
    // Simplified similarity calculation
    const wordsA = new Set(a.toLowerCase().split(" "));
    const wordsB = new Set(b.toLowerCase().split(" "));
    const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
    const union = new Set([...wordsA, ...wordsB]);
    
    return intersection.size / union.size;
  }
}

Implements caching to reduce redundant reflection cycles for similar tasks.

8. Cost Tracking and Optimization

// lib/monitoring/cost-tracker.ts
interface ReflectionMetrics {
  totalTokens: number;
  inputTokens: number;
  outputTokens: number;
  iterations: number;
  duration: number;
  estimatedCost: number;
}

export class CostTracker {
  private metrics: ReflectionMetrics = {
    totalTokens: 0,
    inputTokens: 0,
    outputTokens: 0,
    iterations: 0,
    duration: 0,
    estimatedCost: 0,
  };
  
  private readonly costPerToken = {
    "gemini-2.5-pro": { input: 0.00125, output: 0.005 },
    "gemini-2.5-flash": { input: 0.00015, output: 0.0006 },
  };
  
  trackIteration(model: string, inputTokens: number, outputTokens: number): void {
    this.metrics.inputTokens += inputTokens;
    this.metrics.outputTokens += outputTokens;
    this.metrics.totalTokens += inputTokens + outputTokens;
    this.metrics.iterations += 1;
    
    const modelCost = this.costPerToken[model as keyof typeof this.costPerToken];
    if (modelCost) {
      this.metrics.estimatedCost += 
        (inputTokens * modelCost.input + outputTokens * modelCost.output) / 1000;
    }
  }
  
  shouldContinue(maxCost: number = 0.10): boolean {
    return this.metrics.estimatedCost < maxCost;
  }
  
  getMetrics(): ReflectionMetrics {
    return { ...this.metrics };
  }
}

Tracks token usage and costs to implement budget-aware reflection cycles.

Conclusion

The Reflection pattern transforms AI agents from single-shot responders into iterative learners capable of self-improvement. By implementing Producer-Critic architectures with proper state management, streaming capabilities, and cost optimization, you can deploy sophisticated reflection systems on Vercel's serverless platform. The key is balancing quality improvements against computational costs through intelligent caching, early stopping, and adaptive routing strategies. Start with basic self-reflection for simple tasks, then scale to multi-agent Producer-Critic systems for complex scenarios requiring higher quality outputs.