@kernel.chat/kbot 3.88.0 → 3.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,11 +43,11 @@ export function renderLighting(ctx, lights, width, height, ambientLight) {
43
43
  */
44
44
  export function getAmbientForTime(timeOfDay) {
45
45
  switch (timeOfDay) {
46
- case 'night': return 0.15;
47
- case 'day': return 0.6;
48
- case 'sunset': return 0.35;
49
- case 'dawn': return 0.25;
50
- default: return 0.3;
46
+ case 'night': return 0.45;
47
+ case 'day': return 0.8;
48
+ case 'sunset': return 0.6;
49
+ case 'dawn': return 0.5;
50
+ default: return 0.55;
51
51
  }
52
52
  }
53
53
  /**
@@ -0,0 +1,58 @@
1
+ /**
2
+ * research-engine.ts — Autonomous Web Research Pipeline
3
+ *
4
+ * kbot discovers new techniques, knowledge, and information from the internet
5
+ * using its own built-in browser. No Chrome, no Playwright — pure HTML.
6
+ *
7
+ * Architecture:
8
+ * 1. Research Queue — tasks from evolution, brain, narrative, user, or autonomous
9
+ * 2. Web Search — kbot_search via DuckDuckGo (built-in browser)
10
+ * 3. Page Reading — kbot_browse for full content extraction
11
+ * 4. Summarization — local Ollama (kernel:latest) for zero-cost summaries
12
+ * 5. Result Storage — ~/.kbot/research-results.json (persists across sessions)
13
+ * 6. Engine Integration — results tagged by applicable engine for routing
14
+ *
15
+ * Tools: research_queue, research_results, research_now
16
+ *
17
+ * Tick-based: tickResearch() called from the frame loop processes one task
18
+ * per interval (default 3600 frames = ~10 min at 6 fps).
19
+ */
20
+ export interface ResearchEngine {
21
+ queue: ResearchTask[];
22
+ completed: ResearchResult[];
23
+ activeTask: ResearchTask | null;
24
+ lastResearchFrame: number;
25
+ researchInterval: number;
26
+ topicsOfInterest: string[];
27
+ }
28
+ export interface ResearchTask {
29
+ id: string;
30
+ query: string;
31
+ purpose: string;
32
+ source: 'evolution' | 'brain' | 'narrative' | 'user' | 'autonomous';
33
+ status: 'queued' | 'searching' | 'reading' | 'summarizing' | 'complete' | 'failed';
34
+ startedAt: number;
35
+ }
36
+ export interface ResearchResult {
37
+ taskId: string;
38
+ query: string;
39
+ summary: string;
40
+ sources: string[];
41
+ keyFindings: string[];
42
+ applicableTo: string[];
43
+ timestamp: number;
44
+ }
45
+ export interface ResearchAction {
46
+ type: 'start_research' | 'search_complete' | 'read_complete' | 'summarize_complete' | 'failed';
47
+ task: ResearchTask;
48
+ result?: ResearchResult;
49
+ speech?: string;
50
+ }
51
+ export declare function autonomousResearchTopics(): string[];
52
+ export declare function initResearchEngine(): ResearchEngine;
53
+ export declare function getResearchEngine(): ResearchEngine;
54
+ export declare function queueResearch(engine: ResearchEngine, query: string, purpose: string, source: ResearchTask['source']): ResearchTask;
55
+ export declare function getResearchForEngine(engine: ResearchEngine, engineName: string): ResearchResult[];
56
+ export declare function tickResearch(engine: ResearchEngine, frame: number): Promise<ResearchAction | null>;
57
+ export declare function registerResearchEngineTools(): void;
58
+ //# sourceMappingURL=research-engine.d.ts.map
@@ -0,0 +1,550 @@
1
+ /**
2
+ * research-engine.ts — Autonomous Web Research Pipeline
3
+ *
4
+ * kbot discovers new techniques, knowledge, and information from the internet
5
+ * using its own built-in browser. No Chrome, no Playwright — pure HTML.
6
+ *
7
+ * Architecture:
8
+ * 1. Research Queue — tasks from evolution, brain, narrative, user, or autonomous
9
+ * 2. Web Search — kbot_search via DuckDuckGo (built-in browser)
10
+ * 3. Page Reading — kbot_browse for full content extraction
11
+ * 4. Summarization — local Ollama (kernel:latest) for zero-cost summaries
12
+ * 5. Result Storage — ~/.kbot/research-results.json (persists across sessions)
13
+ * 6. Engine Integration — results tagged by applicable engine for routing
14
+ *
15
+ * Tools: research_queue, research_results, research_now
16
+ *
17
+ * Tick-based: tickResearch() called from the frame loop processes one task
18
+ * per interval (default 3600 frames = ~10 min at 6 fps).
19
+ */
20
+ import { registerTool } from './index.js';
21
+ import { homedir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
24
+ import { randomUUID } from 'node:crypto';
25
+ // ─── Constants ──────────────────────────────────────────────────
26
+ const KBOT_DIR = join(homedir(), '.kbot');
27
+ const RESULTS_FILE = join(KBOT_DIR, 'research-results.json');
28
+ const OLLAMA_URL = 'http://localhost:11434';
29
+ const OLLAMA_MODEL = 'kernel:latest';
30
+ const OLLAMA_TIMEOUT = 90_000;
31
+ const MAX_COMPLETED = 200; // keep last N results in memory
32
+ const MAX_PERSISTED = 500; // keep last N results on disk
33
+ const MAX_LINKS_TO_READ = 3; // read top N links per search
34
+ const DEFAULT_RESEARCH_INTERVAL = 3600; // frames between research tasks (~10 min at 6fps)
35
+ // ─── Module State ───────────────────────────────────────────────
36
+ let engineState = null;
37
+ // ─── Persistence ────────────────────────────────────────────────
38
+ function loadResults() {
39
+ try {
40
+ if (existsSync(RESULTS_FILE)) {
41
+ const raw = readFileSync(RESULTS_FILE, 'utf-8');
42
+ const parsed = JSON.parse(raw);
43
+ if (Array.isArray(parsed))
44
+ return parsed.slice(-MAX_PERSISTED);
45
+ }
46
+ }
47
+ catch { /* corrupt file — start fresh */ }
48
+ return [];
49
+ }
50
+ function saveResults(results) {
51
+ try {
52
+ if (!existsSync(KBOT_DIR))
53
+ mkdirSync(KBOT_DIR, { recursive: true });
54
+ const trimmed = results.slice(-MAX_PERSISTED);
55
+ writeFileSync(RESULTS_FILE, JSON.stringify(trimmed, null, 2));
56
+ }
57
+ catch { /* disk error — non-fatal */ }
58
+ }
59
+ // ─── Ollama Integration ─────────────────────────────────────────
60
+ async function isOllamaAvailable() {
61
+ try {
62
+ const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) });
63
+ return res.ok;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ async function ollamaGenerate(prompt, model = OLLAMA_MODEL) {
70
+ try {
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT);
73
+ const res = await fetch(`${OLLAMA_URL}/api/generate`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({
77
+ model,
78
+ prompt,
79
+ stream: false,
80
+ options: { temperature: 0.3, num_predict: 2048 },
81
+ }),
82
+ signal: controller.signal,
83
+ });
84
+ clearTimeout(timer);
85
+ if (!res.ok)
86
+ return null;
87
+ const data = await res.json();
88
+ return data.response?.trim() || null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ // ─── Link Extraction ────────────────────────────────────────────
95
+ /**
96
+ * Extract URLs from kbot_search / kbot_browse output.
97
+ * The browser format is: `[N] link text` with URLs on the page,
98
+ * plus inline `URL: https://...` lines in the content.
99
+ */
100
+ function extractLinks(searchOutput) {
101
+ const urls = [];
102
+ const seen = new Set();
103
+ // Match URLs in the output (DuckDuckGo results include them in content)
104
+ const urlRegex = /https?:\/\/[^\s<>"')\]]+/g;
105
+ let match;
106
+ while ((match = urlRegex.exec(searchOutput)) !== null) {
107
+ const url = match[0].replace(/[.,;:!?]+$/, ''); // trim trailing punctuation
108
+ if (!seen.has(url) && !url.includes('duckduckgo.com') && !url.includes('127.0.0.1')) {
109
+ seen.add(url);
110
+ urls.push(url);
111
+ }
112
+ }
113
+ return urls;
114
+ }
115
+ // ─── Key Findings Extraction ────────────────────────────────────
116
+ function extractKeyFindings(summary) {
117
+ const findings = [];
118
+ const lines = summary.split('\n');
119
+ for (const line of lines) {
120
+ const trimmed = line.trim();
121
+ // Bullet points, numbered items, or lines starting with key phrases
122
+ if (/^[-*]\s+/.test(trimmed) || /^\d+[.)]\s+/.test(trimmed)) {
123
+ const clean = trimmed.replace(/^[-*\d.)\s]+/, '').trim();
124
+ if (clean.length > 15 && clean.length < 300) {
125
+ findings.push(clean);
126
+ }
127
+ }
128
+ }
129
+ // If no structured findings, split summary into sentence-level findings
130
+ if (findings.length === 0) {
131
+ const sentences = summary.split(/[.!?]+/).filter(s => s.trim().length > 20);
132
+ for (const s of sentences.slice(0, 5)) {
133
+ findings.push(s.trim());
134
+ }
135
+ }
136
+ return findings.slice(0, 10);
137
+ }
138
+ // ─── Engine Applicability Detection ─────────────────────────────
139
+ const ENGINE_KEYWORDS = {
140
+ evolution: ['rendering', 'pixel', 'canvas', 'shader', 'graphics', 'animation', 'parallax', 'palette', 'lighting', 'visual', 'sprite', 'tile', 'effect'],
141
+ narrative: ['story', 'narrative', 'dialogue', 'character', 'quest', 'lore', 'writing', 'plot', 'world-building', 'fiction'],
142
+ audio: ['audio', 'sound', 'music', 'synthesizer', 'frequency', 'waveform', 'oscillator', 'reverb', 'filter', 'beat'],
143
+ social: ['community', 'engagement', 'viewer', 'follower', 'chat', 'moderation', 'streaming', 'audience'],
144
+ brain: ['ai', 'machine learning', 'neural', 'llm', 'model', 'training', 'inference', 'optimization', 'algorithm'],
145
+ tile: ['tile', 'map', 'biome', 'terrain', 'procedural generation', 'world', 'dungeon', 'level design'],
146
+ security: ['security', 'vulnerability', 'exploit', 'cve', 'owasp', 'penetration', 'hardening', 'encryption'],
147
+ research: ['paper', 'arxiv', 'study', 'methodology', 'experiment', 'hypothesis', 'data', 'analysis'],
148
+ };
149
+ function detectApplicableEngines(query) {
150
+ const lower = query.toLowerCase();
151
+ const applicable = [];
152
+ for (const [engine, keywords] of Object.entries(ENGINE_KEYWORDS)) {
153
+ for (const kw of keywords) {
154
+ if (lower.includes(kw)) {
155
+ applicable.push(engine);
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ // Always include 'general' as a catch-all
161
+ if (applicable.length === 0)
162
+ applicable.push('general');
163
+ return applicable;
164
+ }
165
+ // ─── Summarize with Ollama ──────────────────────────────────────
166
+ async function summarizeWithOllama(query, content) {
167
+ const available = await isOllamaAvailable();
168
+ if (!available) {
169
+ // Fallback: extract first ~500 chars as basic summary
170
+ return content.slice(0, 500).trim() + (content.length > 500 ? '...' : '');
171
+ }
172
+ const truncatedContent = content.slice(0, 8000); // keep prompt under token limit
173
+ const prompt = [
174
+ 'You are a research assistant. Summarize the following web content in relation to the research query.',
175
+ 'Extract key findings as bullet points. Be concise and factual.',
176
+ '',
177
+ `Research query: "${query}"`,
178
+ '',
179
+ 'Web content:',
180
+ truncatedContent,
181
+ '',
182
+ 'Provide:',
183
+ '1. A 2-3 sentence summary',
184
+ '2. Key findings as bullet points (prefix with -)',
185
+ '3. Any actionable techniques or insights',
186
+ ].join('\n');
187
+ const result = await ollamaGenerate(prompt);
188
+ return result || content.slice(0, 500).trim() + (content.length > 500 ? '...' : '');
189
+ }
190
+ // ─── Autonomous Topic Generation ────────────────────────────────
191
+ const AUTONOMOUS_TOPIC_TEMPLATES = [
192
+ 'Canvas 2D {technique} optimization',
193
+ '{biome} pixel art reference techniques',
194
+ 'ROM hack {technique} implementation',
195
+ 'procedural {feature} generation algorithms',
196
+ 'indie game {effect} techniques 2026',
197
+ 'retro game {style} rendering methods',
198
+ 'lo-fi aesthetic {element} techniques',
199
+ 'web audio API {feature} tutorial',
200
+ 'terminal UI {technique} best practices',
201
+ 'AI agent {capability} implementation',
202
+ 'real-time {effect} in JavaScript',
203
+ 'pixel art {style} color palettes',
204
+ ];
205
+ const TOPIC_FILLS = {
206
+ technique: ['parallax scrolling', 'palette cycling', 'dithering', 'scanline effects', 'sprite animation', 'tile blending', 'water reflection', 'particle systems'],
207
+ biome: ['forest', 'ocean', 'desert', 'mountain', 'cave', 'tundra', 'swamp', 'volcanic'],
208
+ feature: ['terrain', 'vegetation', 'weather', 'rivers', 'clouds', 'fire', 'fog', 'snow'],
209
+ effect: ['lighting', 'shadow', 'bloom', 'chromatic aberration', 'CRT filter', 'vignette', 'glow', 'distortion'],
210
+ style: ['SNES', 'GBA', 'NES', 'Amiga', 'PC-98', 'C64', 'MSX', 'Genesis'],
211
+ element: ['grain', 'noise', 'halftone', 'dot matrix', 'glitch', 'static', 'tape hiss'],
212
+ capability: ['tool use', 'memory', 'planning', 'self-evaluation', 'context management', 'learning'],
213
+ };
214
+ export function autonomousResearchTopics() {
215
+ const topics = [];
216
+ const now = Date.now();
217
+ for (const template of AUTONOMOUS_TOPIC_TEMPLATES) {
218
+ let topic = template;
219
+ const placeholders = template.match(/\{(\w+)\}/g) || [];
220
+ for (const placeholder of placeholders) {
221
+ const key = placeholder.replace(/[{}]/g, '');
222
+ const options = TOPIC_FILLS[key];
223
+ if (options) {
224
+ // Deterministic-ish pick based on time + template hash
225
+ const hash = template.length + key.length + (now % 10000);
226
+ const pick = options[hash % options.length];
227
+ topic = topic.replace(placeholder, pick);
228
+ }
229
+ }
230
+ topics.push(topic);
231
+ }
232
+ return topics;
233
+ }
234
+ // ─── Core Engine Functions ──────────────────────────────────────
235
+ export function initResearchEngine() {
236
+ const completed = loadResults();
237
+ const engine = {
238
+ queue: [],
239
+ completed,
240
+ activeTask: null,
241
+ lastResearchFrame: 0,
242
+ researchInterval: DEFAULT_RESEARCH_INTERVAL,
243
+ topicsOfInterest: autonomousResearchTopics().slice(0, 5),
244
+ };
245
+ engineState = engine;
246
+ return engine;
247
+ }
248
+ export function getResearchEngine() {
249
+ if (!engineState)
250
+ return initResearchEngine();
251
+ return engineState;
252
+ }
253
+ export function queueResearch(engine, query, purpose, source) {
254
+ const task = {
255
+ id: randomUUID().slice(0, 8),
256
+ query,
257
+ purpose,
258
+ source,
259
+ status: 'queued',
260
+ startedAt: 0,
261
+ };
262
+ engine.queue.push(task);
263
+ return task;
264
+ }
265
+ export function getResearchForEngine(engine, engineName) {
266
+ return engine.completed.filter(r => r.applicableTo.includes(engineName));
267
+ }
268
+ // ─── Research Execution ─────────────────────────────────────────
269
+ async function executeResearch(task) {
270
+ const { ensureLazyToolsLoaded, executeTool } = await import('./index.js');
271
+ await ensureLazyToolsLoaded();
272
+ // Search
273
+ task.status = 'searching';
274
+ task.startedAt = Date.now();
275
+ const searchResult = await executeTool({
276
+ id: `research_search_${task.id}`,
277
+ name: 'kbot_search',
278
+ arguments: { query: task.query },
279
+ });
280
+ // Extract links from search results
281
+ const links = extractLinks(searchResult.result);
282
+ // Read top results
283
+ task.status = 'reading';
284
+ let content = searchResult.result; // use search results as baseline
285
+ const readContents = [];
286
+ for (const url of links.slice(0, MAX_LINKS_TO_READ)) {
287
+ try {
288
+ const readResult = await executeTool({
289
+ id: `research_read_${task.id}_${readContents.length}`,
290
+ name: 'kbot_browse',
291
+ arguments: { url },
292
+ });
293
+ if (!readResult.error) {
294
+ readContents.push(readResult.result);
295
+ }
296
+ }
297
+ catch {
298
+ // Skip failed reads — non-fatal
299
+ }
300
+ }
301
+ if (readContents.length > 0) {
302
+ // Combine: search overview + page content (trimmed)
303
+ content = [
304
+ '## Search Results Overview',
305
+ searchResult.result.slice(0, 2000),
306
+ '',
307
+ ...readContents.map((c, i) => [
308
+ `## Source ${i + 1}`,
309
+ c.slice(0, 4000),
310
+ ].join('\n')),
311
+ ].join('\n');
312
+ }
313
+ // Summarize using local Ollama
314
+ task.status = 'summarizing';
315
+ const summary = await summarizeWithOllama(task.query, content);
316
+ task.status = 'complete';
317
+ return {
318
+ taskId: task.id,
319
+ query: task.query,
320
+ summary,
321
+ sources: links.slice(0, 5),
322
+ keyFindings: extractKeyFindings(summary),
323
+ applicableTo: detectApplicableEngines(task.query),
324
+ timestamp: Date.now(),
325
+ };
326
+ }
327
+ // ─── Tick (Frame Loop Integration) ──────────────────────────────
328
+ export async function tickResearch(engine, frame) {
329
+ // Not time yet
330
+ if (frame - engine.lastResearchFrame < engine.researchInterval)
331
+ return null;
332
+ // Already working on something
333
+ if (engine.activeTask)
334
+ return null;
335
+ // Auto-queue autonomous topics if queue is empty
336
+ if (engine.queue.length === 0) {
337
+ const topics = autonomousResearchTopics();
338
+ if (topics.length > 0) {
339
+ // Pick one topic we haven't researched recently
340
+ const recentQueries = new Set(engine.completed.slice(-20).map(r => r.query));
341
+ const fresh = topics.find(t => !recentQueries.has(t));
342
+ if (fresh) {
343
+ queueResearch(engine, fresh, 'Autonomous discovery of new techniques', 'autonomous');
344
+ }
345
+ }
346
+ }
347
+ // Nothing to do
348
+ if (engine.queue.length === 0)
349
+ return null;
350
+ // Pick next task
351
+ const task = engine.queue.shift();
352
+ engine.activeTask = task;
353
+ engine.lastResearchFrame = frame;
354
+ try {
355
+ const result = await executeResearch(task);
356
+ // Store result
357
+ engine.completed.push(result);
358
+ if (engine.completed.length > MAX_COMPLETED) {
359
+ engine.completed = engine.completed.slice(-MAX_COMPLETED);
360
+ }
361
+ saveResults(engine.completed);
362
+ engine.activeTask = null;
363
+ return {
364
+ type: 'summarize_complete',
365
+ task,
366
+ result,
367
+ speech: `Research complete: "${task.query}" — found ${result.keyFindings.length} key findings.`,
368
+ };
369
+ }
370
+ catch (err) {
371
+ task.status = 'failed';
372
+ engine.activeTask = null;
373
+ return {
374
+ type: 'failed',
375
+ task,
376
+ speech: `Research failed for "${task.query}": ${err instanceof Error ? err.message : String(err)}`,
377
+ };
378
+ }
379
+ }
380
+ // ─── Tool Registration ──────────────────────────────────────────
381
+ export function registerResearchEngineTools() {
382
+ // ── research_queue ──────────────────────────────────────────
383
+ registerTool({
384
+ name: 'research_queue',
385
+ description: 'Queue a web research task for the Research Engine. The engine will search the web, read relevant pages, and summarize findings using local Ollama. Results are persisted to ~/.kbot/research-results.json.',
386
+ parameters: {
387
+ query: { type: 'string', description: 'Search query / research topic', required: true },
388
+ purpose: { type: 'string', description: 'Why this research is needed (guides summarization)' },
389
+ source: { type: 'string', description: 'Source of the request: evolution | brain | narrative | user | autonomous (default: user)' },
390
+ },
391
+ tier: 'free',
392
+ async execute(args) {
393
+ const query = String(args.query);
394
+ const purpose = String(args.purpose || 'User-requested research');
395
+ const source = (String(args.source || 'user'));
396
+ const validSources = ['evolution', 'brain', 'narrative', 'user', 'autonomous'];
397
+ const safeSource = validSources.includes(source) ? source : 'user';
398
+ const engine = getResearchEngine();
399
+ const task = queueResearch(engine, query, purpose, safeSource);
400
+ const queuePos = engine.queue.length;
401
+ const ollamaUp = await isOllamaAvailable();
402
+ return [
403
+ `# Research Queued`,
404
+ ``,
405
+ `**Task ID**: ${task.id}`,
406
+ `**Query**: ${task.query}`,
407
+ `**Purpose**: ${purpose}`,
408
+ `**Source**: ${safeSource}`,
409
+ `**Queue position**: ${queuePos}`,
410
+ `**Ollama**: ${ollamaUp ? 'available (will summarize with ' + OLLAMA_MODEL + ')' : 'unavailable (will use raw extraction)'}`,
411
+ ``,
412
+ engine.activeTask
413
+ ? `Currently researching: "${engine.activeTask.query}" (${engine.activeTask.status})`
414
+ : 'No active research — will start on next tick.',
415
+ ``,
416
+ `Completed research: ${engine.completed.length} results on file.`,
417
+ ].join('\n');
418
+ },
419
+ });
420
+ // ── research_results ────────────────────────────────────────
421
+ registerTool({
422
+ name: 'research_results',
423
+ description: 'View completed research results. Filter by engine, query, or show all. Results are persisted across sessions in ~/.kbot/research-results.json.',
424
+ parameters: {
425
+ engine: { type: 'string', description: 'Filter results applicable to a specific engine (evolution, narrative, audio, social, brain, tile, security, research, general)' },
426
+ query: { type: 'string', description: 'Filter results by query substring' },
427
+ limit: { type: 'number', description: 'Max results to return (default 10)' },
428
+ },
429
+ tier: 'free',
430
+ async execute(args) {
431
+ const engineFilter = args.engine ? String(args.engine).toLowerCase() : undefined;
432
+ const queryFilter = args.query ? String(args.query).toLowerCase() : undefined;
433
+ const limit = Number(args.limit) || 10;
434
+ const engine = getResearchEngine();
435
+ let results = engine.completed;
436
+ if (engineFilter) {
437
+ results = results.filter(r => r.applicableTo.includes(engineFilter));
438
+ }
439
+ if (queryFilter) {
440
+ results = results.filter(r => r.query.toLowerCase().includes(queryFilter));
441
+ }
442
+ results = results.slice(-limit);
443
+ if (results.length === 0) {
444
+ return [
445
+ '# Research Results',
446
+ '',
447
+ 'No research results found.',
448
+ engineFilter ? `Filter: engine=${engineFilter}` : '',
449
+ queryFilter ? `Filter: query contains "${queryFilter}"` : '',
450
+ '',
451
+ `Total results on file: ${engine.completed.length}`,
452
+ `Queue: ${engine.queue.length} tasks pending`,
453
+ engine.activeTask ? `Active: "${engine.activeTask.query}" (${engine.activeTask.status})` : 'No active research.',
454
+ ].filter(Boolean).join('\n');
455
+ }
456
+ const lines = ['# Research Results', ''];
457
+ for (const r of results) {
458
+ lines.push(`## ${r.query}`);
459
+ lines.push(`*${new Date(r.timestamp).toISOString().slice(0, 16)}* | Sources: ${r.sources.length} | Engines: ${r.applicableTo.join(', ')}`);
460
+ lines.push('');
461
+ lines.push(r.summary.slice(0, 800));
462
+ if (r.keyFindings.length > 0) {
463
+ lines.push('');
464
+ lines.push('**Key findings:**');
465
+ for (const f of r.keyFindings.slice(0, 5)) {
466
+ lines.push(`- ${f}`);
467
+ }
468
+ }
469
+ if (r.sources.length > 0) {
470
+ lines.push('');
471
+ lines.push('**Sources:**');
472
+ for (const s of r.sources.slice(0, 3)) {
473
+ lines.push(`- ${s}`);
474
+ }
475
+ }
476
+ lines.push('');
477
+ lines.push('---');
478
+ lines.push('');
479
+ }
480
+ lines.push(`Showing ${results.length} of ${engine.completed.length} total results.`);
481
+ lines.push(`Queue: ${engine.queue.length} pending | Active: ${engine.activeTask ? `"${engine.activeTask.query}"` : 'none'}`);
482
+ return lines.join('\n');
483
+ },
484
+ });
485
+ // ── research_now ────────────────────────────────────────────
486
+ registerTool({
487
+ name: 'research_now',
488
+ description: 'Execute a research task immediately (bypasses the queue). Searches the web, reads top results, summarizes with Ollama, and returns findings. Use for urgent research needs.',
489
+ parameters: {
490
+ query: { type: 'string', description: 'Search query / research topic', required: true },
491
+ purpose: { type: 'string', description: 'Why this research is needed' },
492
+ },
493
+ tier: 'free',
494
+ timeout: 120_000, // 2 min — research involves multiple web requests + Ollama
495
+ async execute(args) {
496
+ const query = String(args.query);
497
+ const purpose = String(args.purpose || 'Immediate research request');
498
+ const engine = getResearchEngine();
499
+ const task = {
500
+ id: randomUUID().slice(0, 8),
501
+ query,
502
+ purpose,
503
+ source: 'user',
504
+ status: 'queued',
505
+ startedAt: 0,
506
+ };
507
+ try {
508
+ const result = await executeResearch(task);
509
+ // Store result
510
+ engine.completed.push(result);
511
+ if (engine.completed.length > MAX_COMPLETED) {
512
+ engine.completed = engine.completed.slice(-MAX_COMPLETED);
513
+ }
514
+ saveResults(engine.completed);
515
+ const lines = [
516
+ '# Research Complete',
517
+ '',
518
+ `**Query**: ${result.query}`,
519
+ `**Sources found**: ${result.sources.length}`,
520
+ `**Applicable engines**: ${result.applicableTo.join(', ')}`,
521
+ '',
522
+ '## Summary',
523
+ '',
524
+ result.summary,
525
+ '',
526
+ ];
527
+ if (result.keyFindings.length > 0) {
528
+ lines.push('## Key Findings', '');
529
+ for (const f of result.keyFindings) {
530
+ lines.push(`- ${f}`);
531
+ }
532
+ lines.push('');
533
+ }
534
+ if (result.sources.length > 0) {
535
+ lines.push('## Sources', '');
536
+ for (const s of result.sources) {
537
+ lines.push(`- ${s}`);
538
+ }
539
+ lines.push('');
540
+ }
541
+ lines.push(`*Task ${task.id} completed in ${((Date.now() - task.startedAt) / 1000).toFixed(1)}s*`);
542
+ return lines.join('\n');
543
+ }
544
+ catch (err) {
545
+ return `Research failed: ${err instanceof Error ? err.message : String(err)}`;
546
+ }
547
+ },
548
+ });
549
+ }
550
+ //# sourceMappingURL=research-engine.js.map