DRAFT Agentic Design Patterns - Exploration and Discovery

agentslangchainlanggraphtypescriptnextjsaiexploration
By sko X opus 4.19/21/202515 min read

This guide demonstrates how to build autonomous exploration and discovery agents using TypeScript, Next.js 15, LangChain, and LangGraph on Vercel's serverless platform. We'll create agents that proactively seek novel information, uncover hidden patterns, and generate new insights beyond predefined solution spaces.

Mental Model: Scientific Research Lab

Think of exploration agents like assembling a virtual research laboratory. Your agent acts as the lead scientist (orchestrator) that spawns specialized research assistants (worker agents) to explore different hypotheses. LangGraph provides the lab infrastructure (state management and workflow), while LangChain tools serve as research equipment. The exploration process mimics the scientific method: generate hypotheses, test them through exploration, evaluate findings, and iterate based on discoveries. Vercel's serverless platform acts as the scalable lab space that can expand or contract based on research demands.

Basic Example: Hypothesis-Driven Explorer

1. Core Exploration State Management

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

export const ExplorationStateSchema = z.object({
  query: z.string(),
  currentHypothesis: z.string().optional(),
  explorationDepth: z.number().default(0),
  discoveredPaths: z.array(z.string()).default([]),
  findings: z.array(z.object({
    path: z.string(),
    content: z.string(),
    confidence: z.number(),
    timestamp: z.number()
  })).default([]),
  confidenceThreshold: z.number().default(0.7),
  maxDepth: z.number().default(5),
  status: z.enum(['exploring', 'evaluating', 'backtracking', 'complete']).default('exploring')
});

export type ExplorationState = z.infer<typeof ExplorationStateSchema>;

export interface ExplorationNode {
  id: string;
  hypothesis: string;
  score: number;
  children: ExplorationNode[];
  visited: boolean;
  metadata: Record<string, any>;
}

Defines the core state structure for exploration agents including hypotheses tracking, discovered paths, confidence scores, and tree-based exploration nodes.

2. Basic Exploration Agent

// lib/exploration/basic-explorer.ts
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { pull } from 'langchain/hub';
import { filter, map, sortBy } from 'es-toolkit';

interface BasicExplorationState {
  messages: BaseMessage[];
  hypothesis: string;
  explorationCount: number;
  discoveries: string[];
}

export async function createBasicExplorer() {
  const model = new ChatGoogleGenerativeAI({ 
    temperature: 0.7,
    modelName: 'gemini-2.5-flash'
  });
  
  const searchTool = new TavilySearchResults({ maxResults: 3 });
  
  // Node: Generate hypothesis
  async function generateHypothesis(state: BasicExplorationState) {
    const prompt = await pull('exploration/hypothesis-generator');
    const response = await model.invoke([
      new HumanMessage(`Based on: ${state.messages.slice(-1)[0].content}
        Generate a testable hypothesis for exploration.`)
    ]);
    
    return {
      hypothesis: response.content,
      messages: [...state.messages, response]
    };
  }
  
  // Node: Explore hypothesis
  async function exploreHypothesis(state: BasicExplorationState) {
    const searchResults = await searchTool.invoke(state.hypothesis);
    const parsedResults = JSON.parse(searchResults);
    
    const findings = map(
      filter(parsedResults, (r: any) => r.score > 0.5),
      (result: any) => result.content
    );
    
    return {
      discoveries: [...state.discoveries, ...findings],
      explorationCount: state.explorationCount + 1,
      messages: [...state.messages, new AIMessage(`Explored: ${findings.length} findings`)]
    };
  }
  
  // Node: Evaluate findings
  async function evaluateFindings(state: BasicExplorationState) {
    const sortedDiscoveries = sortBy(
      state.discoveries,
      (d: string) => d.length
    );
    
    const evaluation = await model.invoke([
      new HumanMessage(`Evaluate these discoveries: ${sortedDiscoveries.join('\n')}
        Should we continue exploring or have we found sufficient insights?`)
    ]);
    
    return {
      messages: [...state.messages, evaluation]
    };
  }
  
  // Conditional edge: Should continue exploring?
  function shouldContinue(state: BasicExplorationState) {
    if (state.explorationCount >= 5) return 'end';
    if (state.discoveries.length > 10) return 'evaluate';
    return 'explore';
  }
  
  // Build the graph
  const workflow = new StateGraph<BasicExplorationState>({
    channels: {
      messages: { value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y], default: () => [] },
      hypothesis: { value: (x, y) => y || x, default: () => '' },
      explorationCount: { value: (x, y) => y || x, default: () => 0 },
      discoveries: { value: (x: string[], y: string[]) => [...x, ...y], default: () => [] }
    }
  });
  
  workflow.addNode('generate', generateHypothesis);
  workflow.addNode('explore', exploreHypothesis);
  workflow.addNode('evaluate', evaluateFindings);
  
  workflow.addEdge('__start__', 'generate');
  workflow.addEdge('generate', 'explore');
  workflow.addConditionalEdges('explore', shouldContinue, {
    'explore': 'generate',
    'evaluate': 'evaluate',
    'end': '__end__'
  });
  workflow.addEdge('evaluate', '__end__');
  
  return workflow.compile();
}

