Skip to main content
Research is knowledge accumulation under uncertainty. You start with a question, gather information from multiple sources, evaluate what you find, synthesize it into understanding, and produce something useful—a report, a summary, an answer. Unlike the virtual office where work items have clear completion criteria, research tasks often expand as you learn more. A question about market trends leads to competitor analysis which surfaces regulatory considerations which require legal research. Good research systems handle this expansion gracefully while producing coherent outputs. This chapter builds a research system that accumulates knowledge over time. Each research task contributes to a growing knowledge base that informs future tasks. The system gets smarter with use—not through model fine-tuning, but through accumulated, retrievable knowledge that grounds future reasoning.

12.1 The Research Challenge

Research differs from other agentic tasks in several ways that affect system design. Uncertain scope. When you ask “What are the security implications of this architecture?”, you don’t know upfront how many sources you’ll need, how deep you’ll go, or what tangents matter. The system must explore adaptively rather than following a fixed plan. Variable source quality. Web search results, academic papers, internal documents, expert opinions—these sources have different reliability, different biases, different levels of detail. The system must assess and weight sources appropriately. Synthesis over retrieval. Finding information isn’t enough. Research requires connecting facts, identifying patterns, resolving contradictions, and producing coherent narratives from fragmented sources. This is generative work that uses retrieved information as input. Cumulative value. Research on one topic often relates to past or future research. A system that forgets everything after each task wastes the understanding it developed. Persistent knowledge makes the system more capable over time. These characteristics shape the architecture we’ll build: adaptive exploration rather than fixed plans, source evaluation and scoring, synthesis as a distinct phase, and a growing knowledge base that persists across tasks.

12.2 System Architecture

The research system has four main components that work together in a pipeline with feedback loops.
┌─────────────────────────────────────────────────────────────────────────┐
│                         Research System                                  │
│                                                                         │
│   Topic Queue                                                           │
│       │                                                                 │
│       ▼                                                                 │
│   ┌─────────┐    ┌─────────────┐    ┌──────────┐    ┌─────────┐       │
│   │ Planner │───▶│  Gatherer   │───▶│ Analyzer │───▶│  Writer │       │
│   └─────────┘    └─────────────┘    └──────────┘    └─────────┘       │
│       │                │                  │              │             │
│       │                ▼                  │              ▼             │
│       │         ┌───────────┐            │         ┌─────────┐        │
│       │         │  Sources  │            │         │ Report  │        │
│       │         └───────────┘            │         └─────────┘        │
│       │                                  │                             │
│       │                                  ▼                             │
│       │                          ┌─────────────┐                       │
│       └─────────────────────────▶│ Knowledge   │◀──────────────────────┘
│             (learning)           │    Base     │     (storage)         │
│                                  └─────────────┘                       │
└─────────────────────────────────────────────────────────────────────────┘
The Planner takes a research topic and decomposes it into specific questions. It consults the knowledge base to understand what’s already known and identifies gaps. Planning is adaptive—the plan may be revised as research reveals new questions. The Gatherer executes searches, retrieves documents, and collects raw information. It manages multiple source types—web search, document retrieval, database queries—and tracks provenance so findings can be cited. The Analyzer evaluates and processes gathered information. It scores sources for reliability, extracts key findings, identifies contradictions, and flags gaps that need more research. Analysis may trigger additional gathering cycles. The Writer synthesizes findings into coherent output. It structures information, ensures claims are supported by sources, and produces reports that answer the original questions. The Knowledge Base persists across all tasks. High-quality findings get stored with their sources and context. Future research retrieves relevant prior knowledge, building on what the system has learned.

12.3 The Knowledge Base

The knowledge base is the system’s long-term memory. Unlike conversation history (which stores what was said) or task state (which stores current progress), the knowledge base stores what the system knows—facts, findings, and insights extracted from research.
interface KnowledgeEntry {
  id: string;
  content: string;           // The actual knowledge
  topic: string;             // Topic category
  sources: SourceRef[];      // Where this came from
  confidence: number;        // How reliable (0-1)
  createdAt: number;
  lastVerified: number;      // When last confirmed accurate
  accessCount: number;       // How often retrieved
  relatedEntries: string[];  // Links to related knowledge
}

