Skip to content

Research Assistant

A research assistant that accepts research requests over HTTP, ingests reference documents, searches the web for current information, uses a plan-and-execute pattern for multi-step research, self-reflects on report quality, and saves the final cited report to disk. It combines RAG retrieval, web search, and structured planning to produce thorough, sourced reports.


Project Structure

research-assistant/
├── src/
│ └── index.ts # Main application entry point
├── docs/ # Reference documents to ingest
│ ├── company-brief.md
│ └── style-guide.md
├── reports/ # Generated reports saved here
├── package.json
├── tsconfig.json
└── .env

Setup

Terminal window
mkdir research-assistant && cd research-assistant
npm init -y
npm install @kernova/sdk @kernova/core
npm install -D typescript @types/node
npx tsc --init
mkdir docs reports

Environment Variables

Create a .env file:

OPENAI_API_KEY=sk-...
GATEWAY_PORT=8080 # Port for the research HTTP endpoint
REPORTS_DIR=./reports # Directory for saved reports

Full Source Code

src/index.ts
import { Kernova } from '@kernova/sdk';
import { readFileSync, readdirSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { createServer, type ServerResponse } from 'node:http';
const REPORTS_DIR = process.env.REPORTS_DIR || './reports';
const GATEWAY_PORT = Number(process.env.GATEWAY_PORT) || 8080;
async function main() {
// ─── 1. Initialize Kernova with web search and file tools ─────
const app = await Kernova.create({
env: 'production',
dataDir: './research-data',
builtInTools: {
enabled: ['web-search', 'http-fetch', 'file-write', 'date-time'],
// web-search also needs a backend: webSearchProvider (see the Built-in Tools guide)
sandboxDir: REPORTS_DIR,
},
});
// Ensure reports directory exists
if (!existsSync(REPORTS_DIR)) mkdirSync(REPORTS_DIR, { recursive: true });
// ─── 2. Ingest reference documents via RAG ────────────────────
// Load all markdown files from the docs/ directory
const docsDir = './docs';
if (existsSync(docsDir)) {
const docFiles = readdirSync(docsDir).filter(f => f.endsWith('.md'));
const kernel = app.getKernel();
for (const file of docFiles) {
const content = readFileSync(join(docsDir, file), 'utf-8');
await kernel.rag.ingest(content, file, {
category: 'reference',
chunkStrategy: 'recursive',
chunkSize: 512,
chunkOverlap: 50,
});
console.log(`[RAG] Ingested: ${file}`);
}
const stats = kernel.rag.stats();
console.log(`[RAG] ${stats.documents} documents, ${stats.chunks} chunks indexed`);
}
// ─── 3. Define the research agent ─────────────────────────────
app.agent('researcher', {
instructions: `You are a thorough research assistant.
Your process for every research request:
1. Break the topic into 3-5 specific sub-questions
2. Search the web for each sub-question using web-search
3. Cross-reference findings with ingested reference documents
4. Synthesize findings into a structured report
Report format:
- Executive Summary (2-3 sentences)
- Key Findings (bullet points with citations)
- Detailed Analysis (organized by sub-topic)
- Sources (numbered list of URLs/documents used)
Rules:
- Always cite sources with [1], [2], etc. notation
- Distinguish between facts (from sources) and analysis (your synthesis)
- Flag areas where sources conflict or information is uncertain
- Never fabricate citations or URLs`,
description: 'Multi-step research with web search and document retrieval',
tools: ['web-search', 'http-fetch'],
});
// ─── 4. Define the quality reviewer agent ─────────────────────
app.agent('quality-reviewer', {
instructions: `You are a research quality reviewer.
Evaluate research reports on these criteria:
- Completeness: Does it address all aspects of the research question?
- Accuracy: Are claims properly supported by cited sources?
- Clarity: Is the report well-organized and easy to follow?
- Citations: Are all factual claims cited? Are sources credible?
Score each criterion 1-5 and provide specific improvement suggestions.
If the overall score is below 3.5/5, recommend specific revisions.
Output format:
SCORE: X.X/5
COMPLETENESS: X/5 - brief note
ACCURACY: X/5 - brief note
CLARITY: X/5 - brief note
CITATIONS: X/5 - brief note
VERDICT: PASS | NEEDS_REVISION
SUGGESTIONS: (if revision needed)`,
description: 'Quality control for research reports',
});
// ─── 5. Custom tool: save report to file ──────────────────────
app.tool('save-report', {
description: 'Save a research report to the reports directory',
parameters: {
type: 'object',
properties: {
filename: { type: 'string', description: 'Filename (without extension)' },
content: { type: 'string', description: 'Full report content in Markdown' },
metadata: {
type: 'object',
description: 'Report metadata (topic, date, sources count)',
},
},
},
execute: async (params) => {
const { filename, content, metadata } = params as {
filename: string;
content: string;
metadata?: Record<string, unknown>;
};
const safeName = filename.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
const filepath = join(REPORTS_DIR, `${safeName}.md`);
const header = [
'---',
`title: "${metadata?.topic || filename}"`,
`generated: "${new Date().toISOString()}"`,
`sources: ${metadata?.sourcesCount || 'unknown'}`,
'---',
'',
].join('\n');
const { writeFileSync } = await import('fs');
writeFileSync(filepath, header + content, 'utf-8');
return { saved: true, path: filepath, size: content.length };
},
});
// ─── 6. Memory for tracking research findings ─────────────────
// Store key findings so future research can build on past work
app.tool('store-finding', {
description: 'Store a key research finding for future reference',
parameters: {
type: 'object',
properties: {
finding: { type: 'string', description: 'The research finding to store' },
topic: { type: 'string', description: 'Research topic category' },
confidence: { type: 'number', description: 'Confidence level 0-1' },
},
},
execute: async (params) => {
const { finding, topic, confidence } = params as {
finding: string;
topic: string;
confidence?: number;
};
await app.remember(
`Research finding [${topic}]: ${finding}`,
'longterm',
['research-finding', topic],
);
return { stored: true, topic, confidence };
},
});
// ─── 7. The research workflow (plan → execute → reflect) ──────
async function conductResearch(topic: string, depth: 'quick' | 'thorough' = 'thorough') {
console.log(`[RESEARCH] Starting: "${topic}" (${depth})`);
const startTime = Date.now();
// Step 1: Recall any previous research on this topic
const priorFindings = await app.recall(topic, { layer: 'longterm', limit: 10 });
const priorContext = priorFindings.length > 0
? `\nPrior research on this topic:\n${priorFindings.map(f => `- ${f.content}`).join('\n')}`
: '';
// Step 2: Plan and execute the research using multi-step planning
const maxSteps = depth === 'thorough' ? 8 : 4;
const planResult = await app.plan(
`Research the following topic thoroughly: "${topic}"
${priorContext}
Steps to follow:
1. Use web-search to find current information about this topic
2. Search for 2-3 different angles or sub-topics
3. Check ingested reference documents for relevant context
4. Compile findings with proper citations
5. Write a structured report following the standard format
Use web-search and http-fetch tools to gather real information.
Do NOT fabricate sources or URLs.`,
{ maxSteps },
);
console.log(`[RESEARCH] Plan completed: ${planResult.success ? 'success' : 'partial'}`);
// Step 3: Generate the final report
const { answer: report } = await app.ask(
`Based on the research just conducted on "${topic}", write a final research report.
Include all findings with proper [1], [2] citation notation.
Follow the standard report format: Executive Summary, Key Findings,
Detailed Analysis, and Sources list.
${priorContext}`,
{ type: 'action', priority: 'high' },
);
// Step 4: Self-reflection — quality review
const { answer: review } = await app.ask(
`Review this research report for quality:
TOPIC: ${topic}
REPORT:
${report}
Evaluate completeness, accuracy, clarity, and citation quality.
Provide your SCORE and VERDICT.`,
{ type: 'query', priority: 'medium' },
);
console.log(`[QUALITY] Review:\n${review.slice(0, 200)}`);
// Step 5: If quality is low, revise
let finalReport = report;
if (review.includes('NEEDS_REVISION')) {
console.log('[QUALITY] Revision needed, improving report...');
const { answer: revised } = await app.ask(
`The research report on "${topic}" needs revision based on this feedback:
${review}
Original report:
${report}
Please produce an improved version addressing all feedback points.`,
{ type: 'action', priority: 'high' },
);
finalReport = revised;
}
// Step 6: Save the report
const { answer: saveConfirm } = await app.ask(
`Use the save-report tool to save this research report:
Filename: ${topic.toLowerCase().replace(/\s+/g, '-')}
Content: ${finalReport}
Metadata: { "topic": "${topic}", "sourcesCount": 5 }`,
{ type: 'action', priority: 'medium' },
);
// Step 7: Store key findings for future research
await app.remember(
`Completed research on "${topic}" (${depth}). Key takeaway: ${finalReport.slice(0, 200)}`,
'longterm',
['research-completed', topic],
);
const durationMs = Date.now() - startTime;
console.log(`[RESEARCH] Completed in ${(durationMs / 1000).toFixed(1)}s`);
return { report: finalReport, review, durationMs };
}
// ─── 8. HTTP endpoint for research requests ───────────────────
// POST /research { "topic": "...", "depth": "quick|thorough" } and GET /reports.
// A plain Node server: kernel.gateway serves Kernova's built-in API, not custom routes.
const kernel = app.getKernel();
const json = (res: ServerResponse, status: number, body: unknown) => {
res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify(body));
};
createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/research') {
let raw = '';
for await (const chunk of req) raw += chunk;
const { topic, depth } = JSON.parse(raw || '{}') as { topic?: string; depth?: 'quick' | 'thorough' };
if (!topic) return json(res, 400, { error: 'Missing "topic" field' });
// Run research asynchronously and return a task ID
const taskId = await app.enqueue('research-job', { topic, depth: depth || 'thorough' });
return json(res, 202, { taskId, message: `Research started: "${topic}"`, status: 'processing' });
}
if (req.method === 'GET' && req.url === '/reports') {
const files = existsSync(REPORTS_DIR)
? readdirSync(REPORTS_DIR).filter(f => f.endsWith('.md'))
: [];
return json(res, 200, { reports: files, count: files.length });
}
json(res, 404, { error: 'Not found' });
}).listen(GATEWAY_PORT, '127.0.0.1'); // add authentication before exposing it beyond localhost
// ─── 9. Task queue handler for research jobs ──────────────────
kernel.taskQueue.registerHandler('research-job', async (task) => {
const { topic, depth } = task.payload as { topic: string; depth: 'quick' | 'thorough' };
await conductResearch(topic, depth);
});
// ─── 10. Start the daemon ─────────────────────────────────────
console.log('📚 Research Assistant starting...');
console.log(` HTTP endpoint on port ${GATEWAY_PORT}`);
console.log(` POST /research { "topic": "...", "depth": "quick|thorough" }`);
console.log(` GET /reports`);
console.log(` Reports saved to: ${REPORTS_DIR}`);
await app.serve({
shutdownTimeoutMs: 60_000, // Research tasks can take a while
heartbeatIntervalMs: 30_000,
});
}
main().catch(console.error);

Testing It

Submit a research request via the API:

Terminal window
# Start a research job
curl -X POST http://localhost:8080/research \
-H "Content-Type: application/json" \
-d '{"topic": "Best practices for LLM-powered agents in production", "depth": "thorough"}'
# Check generated reports
curl http://localhost:8080/reports

Add reference documents to the docs/ folder before starting — the agent will cross-reference them during research:

Terminal window
echo "# Company Brief\nWe build developer tools..." > docs/company-brief.md
echo "# Style Guide\nReports should be concise..." > docs/style-guide.md

What This Demonstrates

FeatureGuide
RAG pipeline (document ingestion & retrieval)Context & Memory
Built-in web-search toolBuilt-in Tools
Plan-and-execute patternIntelligence Modules
Self-reflection for quality controlIntelligence Modules
Long-term memory for research findingsContext & Memory
File-write tool for saving reportsBuilt-in Tools
HTTP endpoint (a plain Node server; see the Gateway for the built-in API)Gateway
Task queue for async job processingTask Queue
Multi-agent (researcher + reviewer)Building Agents