Creates a basic exploration agent that generates hypotheses, explores them using search tools, and evaluates findings in an iterative cycle.

3. API Route for Exploration

// app/api/explore/route.ts
import { createBasicExplorer } from '@/lib/exploration/basic-explorer';
import { HumanMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';

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

export async function POST(req: Request) {
  try {
    const { query, sessionId } = await req.json();
    
    const explorer = await createBasicExplorer();
    
    const encoder = new TextEncoder();
    const stream = new TransformStream();
    const writer = stream.writable.getWriter();
    
    // Run exploration in background
    (async () => {
      try {
        const eventStream = await explorer.stream({
          messages: [new HumanMessage(query)],
          hypothesis: '',
          explorationCount: 0,
          discoveries: []
        });
        
        for await (const event of eventStream) {
          const update = {
            type: 'exploration_update',
            hypothesis: event.hypothesis,
            discoveries: event.discoveries?.length || 0,
            status: event.explorationCount < 5 ? 'exploring' : 'complete'
          };
          
          await writer.write(
            encoder.encode(`data: ${JSON.stringify(update)}\n\n`)
          );
        }
      } catch (error) {
        console.error('Exploration error:', error);
      } 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('API error:', error);
    return NextResponse.json(
      { error: 'Failed to start exploration' },
      { status: 500 }
    );
  }
}

API endpoint that streams exploration progress in real-time using Server-Sent Events for responsive user experience.

4. Frontend Exploration Interface

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

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

interface ExplorationUpdate {
  type: string;
  hypothesis?: string;
  discoveries?: number;
  status?: string;
}

export default function ExplorationInterface() {
  const [query, setQuery] = useState('');
  const [updates, setUpdates] = useState<ExplorationUpdate[]>([]);
  const [isExploring, setIsExploring] = useState(false);
  
  const startExploration = useMutation({
    mutationFn: async (explorationQuery: string) => {
      const response = await fetch('/api/explore', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query: explorationQuery })
      });
      
      if (!response.ok) throw new Error('Exploration failed');
      
      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      
      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const text = decoder.decode(value);
        const lines = text.split('\n');
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));
            setUpdates(prev => [...prev, data]);
          }
        }
      }
    },
    onSuccess: () => {
      setIsExploring(false);
    },
    onError: (error) => {
      console.error('Exploration error:', error);
      setIsExploring(false);
    }
  });
  
  const handleExplore = useCallback(
    debounce(() => {
      if (query.trim()) {
        setIsExploring(true);
        setUpdates([]);
        startExploration.mutate(query);
      }
    }, 500),
    [query]
  );
  
  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">Exploration & Discovery Agent</h2>
        
        <div className="form-control">
          <label className="label">
            <span className="label-text">What would you like to explore?</span>
          </label>
          <textarea
            className="textarea textarea-bordered h-24"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Enter a topic or question for deep exploration..."
          />
        </div>
        
        <div className="card-actions justify-end">
          <button 
            className="btn btn-primary"
            onClick={handleExplore}
            disabled={isExploring || !query.trim()}
          >
            {isExploring ? (
              <>
                <span className="loading loading-spinner"></span>
                Exploring...
              </>
            ) : 'Start Exploration'}
          </button>
        </div>
        
        {updates.length > 0 && (
          <div className="mt-4">
            <h3 className="font-semibold mb-2">Exploration Progress:</h3>
            <div className="space-y-2 max-h-64 overflow-y-auto">
              {updates.map((update, idx) => (
                <div key={idx} className="alert alert-info">
                  <div>
                    <span className="font-semibold">Hypothesis:</span> {update.hypothesis}
                    <br />
                    <span className="font-semibold">Discoveries:</span> {update.discoveries}
                    <br />
                    <span className="badge badge-sm">{update.status}</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

React component that provides real-time visualization of the exploration process with hypothesis generation and discovery tracking.

Advanced Example: Multi-Agent Scientific Discovery System

1. Monte Carlo Tree Search Explorer

// lib/exploration/mcts-explorer.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { mean, sum, maxBy, sample } from 'es-toolkit';
import { v4 as uuidv4 } from 'uuid';

interface MCTSNode {
  id: string;
  state: string;
  value: number;
  visits: number;
  parent?: MCTSNode;
  children: MCTSNode[];
  untried_actions: string[];
}

export class MonteCarloTreeSearchExplorer {
  private model: ChatGoogleGenerativeAI;
  private explorationConstant: number = Math.sqrt(2);
  private maxIterations: number = 100;
  
  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      temperature: 0.9,
      modelName: 'gemini-2.5-pro'
    });
  }
  
  async explore(problem: string, iterations: number = 100): Promise<any> {
    this.maxIterations = iterations;
    const root = this.createNode(problem);
    
    for (let i = 0; i < this.maxIterations; i++) {
      // Selection
      const selectedNode = await this.select(root);
      
      // Expansion
      const expandedNode = await this.expand(selectedNode);
      
      // Simulation (Self-Refine)
      const reward = await this.simulate(expandedNode);
      
      // Backpropagation
      await this.backpropagate(expandedNode, reward);
    }
    
    return this.getBestPath(root);
  }
  
  private createNode(state: string, parent?: MCTSNode): MCTSNode {
    return {
      id: uuidv4(),
      state,
      value: 0,
      visits: 0,
      parent,
      children: [],
      untried_actions: []
    };
  }
  
  private async select(node: MCTSNode): Promise<MCTSNode> {
    while (node.children.length > 0) {
      const ucbValues = node.children.map(child => this.calculateUCB(child));
      const maxIndex = ucbValues.indexOf(Math.max(...ucbValues));
      node = node.children[maxIndex];
    }
    return node;
  }
  
  private calculateUCB(node: MCTSNode): number {
    if (node.visits === 0) return Infinity;
    
    const exploitation = node.value / node.visits;
    const exploration = this.explorationConstant * 
      Math.sqrt(Math.log(node.parent!.visits) / node.visits);
    
    return exploitation + exploration;
  }
  
  private async expand(node: MCTSNode): Promise<MCTSNode> {
    // Generate possible actions using LLM
    const response = await this.model.invoke([{
      role: 'system',
      content: 'You are exploring solution spaces. Generate 3 diverse next steps.'
    }, {
      role: 'user',
      content: `Current state: ${node.state}\nGenerate next exploration steps as JSON array.`
    }]);
    
    try {
      const actions = JSON.parse(response.content as string);
      node.untried_actions = actions;
      
      if (actions.length > 0) {
        const action = sample(actions);
        const childNode = this.createNode(action, node);
        node.children.push(childNode);
        return childNode;
      }
    } catch (e) {
      console.error('Failed to parse actions:', e);
    }
    
    return node;
  }
  
  private async simulate(node: MCTSNode): Promise<number> {
    // Self-refine: Use LLM to evaluate and refine the current path
    const response = await this.model.invoke([{
      role: 'system',
      content: 'Evaluate this exploration path and suggest improvements. Rate quality 0-1.'
    }, {
      role: 'user',
      content: `Path: ${this.getPath(node).join(' -> ')}\nEvaluate and score.`
    }]);
    
    // Extract score from response
    const scoreMatch = response.content.toString().match(/\d\.\d+/);
    return scoreMatch ? parseFloat(scoreMatch[0]) : 0.5;
  }
  
  private async backpropagate(node: MCTSNode, reward: number) {
    let current: MCTSNode | undefined = node;
    while (current) {
      current.visits++;
      current.value += reward;
      current = current.parent;
    }
  }
  
  private getPath(node: MCTSNode): string[] {
    const path: string[] = [];
    let current: MCTSNode | undefined = node;
    
    while (current) {
      path.unshift(current.state);
      current = current.parent;
    }
    
    return path;
  }
  
  private getBestPath(root: MCTSNode): any {
    const bestChild = maxBy(root.children, child => child.value / child.visits);
    
    if (!bestChild) return { path: [root.state], score: 0 };
    
    return {
      path: this.getPath(bestChild),
      score: bestChild.value / bestChild.visits,
      explorations: this.countTotalNodes(root)
    };
  }
  
  private countTotalNodes(node: MCTSNode): number {
    return 1 + sum(node.children.map(child => this.countTotalNodes(child)));
  }
}

Implements Monte Carlo Tree Search for intelligent exploration with UCB-based selection, LLM-powered expansion, and self-refinement.

2. Multi-Agent Research Laboratory

// lib/exploration/research-laboratory.ts
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { groupBy, flatten, uniqBy } from 'es-toolkit';
import { MonteCarloTreeSearchExplorer } from './mcts-explorer';

interface ResearchState {
  messages: BaseMessage[];
  research_topic: string;
  hypotheses: Array<{id: string; content: string; score: number}>;
  literature_review: string;
  experiments: Array<{id: string; design: string; results?: string}>;
  synthesis: string;
  phase: 'planning' | 'literature' | 'hypothesis' | 'experimentation' | 'synthesis';
}

export class ResearchLaboratory {
  private leadModel: ChatGoogleGenerativeAI;
  private workerModel: ChatGoogleGenerativeAI;
  private mctsExplorer: MonteCarloTreeSearchExplorer;
  
  constructor() {
    this.leadModel = new ChatGoogleGenerativeAI({
      temperature: 0.3,
      modelName: 'gemini-2.5-pro'
    });
    
    this.workerModel = new ChatGoogleGenerativeAI({
      temperature: 0.7,
      modelName: 'gemini-2.5-flash'
    });
    
    this.mctsExplorer = new MonteCarloTreeSearchExplorer();
  }
  
  async createResearchWorkflow() {
    const workflow = new StateGraph<ResearchState>({
      channels: {
        messages: {
          value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
          default: () => []
        },
        research_topic: {
          value: (x, y) => y || x,
          default: () => ''
        },
        hypotheses: {
          value: (x, y) => y || x,
          default: () => []
        },
        literature_review: {
          value: (x, y) => y || x,
          default: () => ''
        },
        experiments: {
          value: (x, y) => y || x,
          default: () => []
        },
        synthesis: {
          value: (x, y) => y || x,
          default: () => ''
        },
        phase: {
          value: (x, y) => y || x,
          default: () => 'planning'
        }
      }
    });
    
    // Planning Agent
    workflow.addNode('planner', async (state) => {
      const plan = await this.leadModel.invoke([
        new HumanMessage(`Create a research plan for: ${state.research_topic}
          Include: 1) Key areas to investigate 2) Methodology 3) Success metrics`)
      ]);
      
      return {
        messages: [...state.messages, plan],
        phase: 'literature' as const
      };
    });
    
    // Literature Review Agent
    workflow.addNode('literature_reviewer', async (state) => {
      // Simulate parallel literature review by multiple agents
      const reviewPromises = Array.from({ length: 3 }, async (_, i) => {
        const review = await this.workerModel.invoke([
          new HumanMessage(`Review literature on ${state.research_topic}
            Focus area ${i + 1}: ${['theoretical foundations', 'recent advances', 'open problems'][i]}`)
        ]);
        return review.content;
      });
      
      const reviews = await Promise.all(reviewPromises);
      const combinedReview = reviews.join('\n\n');
      
      return {
        literature_review: combinedReview,
        messages: [...state.messages, new AIMessage(combinedReview)],
        phase: 'hypothesis' as const
      };
    });
    
    // Hypothesis Generation Agent (using MCTS)
    workflow.addNode('hypothesis_generator', async (state) => {
      const explorationResult = await this.mctsExplorer.explore(
        `Generate hypotheses for: ${state.research_topic}\nBased on: ${state.literature_review}`,
        50
      );
      
      const hypotheses = explorationResult.path.map((h: string, idx: number) => ({
        id: `hyp_${idx}`,
        content: h,
        score: Math.random() * (1 - 0.5) + 0.5 // Simulate scoring
      }));
      
      return {
        hypotheses: hypotheses,
        messages: [...state.messages, new AIMessage(`Generated ${hypotheses.length} hypotheses`)],
        phase: 'experimentation' as const
      };
    });
    
    // Experiment Design Agent
    workflow.addNode('experimenter', async (state) => {
      const topHypotheses = state.hypotheses
        .sort((a, b) => b.score - a.score)
        .slice(0, 3);
      
      const experiments = await Promise.all(
        topHypotheses.map(async (hyp) => {
          const design = await this.workerModel.invoke([
            new HumanMessage(`Design an experiment to test: ${hyp.content}`)
          ]);
          
          return {
            id: hyp.id,
            design: design.content as string,
            results: `Simulated results for ${hyp.id}`
          };
        })
      );
      
      return {
        experiments: experiments,
        messages: [...state.messages, new AIMessage(`Designed ${experiments.length} experiments`)],
        phase: 'synthesis' as const
      };
    });
    
    // Synthesis Agent
    workflow.addNode('synthesizer', async (state) => {
      const synthesis = await this.leadModel.invoke([
        new HumanMessage(`Synthesize research findings:
          Topic: ${state.research_topic}
          Literature: ${state.literature_review}
          Hypotheses: ${JSON.stringify(state.hypotheses)}
          Experiments: ${JSON.stringify(state.experiments)}
          
          Provide comprehensive insights and conclusions.`)
      ]);
      
      return {
        synthesis: synthesis.content as string,
        messages: [...state.messages, synthesis]
      };
    });
    
    // Define workflow edges
    workflow.addEdge('__start__', 'planner');
    workflow.addEdge('planner', 'literature_reviewer');
    workflow.addEdge('literature_reviewer', 'hypothesis_generator');
    workflow.addEdge('hypothesis_generator', 'experimenter');
    workflow.addEdge('experimenter', 'synthesizer');
    workflow.addEdge('synthesizer', '__end__');
    
    return workflow.compile();
  }
}

Implements a complete multi-agent research laboratory with specialized agents for planning, literature review, hypothesis generation using MCTS, experimentation, and synthesis.

3. Serverless Exploration with State Persistence

// lib/exploration/serverless-explorer.ts
import { kv } from '@vercel/kv';
import { Queue } from 'bullmq';
import { chunk, throttle } from 'es-toolkit';

interface ExplorationChunk {
  id: string;
  sessionId: string;
  chunkIndex: number;
  totalChunks: number;
  state: any;
  timestamp: number;
}

export class ServerlessExplorer {
  private maxExecutionTime = 777; // seconds (with safety buffer)
  private checkpointInterval = 60; // seconds
  
  async executeWithCheckpoints(
    explorationFn: () => Promise<any>,
    sessionId: string
  ) {
    const startTime = Date.now();
    const checkpointKey = `exploration:${sessionId}`;
    
    // Try to restore previous state
    const previousState = await kv.get<ExplorationChunk>(checkpointKey);
    let currentState = previousState?.state || {};
    
    const executeWithTimeout = async () => {
      const elapsedSeconds = (Date.now() - startTime) / 1000;
      
      if (elapsedSeconds >= this.maxExecutionTime - 30) {
        // Save state and schedule continuation
        await this.saveCheckpoint(sessionId, currentState);
        await this.scheduleContinuation(sessionId);
        return { status: 'paused', state: currentState };
      }
      
      // Execute exploration chunk
      const result = await explorationFn();
      currentState = { ...currentState, ...result };
      
      // Periodic checkpoint
      if (elapsedSeconds % this.checkpointInterval < 1) {
        await this.saveCheckpoint(sessionId, currentState);
      }
      
      return { status: 'continuing', state: currentState };
    };
    
    // Throttle execution to prevent rate limits
    const throttledExecute = throttle(executeWithTimeout, 1000);
    
    let status = 'continuing';
    while (status === 'continuing') {
      const result = await throttledExecute();
      status = result.status;
      currentState = result.state;
    }
    
    return currentState;
  }
  
  private async saveCheckpoint(sessionId: string, state: any) {
    const checkpoint: ExplorationChunk = {
      id: `checkpoint_${Date.now()}`,
      sessionId,
      chunkIndex: state.chunkIndex || 0,
      totalChunks: state.totalChunks || 1,
      state,
      timestamp: Date.now()
    };
    
    await kv.set(
      `exploration:${sessionId}`,
      checkpoint,
      { ex: 3600 } // 1 hour expiry
    );
  }
  
  private async scheduleContinuation(sessionId: string) {
    // Use Inngest or similar for scheduling
    await fetch('/api/schedule', {
      method: 'POST',
      body: JSON.stringify({
        event: 'exploration.continue',
        data: { sessionId },
        delay: '5s'
      })
    });
  }
}

// API Route for chunked exploration
export async function POST(req: Request) {
  const { sessionId, query } = await req.json();
  const explorer = new ServerlessExplorer();
  
  const explorationTasks = chunk(
    Array.from({ length: 20 }, (_, i) => i),
    5
  );
  
  const result = await explorer.executeWithCheckpoints(
    async () => {
      // Execute one chunk of exploration
      const tasks = explorationTasks.shift();
      if (!tasks) return { complete: true };
      
      const results = await Promise.all(
        tasks.map(async (taskId) => {
          // Simulate exploration task
          return { taskId, result: `Discovery ${taskId}` };
        })
      );
      
      return {
        chunkIndex: (explorationTasks.length || 0) + 1,
        discoveries: results
      };
    },
    sessionId
  );
  
  return new Response(JSON.stringify(result), {
    headers: { 'Content-Type': 'application/json' }
  });
}

Implements serverless-aware exploration with automatic checkpointing, state persistence, and task continuation for long-running explorations.

4. Vector Memory Integration

// lib/exploration/memory-system.ts
import { Pinecone } from '@pinecone-database/pinecone';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
import { Document } from '@langchain/core/documents';
import { groupBy, sortBy, take } from 'es-toolkit';

export class ExplorationMemorySystem {
  private pinecone: Pinecone;
  private embeddings: GoogleGenerativeAIEmbeddings;
  private indexName = 'exploration-memory';
  
  constructor() {
    this.pinecone = new Pinecone({
      apiKey: process.env.PINECONE_API_KEY!
    });
    
    this.embeddings = new GoogleGenerativeAIEmbeddings({
      modelName: 'embedding-001'
    });
  }
  
  async storeDiscovery(
    content: string,
    metadata: {
      sessionId: string;
      hypothesis: string;
      confidence: number;
      timestamp: number;
      explorationPath: string[];
    }
  ) {
    const embedding = await this.embeddings.embedQuery(content);
    const index = this.pinecone.index(this.indexName);
    
    await index.upsert([{
      id: `discovery_${metadata.sessionId}_${metadata.timestamp}`,
      values: embedding,
      metadata: {
        ...metadata,
        content: content.slice(0, 1000) // Store preview
      }
    }]);
  }
  
  async querySemanticMemory(
    query: string,
    filters?: Record<string, any>
  ): Promise<Document[]> {
    const queryEmbedding = await this.embeddings.embedQuery(query);
    const index = this.pinecone.index(this.indexName);
    
    const results = await index.query({
      vector: queryEmbedding,
      topK: 20,
      includeMetadata: true,
      filter: filters
    });
    
    // Group by hypothesis and take best from each
    const grouped = groupBy(
      results.matches,
      (match: any) => match.metadata.hypothesis
    );
    
    const diverseResults = Object.values(grouped).map(group => {
      const sorted = sortBy(group, (m: any) => -m.score);
      return sorted[0];
    });
    
    return take(diverseResults, 10).map(match => 
      new Document({
        pageContent: match.metadata.content,
        metadata: match.metadata
      })
    );
  }
  
  async getExplorationPattern(sessionId: string) {
    const index = this.pinecone.index(this.indexName);
    
    const results = await index.query({
      vector: new Array(768).fill(0), // Dummy vector for Gemini embeddings
      topK: 100,
      includeMetadata: true,
      filter: { sessionId }
    });
    
    // Analyze exploration pattern
    const pathFrequency = new Map<string, number>();
    results.matches.forEach(match => {
      const path = match.metadata.explorationPath?.join(' -> ') || '';
      pathFrequency.set(path, (pathFrequency.get(path) || 0) + 1);
    });
    
    return {
      totalDiscoveries: results.matches.length,
      avgConfidence: results.matches.reduce(
        (sum, m) => sum + (m.metadata.confidence || 0), 0
      ) / results.matches.length,
      mostFrequentPaths: Array.from(pathFrequency.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
    };
  }
}

Implements semantic memory storage and retrieval using Pinecone vector database for long-term exploration pattern learning.

5. Advanced Exploration UI with Real-time Visualization

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

import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { debounce } from 'es-toolkit';

interface ExplorationMetrics {
  timestamp: number;
  depth: number;
  discoveries: number;
  confidence: number;
}

export default function AdvancedExplorationUI() {
  const [topic, setTopic] = useState('');
  const [metrics, setMetrics] = useState<ExplorationMetrics[]>([]);
  const [currentPhase, setCurrentPhase] = useState('idle');
  const [hypotheses, setHypotheses] = useState<Array<{id: string; content: string; score: number}>>([]);
  
  const startResearch = useMutation({
    mutationFn: async (researchTopic: string) => {
      const response = await fetch('/api/research-lab', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ topic: researchTopic })
      });
      
      if (!response.ok) throw new Error('Research failed');
      
      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      
      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const text = decoder.decode(value);
        const lines = text.split('\n');
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));
            
            // Update phase
            if (data.phase) setCurrentPhase(data.phase);
            
            // Update hypotheses
            if (data.hypotheses) setHypotheses(data.hypotheses);
            
            // Update metrics
            if (data.metrics) {
              setMetrics(prev => [...prev, {
                timestamp: Date.now(),
                depth: data.metrics.depth,
                discoveries: data.metrics.discoveries,
                confidence: data.metrics.confidence
              }]);
            }
          }
        }
      }
    }
  });
  
  const memoryStats = useQuery({
    queryKey: ['memory-stats', topic],
    queryFn: async () => {
      const response = await fetch(`/api/memory-stats?topic=${encodeURIComponent(topic)}`);
      return response.json();
    },
    enabled: !!topic,
    refetchInterval: 5000
  });
  
  return (
    <div className="min-h-screen bg-base-200 p-4">
      <div className="max-w-7xl mx-auto">
        <div className="text-center mb-8">
          <h1 className="text-5xl font-bold mb-2">Research Laboratory</h1>
          <p className="text-xl">Multi-Agent Scientific Discovery System</p>
        </div>
        
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
          {/* Control Panel */}
          <div className="lg:col-span-1">
            <div className="card bg-base-100 shadow-xl">
              <div className="card-body">
                <h2 className="card-title">Research Control</h2>
                
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Research Topic</span>
                  </label>
                  <input
                    type="text"
                    className="input input-bordered"
                    value={topic}
                    onChange={(e) => setTopic(e.target.value)}
                    placeholder="Enter research topic..."
                  />
                </div>
                
                <button
                  className="btn btn-primary mt-4"
                  onClick={() => startResearch.mutate(topic)}
                  disabled={!topic || startResearch.isPending}
                >
                  {startResearch.isPending ? (
                    <>
                      <span className="loading loading-spinner"></span>
                      Researching...
                    </>
                  ) : 'Start Research'}
                </button>
                
                <div className="divider"></div>
                
                <div className="stats stats-vertical shadow">
                  <div className="stat">
                    <div className="stat-title">Current Phase</div>
                    <div className="stat-value text-primary">{currentPhase}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">Hypotheses</div>
                    <div className="stat-value">{hypotheses.length}</div>
                  </div>
                  <div className="stat">
                    <div className="stat-title">Memory Entries</div>
                    <div className="stat-value">{memoryStats.data?.totalEntries || 0}</div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          
          {/* Visualization Panel */}
          <div className="lg:col-span-2">
            <div className="card bg-base-100 shadow-xl">
              <div className="card-body">
                <h2 className="card-title">Exploration Metrics</h2>
                
                <ResponsiveContainer width="100%" height={300}>
                  <LineChart data={metrics}>
                    <CartesianGrid strokeDasharray="3 3" />
                    <XAxis dataKey="timestamp" />
                    <YAxis />
                    <Tooltip />
                    <Line 
                      type="monotone" 
                      dataKey="confidence" 
                      stroke="#8884d8" 
                      name="Confidence"
                    />
                    <Line 
                      type="monotone" 
                      dataKey="discoveries" 
                      stroke="#82ca9d" 
                      name="Discoveries"
                    />
                    <Line 
                      type="monotone" 
                      dataKey="depth" 
                      stroke="#ffc658" 
                      name="Depth"
                    />
                  </LineChart>
                </ResponsiveContainer>
                
                <div className="divider"></div>
                
                {/* Hypotheses List */}
                <h3 className="font-semibold mb-2">Generated Hypotheses</h3>
                <div className="space-y-2 max-h-64 overflow-y-auto">
                  {hypotheses.map((hyp) => (
                    <div key={hyp.id} className="alert">
                      <div className="flex-1">
                        <p className="font-semibold">{hyp.content}</p>
                        <div className="flex items-center gap-2 mt-1">
                          <span className="badge badge-sm">Score: {hyp.score.toFixed(2)}</span>
                          <progress 
                            className="progress progress-primary w-24" 
                            value={hyp.score} 
                            max="1"
                          ></progress>
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Advanced UI component with real-time metrics visualization, hypothesis tracking, and memory statistics for comprehensive exploration monitoring.

Conclusion

This implementation demonstrates production-ready exploration and discovery agents that autonomously navigate complex problem spaces using TypeScript, LangChain, and LangGraph on Vercel's serverless platform. The patterns shown enable agents to generate hypotheses, explore solution spaces using Monte Carlo Tree Search, coordinate multi-agent research teams, and maintain semantic memory for continuous learning. Key architectural decisions include using Server-Sent Events for real-time streaming, implementing checkpointing for long-running explorations within serverless constraints, leveraging vector databases for pattern recognition, and employing es-toolkit for efficient data manipulation. These exploration agents transform AI systems from reactive tools into proactive research partners capable of discovering novel insights and expanding their own capabilities.