interface SourceRef {
  type: 'web' | 'document' | 'paper' | 'expert' | 'derived';
  url?: string;
  title: string;
  author?: string;
  date?: string;
  reliability: number;       // Source reliability score
}
The knowledge base uses vector storage for semantic retrieval: each entry is embedded into a vector representation (for example, using an embedding model), and queries are embedded into the same space so the store can return entries with high similarity to the query vector. When researching a new topic, the system queries for related knowledge and incorporates it into context. This means research on “authentication best practices” can surface knowledge from earlier research on “OAuth implementation” or “session management.” To support semantic retrieval, deduplication, and cross-linking, you can implement the knowledge base as a thin wrapper around a vector store:
interface VectorResult<T> {
  item: T;
  similarity: number;
}

export default class KnowledgeBase extends AgenticSystem {
  @field entries: VectorStore<KnowledgeEntry, VectorResult<KnowledgeEntry>>;
  @field topics: Map<string, string[]> = new Map(); // topic → entry IDs

  async search(query: string, limit: number = 10): Promise<KnowledgeEntry[]> {
    const results: VectorResult<KnowledgeEntry>[] = await this.entries.search(query, limit);

    // Update access counts
    for (const r of results) {
      r.item.accessCount += 1;
    }

    return results
      .filter(r => r.similarity > 0.6) // Relevance threshold
      .map(r => r.item);
  }

  async store(entry: Omit<KnowledgeEntry, 'id' | 'createdAt' | 'accessCount' | 'relatedEntries'>): Promise<string> {
    // Check for duplicates
    const similar: VectorResult<KnowledgeEntry>[] = await this.entries.search(entry.content, 3);
    const duplicate = similar.find(s => s.similarity > 0.95);

    if (duplicate) {
      // Merge with existing entry
      return this.mergeEntry(duplicate.item.id, entry);
    }

    // Create new entry
    const id = crypto.randomUUID();
    const fullEntry: KnowledgeEntry = {
      ...entry,
      id,
      createdAt: Date.now(),
      accessCount: 0,
      relatedEntries: []
    };

    await this.entries.insert(fullEntry);

    // Update topic index
    const topicEntries = this.topics.get(entry.topic) || [];
    topicEntries.push(id);
    this.topics.set(entry.topic, topicEntries);

    // Find and link related entries
    await this.linkRelatedEntries(id, entry.content);

    return id;
  }

  async mergeEntry(existingId: string, newInfo: Partial<KnowledgeEntry>): Promise<string> {
    const existing = await this.entries.get(existingId);
    if (!existing) return existingId;

    // Add new sources
    if (newInfo.sources) {
      existing.sources = [...existing.sources, ...newInfo.sources];
    }

    // Update confidence if new info is more reliable
    if (newInfo.confidence && newInfo.confidence > existing.confidence) {
      existing.confidence = newInfo.confidence;
    }

    existing.lastVerified = Date.now();
    await this.entries.update(existingId, existing);

    return existingId;
  }

  async deprecate(id: string, reason: string) {
    const entry = await this.entries.get(id);
    if (entry) {
      entry.confidence = 0;
      entry.content = `[DEPRECATED: ${reason}] ${entry.content}`;
      await this.entries.update(id, entry);
    }
  }

  private async linkRelatedEntries(id: string, content: string) {
    const related: VectorResult<KnowledgeEntry>[] = await this.entries.search(content, 5);
    const relatedIds = related
      .filter(r => r.item.id !== id && r.similarity > 0.7)
      .map(r => r.item.id);

    if (relatedIds.length > 0) {
      const entry = await this.entries.get(id);
      if (entry) {
        entry.relatedEntries = relatedIds;
        await this.entries.update(id, entry);
      }
    }
  }
}
The knowledge base implements several patterns from earlier chapters. Memory provides the storage structure. Learning happens through accumulation—each research task potentially adds knowledge. Evaluation appears in confidence scores and source reliability ratings. The linking of related entries creates a semantic network that grows richer over time.

12.4 The Research Pipeline

With the knowledge base as foundation, let’s build the research pipeline that populates and uses it.

The Planner

The planner turns a topic into a prioritized question list, reusing prior knowledge to avoid redundant work:
export default class ResearchPlanner extends AgenticSystem {
  @field knowledgeBase: KnowledgeBase;
  @field currentPlan: ResearchPlan | null = null;
  @field thinking: stream<string>('');

