@kernel.chat/kbot 3.93.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.
- package/dist/tools/coordination-engine.d.ts +127 -0
- package/dist/tools/coordination-engine.js +543 -0
- package/dist/tools/foundation-engines.d.ts +111 -0
- package/dist/tools/foundation-engines.js +520 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/research-engine.d.ts +58 -0
- package/dist/tools/research-engine.js +550 -0
- package/dist/tools/stream-renderer.js +289 -144
- package/package.json +1 -1
|
@@ -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
|