  async plan(topic: string, depth: 'quick' | 'standard' | 'deep'): Promise<ResearchPlan> {
    this.thinking.append(`Planning research: ${topic}\n`);

    // Check what we already know
    const priorKnowledge = await this.knowledgeBase.search(topic, 10);
    this.thinking.append(`Found ${priorKnowledge.length} relevant prior entries\n`);

    // Generate research questions
    const questions = await this.generateQuestions(topic, priorKnowledge, depth);
    this.thinking.append(`Generated ${questions.length} research questions\n`);

    // Identify gaps based on prior knowledge
    const gaps = await this.identifyGaps(topic, priorKnowledge, questions);
    this.thinking.append(`Identified ${gaps.length} knowledge gaps\n`);

    // Prioritize questions
    const prioritized = await this.prioritizeQuestions(questions, gaps);

    this.currentPlan = {
      topic,
      depth,
      questions: prioritized,
      priorKnowledge: priorKnowledge.map(k => k.id),
      status: 'planned',
      createdAt: Date.now()
    };

    return this.currentPlan;
  }

  private async generateQuestions(
    topic: string,
    priorKnowledge: KnowledgeEntry[],
    depth: 'quick' | 'standard' | 'deep'
  ): Promise<ResearchQuestion[]> {
    const questionCount = depth === 'quick' ? 3 : depth === 'standard' ? 5 : 8;

    const priorContext = priorKnowledge.length > 0
      ? `\n\nPrior knowledge on this topic:\n${priorKnowledge.map(k => `- ${k.content}`).join('\n')}`
      : '';

    const completion = await llm.complete([
      { role: 'system', content: `You are a research planner. Generate specific, answerable research questions.
        Questions should be:
        - Specific enough to have clear answers
        - Broad enough to yield useful findings
        - Independent (answering one shouldn't answer another)
        - Prioritized by importance to understanding the topic` },
      { role: 'user', content: `Generate ${questionCount} research questions for: ${topic}${priorContext}

        Format as JSON array: [{"question": "...", "priority": 1-5, "expectedSources": ["web", "papers", "docs"]}]` }
    ]);

    const text = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
    const data = JSON.parse(text);
    // In production, validate that data is an array of {question, priority, expectedSources}
    return data as ResearchQuestion[];
  }

  private async identifyGaps(
    topic: string,
    priorKnowledge: KnowledgeEntry[],
    questions: ResearchQuestion[]
  ): Promise<string[]> {
    if (priorKnowledge.length === 0) {
      return questions.map(q => q.question);
    }

    const completion = await llm.complete([
      { role: 'system', content: 'Identify what we still need to learn given existing knowledge.' },
      { role: 'user', content: `Topic: ${topic}

        What we know:
        ${priorKnowledge.map(k => `- ${k.content}`).join('\n')}

        Questions to answer:
        ${questions.map(q => `- ${q.question}`).join('\n')}

        What specific gaps need to be filled? Return one gap per line, no bullets or numbering.` }
    ]);

    const response = completion.text ?? completion.choices?.[0]?.message?.content ?? '';

    return response
      .split('\n')
      .map(line => line.trim())
      .filter(Boolean);
  }

  private async prioritizeQuestions(
    questions: ResearchQuestion[],
    gaps: string[]
  ): Promise<ResearchQuestion[]> {
    // Boost priority for questions that address gaps
    return questions
      .map(q => ({
        ...q,
        priority: gaps.some(g => g.toLowerCase().includes(q.question.toLowerCase().slice(0, 20)))
          ? Math.min(5, q.priority + 1)
          : q.priority
      }))
      .sort((a, b) => b.priority - a.priority);
  }
}

interface ResearchPlan {
  topic: string;
  depth: 'quick' | 'standard' | 'deep';
  questions: ResearchQuestion[];
  priorKnowledge: string[];
  status: 'planned' | 'gathering' | 'analyzing' | 'writing' | 'complete';
  createdAt: number;
}

interface ResearchQuestion {
  question: string;
  priority: number;
  expectedSources: string[];
  findings?: Finding[];
}
The planner demonstrates context construction—it retrieves relevant prior knowledge and includes it when generating questions. This grounding prevents the system from researching what it already knows and focuses effort on actual gaps.

The Gatherer

The gatherer fans out across the configured sources and records provenance for each snippet it retrieves:
interface SearchClient<T> {
  search(query: string, limit: number): Promise<T[]>;
}

export default class ResearchGatherer extends AgenticSystem {
  @field findings: stream<Finding[]>([]);
  @field sources: Source[] = [];
  @field thinking: stream<string>('');

  // Assume these are injected dependencies implementing SearchClient<T>
  @field webSearch: SearchClient<{ url: string; title: string; snippet: string }>;
  @field paperSearch: SearchClient<{ url: string; title: string; abstract: string; authors: string[]; publishedDate: string }>;
  @field docStore: SearchClient<{ title: string; content: string }>;

  async gather(questions: ResearchQuestion[]): Promise<Finding[]> {
    const allFindings: Finding[] = [];

    for (const question of questions) {
      this.thinking.append(`\nGathering for: ${question.question}\n`);

      const questionFindings = await this.gatherForQuestion(question);
      allFindings.push(...questionFindings);

      // Stream progress
      this.findings.set([...allFindings]);
    }

    return allFindings;
  }

  private async gatherForQuestion(question: ResearchQuestion): Promise<Finding[]> {
    const findings: Finding[] = [];

    for (const sourceType of question.expectedSources) {
      this.thinking.append(`  Searching ${sourceType}...\n`);

      const sourceFindings = await this.searchSource(question.question, sourceType);
      findings.push(...sourceFindings);

      this.thinking.append(`  Found ${sourceFindings.length} results\n`);
    }

    return findings;
  }

  private async searchSource(query: string, sourceType: string): Promise<Finding[]> {
    switch (sourceType) {
      case 'web':
        return this.searchWeb(query);
      case 'papers':
        return this.searchPapers(query);
      case 'docs':
        return this.searchDocs(query);
      default:
        return [];
    }
  }

  @tool({ description: 'Search the web for information' })
  async searchWeb(query: string): Promise<Finding[]> {
    const results = await this.webSearch.search(query, 5);

    return results.map(result => ({
      content: result.snippet,
      source: {
        type: 'web' as const,
        url: result.url,
        title: result.title,
        reliability: this.estimateWebReliability(result.url)
      },
      query,
      relevance: 0 // Will be scored later
    }));
  }

  @tool({ description: 'Search academic papers' })
  async searchPapers(query: string): Promise<Finding[]> {
    const results = await this.paperSearch.search(query, 3);

    return results.map(result => ({
      content: result.abstract,
      source: {
        type: 'paper' as const,
        title: result.title,
        author: result.authors.join(', '),
        date: result.publishedDate,
        url: result.url,
        reliability: 0.85 // Academic sources generally reliable
      },
      query,
      relevance: 0
    }));
  }

  @tool({ description: 'Search internal documentation' })
  async searchDocs(query: string): Promise<Finding[]> {
    const results = await this.docStore.search(query, 5);

    return results.map(result => ({
      content: result.content,
      source: {
        type: 'document' as const,
        title: result.title,
        reliability: 0.9 // Internal docs are authoritative for internal topics
      },
      query,
      relevance: 0
    }));
  }

  private estimateWebReliability(url: string): number {
    // Simple heuristic based on domain. This is a toy example; in a real system
    // you would maintain explicit allowlists/denylists and richer heuristics.
    const domain = new URL(url).hostname;

    if (domain.endsWith('.gov') || domain.endsWith('.edu')) return 0.85;
    if (domain.includes('wikipedia')) return 0.7;
    if (domain.includes('stackoverflow')) return 0.75;
    if (domain.includes('github')) return 0.8;

    // Example of narrowing to exact hosts instead of substring matches
    const reliableHosts = new Set(['learn.microsoft.com', 'cloud.google.com']);
    if (reliableHosts.has(domain)) return 0.85;

    return 0.6; // Default for unknown sources
  }
}

interface Finding {
  content: string;
  source: Source;
  query: string;
  relevance: number;
}

interface Source {
  type: 'web' | 'paper' | 'document' | 'expert' | 'derived';
  url?: string;
  title: string;
  author?: string;
  date?: string;
  reliability: number;
}
The gatherer demonstrates agency through its tools for searching different sources. Source reliability estimation implements simple evaluation—not all sources are equal. The streaming findings provide observability into gathering progress.

The Analyzer

Analysis transforms raw findings into evaluated, connected knowledge. This is where signal gets separated from noise.
export default class ResearchAnalyzer extends AgenticSystem {
  @field thinking: stream<string>('');

  async analyze(findings: Finding[], questions: ResearchQuestion[]): Promise<AnalysisResult> {
    this.thinking.append(`Analyzing ${findings.length} findings...\n`);

    // Score relevance
    const scored = await this.scoreRelevance(findings, questions);
    this.thinking.append(`Scored relevance for all findings\n`);

    // Filter low-quality findings
    const filtered = scored.filter(f => f.relevance > 0.5 && f.source.reliability > 0.5);
    this.thinking.append(`${filtered.length} findings passed quality filter\n`);

    // Extract key facts
    const facts = await this.extractFacts(filtered);
    this.thinking.append(`Extracted ${facts.length} key facts\n`);

    // Identify contradictions
    const contradictions = await this.findContradictions(facts);
    if (contradictions.length > 0) {
      this.thinking.append(`Found ${contradictions.length} contradictions to resolve\n`);
    }

    // Identify gaps
    const gaps = await this.identifyGaps(questions, facts);
    this.thinking.append(`Identified ${gaps.length} remaining gaps\n`);

    return {
      findings: filtered,
      facts,
      contradictions,
      gaps,
      confidence: this.calculateOverallConfidence(filtered, facts, gaps)
    };
  }

  private async scoreRelevance(findings: Finding[], questions: ResearchQuestion[]): Promise<Finding[]> {
    const questionText = questions.map(q => q.question).join('\n');

    return Promise.all(findings.map(async finding => {
      const completion = await llm.complete([
        { role: 'system', content: 'Score how relevant this finding is to the research questions. Return a number 0-1.' },
        { role: 'user', content: `Questions:\n${questionText}\n\nFinding: ${finding.content}` }
      ]);

      const raw = (completion.text ?? completion.choices?.[0]?.message?.content ?? '').trim();
      const numeric = Number.parseFloat(raw.match(/[0-9.]+/)?.[0] ?? '0.5');
      const score = Math.min(1, Math.max(0, numeric || 0.5));

      return {
        ...finding,
        relevance: score
      };
    }));
  }

  private async extractFacts(findings: Finding[]): Promise<Fact[]> {
    const completion = await llm.complete([
      { role: 'system', content: `Extract discrete, verifiable facts from these findings.
        Each fact should be:
        - A single, specific claim
        - Attributable to a source
        - Verifiable or falsifiable
        Return as JSON array: [{"fact": "...", "sourceIndex": 0}]` },
      { role: 'user', content: findings.map((f, i) => `[${i}] ${f.content}`).join('\n\n') }
    ]);

    const text = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
    const extracted = JSON.parse(text) as { fact: string; sourceIndex: number }[];

    // In production you would validate that sourceIndex is in range
    return extracted.map(e => ({
      content: e.fact,
      source: findings[e.sourceIndex]?.source,
      confidence: findings[e.sourceIndex]?.source.reliability || 0.5
    }));
  }

  private async findContradictions(facts: Fact[]): Promise<Contradiction[]> {
    if (facts.length < 2) return [];

    const completion = await llm.complete([
      { role: 'system', content: `Identify any contradictions between these facts.
        Return as JSON array: [{"fact1Index": 0, "fact2Index": 1, "description": "..."}]
        Return empty array [] if no contradictions.` },
      { role: 'user', content: facts.map((f, i) => `[${i}] ${f.content}`).join('\n') }
    ]);

    const text = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
    const contradictions = JSON.parse(text) as { fact1Index: number; fact2Index: number; description: string }[];

    return contradictions.map(c => ({
      fact1: facts[c.fact1Index],
      fact2: facts[c.fact2Index],
      description: c.description
    }));
  }

  private async identifyGaps(questions: ResearchQuestion[], facts: Fact[]): Promise<string[]> {
    const completion = await llm.complete([
      { role: 'system', content: 'Identify which research questions are not adequately answered by the available facts. Return one gap per line, no bullets or numbering.' },
      { role: 'user', content: `Questions:\n${questions.map(q => `- ${q.question}`).join('\n')}\n\nFacts:\n${facts.map(f => `- ${f.content}`).join('\n')}\n\nWhat questions remain unanswered or inadequately answered?` }
    ]);

    const response = completion.text ?? completion.choices?.[0]?.message?.content ?? '';

    return response
      .split('\n')
      .map(line => line.trim())
      .filter(Boolean);
  }

  private calculateOverallConfidence(findings: Finding[], facts: Fact[], gaps: string[]): number {
    if (facts.length === 0) return 0;

    const avgSourceReliability = findings.reduce((sum, f) => sum + f.source.reliability, 0) / findings.length;
    const avgFactConfidence = facts.reduce((sum, f) => sum + f.confidence, 0) / facts.length;
    const gapPenalty = Math.min(0.3, gaps.length * 0.1);

    return Math.max(0, (avgSourceReliability + avgFactConfidence) / 2 - gapPenalty);
  }
}

interface Fact {
  content: string;
  source: Source;
  confidence: number;
}

interface Contradiction {
  fact1: Fact;
  fact2: Fact;
  description: string;
}

interface AnalysisResult {
  findings: Finding[];
  facts: Fact[];
  contradictions: Contradiction[];
  gaps: string[];
  confidence: number;
}
The analyzer implements the generate-filter pattern—scoring findings and keeping only high-quality ones. Contradiction detection is a form of self-critique, catching inconsistencies before they reach the output. For example, if one fact claims “service A encrypts all data at rest” and another states “backups for service A are stored unencrypted”, the analyzer will flag this pair so the writer can either reconcile it or highlight the inconsistency explicitly in the report. Gap identification enables adaptive planning—if gaps exist, the system can trigger more gathering.

The Writer

Writing synthesizes analysis into coherent output. The writer’s job is to answer the original questions using the facts gathered, with appropriate citations. The writer assembles an outline, fills in sections from facts, and adds citations and summaries to produce a structured report:
export default class ResearchWriter extends AgenticSystem {
  @field draft: stream<string>('');
  @field thinking: stream<string>('');

  async write(
    topic: string,
    questions: ResearchQuestion[],
    analysis: AnalysisResult
  ): Promise<ResearchReport> {
    this.thinking.append(`Writing report on: ${topic}\n`);
    this.draft.reset('');

    // Generate outline
    const outline = await this.createOutline(topic, questions, analysis);
    this.thinking.append(`Created outline with ${outline.sections.length} sections\n`);

    // Write each section
    const sections: ReportSection[] = [];
    for (const section of outline.sections) {
      this.thinking.append(`Writing: ${section.title}\n`);

      const content = await this.writeSection(section, analysis);
      sections.push({ title: section.title, content });

      // Stream progress
      this.draft.append(`## ${section.title}\n\n${content}\n\n`);
    }

    // Write executive summary
    const summary = await this.writeSummary(topic, sections, analysis);
    this.thinking.append(`Completed executive summary\n`);

    // Compile citations
    const citations = this.compileCitations(analysis.findings);

    return {
      topic,
      summary,
      sections,
      citations,
      confidence: analysis.confidence,
      gaps: analysis.gaps,
      generatedAt: Date.now()
    };
  }

  private async createOutline(
    topic: string,
    questions: ResearchQuestion[],
    analysis: AnalysisResult
  ): Promise<{ sections: { title: string; questions: string[]; facts: Fact[] }[] }> {
    const completion = await llm.complete([
      { role: 'system', content: `Create an outline for a research report.
        Each section should address one or more research questions.
        Return as JSON: {"sections": [{"title": "...", "questionIndices": [0, 1]}]}` },
      { role: 'user', content: `Topic: ${topic}\n\nQuestions:\n${questions.map((q, i) => `[${i}] ${q.question}`).join('\n')}` }
    ]);

    const text = completion.text ?? completion.choices?.[0]?.message?.content ?? '';
    const outline = JSON.parse(text);

    return {
      sections: outline.sections.map((s: any) => ({
        title: s.title,
        questions: s.questionIndices.map((i: number) => questions[i]?.question).filter(Boolean),
        facts: analysis.facts // All facts available to each section
      }))
    };
  }

  private async writeSection(
    section: { title: string; questions: string[]; facts: Fact[] },
    analysis: AnalysisResult
  ): Promise<string> {
    // For brevity this example uses a naive keyword match. In practice you would
    // score fact–question similarity (for example using embeddings) and keep
    // facts above a threshold.
    const relevantFacts = section.facts.filter(f =>
      section.questions.some(q =>
        f.content.toLowerCase().includes(q.toLowerCase().split(' ')[0])
      )
    );

    const completion = await llm.complete([
      { role: 'system', content: `Write a section of a research report.
        - Use the provided facts as your source material
        - Cite facts using [n] notation where n is the fact number
        - Be comprehensive but concise
        - Acknowledge limitations or gaps` },
      { role: 'user', content: `Section: ${section.title}

        Questions to address:
        ${section.questions.map(q => `- ${q}`).join('\n')}

        Available facts:
        ${relevantFacts.map((f, i) => `[${i + 1}] ${f.content}`).join('\n')}

        ${analysis.contradictions.length > 0 ? `\nNote these contradictions:\n${analysis.contradictions.map(c => `- ${c.description}`).join('\n')}` : ''}` }
    ]);

    return completion.text ?? completion.choices?.[0]?.message?.content ?? '';
  }

  private async writeSummary(
    topic: string,
    sections: ReportSection[],
    analysis: AnalysisResult
  ): Promise<string> {
    const completion = await llm.complete([
      { role: 'system', content: `Write an executive summary for this research report.
        - Highlight key findings
        - Note confidence level and any major gaps
        - Keep it to 2-3 paragraphs` },
      { role: 'user', content: `Topic: ${topic}

        Sections:
        ${sections.map(s => `${s.title}: ${s.content.slice(0, 200)}...`).join('\n\n')}

        Overall confidence: ${Math.round(analysis.confidence * 100)}%
        Gaps: ${analysis.gaps.join(', ') || 'None identified'}` }
    ]);

    return completion.text ?? completion.choices?.[0]?.message?.content ?? '';
  }

  private compileCitations(findings: Finding[]): Citation[] {
    const uniqueSources = new Map<string, Source>();

    for (const finding of findings) {
      const key = finding.source.url || finding.source.title;
      if (!uniqueSources.has(key)) {
        uniqueSources.set(key, finding.source);
      }
    }

    return Array.from(uniqueSources.values()).map((source, i) => ({
      number: i + 1,
      ...source
    }));
  }
}

interface ReportSection {
  title: string;
  content: string;
}

interface Citation extends Source {
  number: number;
}

interface ResearchReport {
  topic: string;
  summary: string;
  sections: ReportSection[];
  citations: Citation[];
  confidence: number;
  gaps: string[];
  generatedAt: number;
}
The writer demonstrates structured artifact creation—the report has defined structure with sections, citations, and metadata. Citation compilation ensures grounding—claims trace back to sources.

12.5 The Complete System

Now we wire the components together into a complete research system.
export default class ResearchSystem extends AgenticSystem {
  // Components
  @field knowledgeBase: KnowledgeBase;
  @field planner: ResearchPlanner;
  @field gatherer: ResearchGatherer;
  @field analyzer: ResearchAnalyzer;
  @field writer: ResearchWriter;

  // State
  @field queue: ResearchRequest[] = [];
  @field currentResearch: ResearchPlan | null = null;
  @field status: stream<string>('');
  @field reports: ResearchReport[] = [];

  async initialize() {
    this.knowledgeBase = new KnowledgeBase();
    this.planner = new ResearchPlanner();
    this.planner.knowledgeBase = this.knowledgeBase;
    this.gatherer = new ResearchGatherer();
    this.analyzer = new ResearchAnalyzer();
    this.writer = new ResearchWriter();

    this.status.append('Research system initialized\n');
  }

  @action()
  async research(topic: string, depth: 'quick' | 'standard' | 'deep' = 'standard'): Promise<ResearchReport> {
    this.status.append(`\n=== Starting research: ${topic} ===\n`);

    // Plan
    this.status.append('Planning...\n');
    const plan = await this.planner.plan(topic, depth);
    this.currentResearch = plan;

    // Gather
    this.status.append('Gathering information...\n');
    plan.status = 'gathering';
    const findings = await this.gatherer.gather(plan.questions);

    // Analyze
    this.status.append('Analyzing findings...\n');
    plan.status = 'analyzing';
    const analysis = await this.analyzer.analyze(findings, plan.questions);

    // Check if we need more research
    if (analysis.gaps.length > 0 && analysis.confidence < 0.6) {
      this.status.append(`Low confidence (${Math.round(analysis.confidence * 100)}%), gathering more...\n`);
      const additionalFindings = await this.gatherForGaps(analysis.gaps);
      findings.push(...additionalFindings);
      const reanalysis = await this.analyzer.analyze(findings, plan.questions);
      Object.assign(analysis, reanalysis);
    }

    // Write
    this.status.append('Writing report...\n');
    plan.status = 'writing';
    const report = await this.writer.write(topic, plan.questions, analysis);

    // Store knowledge
    this.status.append('Storing knowledge...\n');
    await this.storeKnowledge(analysis.facts);

    // Complete
    plan.status = 'complete';
    this.currentResearch = null;
    this.reports.push(report);

    this.status.append(`=== Research complete: ${Math.round(report.confidence * 100)}% confidence ===\n`);

    return report;
  }

  private async gatherForGaps(gaps: string[]): Promise<Finding[]> {
    const additionalQuestions: ResearchQuestion[] = gaps.map(gap => ({
      question: gap,
      priority: 4,
      expectedSources: ['web', 'docs']
    }));

    return this.gatherer.gather(additionalQuestions);
  }

  private async storeKnowledge(facts: Fact[]) {
    for (const fact of facts) {
      if (fact.confidence >= 0.7) {
        await this.knowledgeBase.store({
          content: fact.content,
          topic: this.currentResearch?.topic || 'general',
          sources: [fact.source],
          confidence: fact.confidence,
          lastVerified: Date.now()
        });
      }
    }
  }

  // Autonomous operation
  // Assume @schedule is provided by the AgenticSystem framework and registers
  // processQueue with a scheduler that runs every 30 minutes. In other environments
  // you would wire this to a cron job or task scheduler explicitly.
  @schedule('every 30 minutes')
  async processQueue() {
    if (this.currentResearch || this.queue.length === 0) return;

    const request = this.queue.shift()!;
    await this.research(request.topic, request.depth);
  }

  @action()
  queueResearch(topic: string, depth: 'quick' | 'standard' | 'deep' = 'standard') {
    this.queue.push({ topic, depth, queuedAt: Date.now() });
    this.status.append(`Queued: ${topic}\n`);
  }
}

interface ResearchRequest {
  topic: string;
  depth: 'quick' | 'standard' | 'deep';
  queuedAt: number;
}

12.6 Learning Through Accumulation

The research system learns by accumulating knowledge. Each research task potentially adds to the knowledge base, and future tasks retrieve relevant prior knowledge. As the knowledge base grows, later research can reuse earlier findings instead of rediscovering them, so the system needs fewer external queries to reach the same level of detail. Consider researching “Kubernetes security best practices” after previously researching “container isolation” and “network policies.” The planner retrieves related knowledge, identifies what’s already known, and focuses new research on actual gaps. The analyzer can cross-reference new findings against prior knowledge. The writer can draw on accumulated understanding to produce richer reports. For instance, once the system has accumulated patterns of common misconfigurations from several cloud security reviews, later Kubernetes security research can reuse those patterns to focus search on likely weak points rather than relearning them from scratch. This is learning without model fine-tuning. The model’s capabilities don’t change, but the context it operates in becomes richer. The knowledge base acts as an external memory that the model can draw upon, effectively expanding its expertise through accumulated research. This form of learning has limits: the knowledge base can become stale or contradictory, and retrieval quality degrades without curation. In practice you need periodic pruning, re-validation of older entries, and monitoring of retrieval quality as the store grows.

Key Takeaways

  • Research systems require adaptive exploration, not fixed plans
  • Source evaluation separates signal from noise—not all information is equally reliable
  • Synthesis is generative work distinct from retrieval
  • Knowledge accumulation lets later research reuse earlier findings instead of repeating work
  • The gather-analyze-write pipeline provides clear separation of concerns
  • Confidence scoring and gap identification enable adaptive depth

Transition

Chapter 12 built a system that accumulates knowledge through research. Chapter 13: Code Agent addresses a different domain—verification-driven development where tests provide the feedback signal that drives iteration toward correctness.