@kernel.chat/kbot 3.44.0 → 3.46.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.
@@ -0,0 +1,1790 @@
1
+ // kbot Emergent Intelligence Layer — Cross-system introspection and synthesis
2
+ // The system that lets all of kbot's subsystems talk to each other and produce
3
+ // insights none of them could produce alone. These tools look inward, not outward.
4
+ // They are contemplative, not analytical — observations, not reports.
5
+ //
6
+ // 10 tools: anticipate, synthesize_across, judge, teach, dream, question,
7
+ // connect_minds, reflect, consolidate, emerge
8
+ //
9
+ // All self-contained — reads from ~/.kbot/ persistent state.
10
+ import { registerTool, executeTool, getAllTools } from './index.js';
11
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ // ── Helpers ─────────────────────────────────────────────────────────────────
15
+ const KBOT_DIR = join(homedir(), '.kbot');
16
+ const MEMORY_DIR = join(KBOT_DIR, 'memory');
17
+ const NOTEBOOKS_DIR = join(KBOT_DIR, 'research-notebooks');
18
+ /** Generate a unique tool call ID */
19
+ let callSeq = 0;
20
+ function callId() {
21
+ return `em_${Date.now()}_${++callSeq}`;
22
+ }
23
+ /** Execute a registered tool by name. Returns the result string. */
24
+ async function runTool(name, args) {
25
+ const call = { id: callId(), name, arguments: args };
26
+ const result = await executeTool(call);
27
+ return result.result;
28
+ }
29
+ /** Safe JSON file read — returns null on missing/corrupt files */
30
+ function safeReadJson(path) {
31
+ try {
32
+ if (!existsSync(path))
33
+ return null;
34
+ return JSON.parse(readFileSync(path, 'utf-8'));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ /** Read all JSON files in a directory. Returns array of parsed objects. */
41
+ function readAllJsonInDir(dir) {
42
+ if (!existsSync(dir))
43
+ return [];
44
+ try {
45
+ return readdirSync(dir)
46
+ .filter(f => f.endsWith('.json'))
47
+ .map(f => {
48
+ try {
49
+ return JSON.parse(readFileSync(join(dir, f), 'utf-8'));
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ })
55
+ .filter(Boolean);
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ /** Extract keywords from text (lowercase, deduplicated, stop words removed) */
62
+ function extractKeywords(text) {
63
+ const stops = new Set([
64
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
65
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
66
+ 'should', 'may', 'might', 'shall', 'can', 'need', 'dare', 'ought',
67
+ 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from',
68
+ 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below',
69
+ 'between', 'out', 'off', 'over', 'under', 'again', 'further', 'then',
70
+ 'once', 'and', 'but', 'or', 'nor', 'not', 'so', 'very', 'just', 'that',
71
+ 'this', 'these', 'those', 'it', 'its', 'he', 'she', 'they', 'them',
72
+ 'we', 'you', 'i', 'me', 'my', 'your', 'his', 'her', 'our', 'their',
73
+ 'what', 'which', 'who', 'when', 'where', 'why', 'how', 'all', 'each',
74
+ 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'no',
75
+ 'than', 'too', 'only', 'own', 'same', 'also', 'about', 'up',
76
+ ]);
77
+ const words = text.toLowerCase().replace(/[^a-z0-9\s-]/g, ' ').split(/\s+/).filter(w => w.length > 2 && !stops.has(w));
78
+ return [...new Set(words)];
79
+ }
80
+ /** Find overlapping keywords between two texts */
81
+ function keywordOverlap(a, b) {
82
+ const kA = new Set(extractKeywords(a));
83
+ return extractKeywords(b).filter(w => kA.has(w));
84
+ }
85
+ /** Simple relevance score between a query and text (0-1) */
86
+ function relevanceScore(query, text) {
87
+ const qKeywords = extractKeywords(query);
88
+ if (qKeywords.length === 0)
89
+ return 0;
90
+ const tKeywords = new Set(extractKeywords(text));
91
+ const matches = qKeywords.filter(k => tKeywords.has(k)).length;
92
+ return matches / qKeywords.length;
93
+ }
94
+ /** Format a timestamp into something human-readable */
95
+ function timeAgo(iso) {
96
+ try {
97
+ const diff = Date.now() - new Date(iso).getTime();
98
+ const mins = Math.floor(diff / 60000);
99
+ if (mins < 60)
100
+ return `${mins}m ago`;
101
+ const hours = Math.floor(mins / 60);
102
+ if (hours < 24)
103
+ return `${hours}h ago`;
104
+ const days = Math.floor(hours / 24);
105
+ return `${days}d ago`;
106
+ }
107
+ catch {
108
+ return 'unknown time';
109
+ }
110
+ }
111
+ /** Gather all memory file paths and their categories */
112
+ function gatherMemoryState() {
113
+ return {
114
+ patterns: safeReadJson(join(MEMORY_DIR, 'patterns.json')),
115
+ solutions: safeReadJson(join(MEMORY_DIR, 'solutions.json')),
116
+ knowledge: safeReadJson(join(MEMORY_DIR, 'knowledge.json')),
117
+ routingHistory: safeReadJson(join(MEMORY_DIR, 'routing-history.json')),
118
+ profile: safeReadJson(join(MEMORY_DIR, 'profile.json')),
119
+ identity: safeReadJson(join(KBOT_DIR, 'identity.json')),
120
+ skillRatings: safeReadJson(join(MEMORY_DIR, 'skill-ratings.json')),
121
+ synthesis: safeReadJson(join(MEMORY_DIR, 'synthesis.json')),
122
+ scienceGraph: safeReadJson(join(KBOT_DIR, 'science-graph.json')),
123
+ notebooks: readAllJsonInDir(NOTEBOOKS_DIR),
124
+ memoryEntries: {
125
+ fact: readAllJsonInDir(join(MEMORY_DIR, 'fact')),
126
+ preference: readAllJsonInDir(join(MEMORY_DIR, 'preference')),
127
+ pattern: readAllJsonInDir(join(MEMORY_DIR, 'pattern')),
128
+ solution: readAllJsonInDir(join(MEMORY_DIR, 'solution')),
129
+ },
130
+ };
131
+ }
132
+ /** Count non-null state sources */
133
+ function countSources(state) {
134
+ let count = 0;
135
+ if (state.patterns)
136
+ count++;
137
+ if (state.solutions)
138
+ count++;
139
+ if (state.knowledge)
140
+ count++;
141
+ if (state.routingHistory)
142
+ count++;
143
+ if (state.profile)
144
+ count++;
145
+ if (state.identity)
146
+ count++;
147
+ if (state.skillRatings)
148
+ count++;
149
+ if (state.synthesis)
150
+ count++;
151
+ if (state.scienceGraph)
152
+ count++;
153
+ if (state.notebooks.length > 0)
154
+ count++;
155
+ for (const entries of Object.values(state.memoryEntries)) {
156
+ if (entries.length > 0)
157
+ count++;
158
+ }
159
+ return count;
160
+ }
161
+ /** Extract all text content from a state object for keyword analysis */
162
+ function stateToText(obj) {
163
+ if (!obj)
164
+ return '';
165
+ if (typeof obj === 'string')
166
+ return obj;
167
+ if (typeof obj === 'number' || typeof obj === 'boolean')
168
+ return String(obj);
169
+ if (Array.isArray(obj))
170
+ return obj.map(stateToText).join(' ');
171
+ if (typeof obj === 'object')
172
+ return Object.values(obj).map(stateToText).join(' ');
173
+ return '';
174
+ }
175
+ const DOMAINS = [
176
+ { id: 'biology', name: 'Biology', keywords: ['gene', 'protein', 'cell', 'dna', 'rna', 'organism', 'evolution', 'species', 'genome', 'mutation', 'enzyme', 'pathway', 'receptor', 'antibody', 'virus', 'bacteria', 'tissue', 'organ'], tools: ['pubmed_search', 'gene_lookup', 'protein_lookup', 'sequence_align', 'dna_translate', 'blast_search', 'pathway_search', 'species_lookup', 'clinical_trials'] },
177
+ { id: 'chemistry', name: 'Chemistry', keywords: ['molecule', 'compound', 'reaction', 'element', 'bond', 'orbital', 'synthesis', 'catalyst', 'polymer', 'solvent', 'acid', 'base', 'ion', 'crystal', 'organic', 'inorganic'], tools: ['compound_search', 'reaction_balance', 'molecular_weight', 'element_lookup', 'iupac_name', 'solubility_predict', 'reaction_predict'] },
178
+ { id: 'physics', name: 'Physics', keywords: ['force', 'energy', 'mass', 'velocity', 'acceleration', 'quantum', 'relativity', 'wave', 'particle', 'field', 'gravity', 'electromagnetism', 'thermodynamic', 'entropy', 'photon', 'electron'], tools: ['unit_convert', 'physics_calc', 'spectrum_analyze', 'orbital_mechanics', 'quantum_state', 'wave_interference'] },
179
+ { id: 'earth', name: 'Earth Science', keywords: ['earthquake', 'climate', 'ocean', 'atmosphere', 'geology', 'mineral', 'volcano', 'weather', 'glacier', 'tectonic', 'sediment', 'fossil', 'erosion', 'groundwater'], tools: ['earthquake_query', 'mineral_identify', 'climate_data', 'weather_station', 'geological_map'] },
180
+ { id: 'neuroscience', name: 'Neuroscience', keywords: ['brain', 'neuron', 'synapse', 'cortex', 'neurotransmitter', 'cognition', 'memory', 'perception', 'consciousness', 'eeg', 'fmri', 'dopamine', 'serotonin', 'hippocampus', 'amygdala'], tools: ['brain_atlas', 'brain_predict', 'neurotransmitter_lookup', 'eeg_simulate', 'cognitive_model', 'psychophysics'] },
181
+ { id: 'social', name: 'Social Sciences', keywords: ['society', 'culture', 'economy', 'politics', 'psychology', 'behavior', 'population', 'inequality', 'market', 'game theory', 'voting', 'survey', 'demographics', 'sentiment'], tools: ['psychometric_scale', 'effect_size', 'social_network', 'game_theory', 'gini_coefficient', 'survey_design', 'voting_system', 'discourse_analyze'] },
182
+ { id: 'humanities', name: 'Humanities', keywords: ['language', 'text', 'literature', 'history', 'philosophy', 'art', 'music', 'rhetoric', 'narrative', 'etymology', 'corpus', 'semiotics', 'hermeneutics'], tools: ['corpus_analyze', 'etymology_trace', 'rhetoric_analyze', 'narrative_structure', 'text_complexity', 'sentiment_literary'] },
183
+ { id: 'health', name: 'Health Sciences', keywords: ['disease', 'treatment', 'diagnosis', 'patient', 'drug', 'therapy', 'symptom', 'epidemic', 'vaccine', 'clinical', 'mortality', 'morbidity', 'public health', 'pharmacology'], tools: ['sir_model', 'drug_interaction', 'clinical_trials', 'diagnostic_predict', 'pharmacokinetic', 'epidemiology'] },
184
+ { id: 'math', name: 'Mathematics', keywords: ['equation', 'proof', 'theorem', 'function', 'matrix', 'integral', 'derivative', 'probability', 'statistics', 'topology', 'algebra', 'geometry', 'combinatorics', 'optimization'], tools: ['symbolic_calc', 'matrix_op', 'stat_test', 'regression', 'optimize', 'graph_theory', 'number_theory'] },
185
+ { id: 'data', name: 'Data Science', keywords: ['dataset', 'model', 'training', 'prediction', 'classification', 'clustering', 'regression', 'feature', 'neural network', 'machine learning', 'deep learning', 'visualization'], tools: ['csv_analyze', 'stat_describe', 'correlation_matrix', 'pca_analyze', 'cluster_analyze', 'forecast'] },
186
+ ];
187
+ /** Score a topic against each domain, return sorted results */
188
+ function scoreDomains(topic) {
189
+ const topicLower = topic.toLowerCase();
190
+ const topicKeywords = extractKeywords(topic);
191
+ return DOMAINS.map(domain => {
192
+ let score = 0;
193
+ // Direct keyword matches
194
+ for (const kw of domain.keywords) {
195
+ if (topicLower.includes(kw))
196
+ score += 0.15;
197
+ if (topicKeywords.includes(kw))
198
+ score += 0.1;
199
+ }
200
+ // Partial keyword matches
201
+ for (const tk of topicKeywords) {
202
+ for (const dk of domain.keywords) {
203
+ if (tk.includes(dk) || dk.includes(tk))
204
+ score += 0.05;
205
+ }
206
+ }
207
+ return { domain, score: Math.min(score, 1) };
208
+ }).sort((a, b) => b.score - a.score);
209
+ }
210
+ // ── Registration ────────────────────────────────────────────────────────────
211
+ export function registerEmergentTools() {
212
+ // ══════════════════════════════════════════════════════════════════════════
213
+ // 1. anticipate — Proactive insight generation
214
+ // ══════════════════════════════════════════════════════════════════════════
215
+ registerTool({
216
+ name: 'anticipate',
217
+ description: 'Analyze kbot\'s memory to generate proactive insights. Finds clusters of related queries, gaps in research, unexplored connections, and suggested next steps — before the user asks.',
218
+ parameters: {
219
+ depth: { type: 'string', description: 'Analysis depth: "quick" for surface-level, "deep" for thorough cross-referencing (default: deep)' },
220
+ },
221
+ tier: 'free',
222
+ async execute(args) {
223
+ const depth = String(args.depth || 'deep');
224
+ const state = gatherMemoryState();
225
+ const sourcesFound = countSources(state);
226
+ if (sourcesFound === 0) {
227
+ return `## Anticipation\n\nkbot's memory is empty — there is nothing yet to anticipate from. Use kbot for a while, and patterns will emerge. Every conversation leaves traces; anticipation needs those traces to work with.`;
228
+ }
229
+ const lines = ['## Anticipatory Insights'];
230
+ lines.push('');
231
+ lines.push(`*Drawing from ${sourcesFound} memory sources...*`);
232
+ lines.push('');
233
+ // ── Cluster analysis on patterns ──
234
+ const patterns = state.patterns;
235
+ if (patterns && typeof patterns === 'object') {
236
+ const patternText = stateToText(patterns);
237
+ const keywords = extractKeywords(patternText);
238
+ const freqMap = new Map();
239
+ for (const kw of keywords) {
240
+ freqMap.set(kw, (freqMap.get(kw) || 0) + 1);
241
+ }
242
+ const topClusters = [...freqMap.entries()]
243
+ .sort((a, b) => b[1] - a[1])
244
+ .slice(0, depth === 'deep' ? 10 : 5);
245
+ if (topClusters.length > 0) {
246
+ lines.push('### Recurring Themes');
247
+ lines.push('');
248
+ lines.push('These concepts appear repeatedly across your activity — they form the gravitational centers of your thinking:');
249
+ lines.push('');
250
+ for (const [word, count] of topClusters) {
251
+ lines.push(`- **${word}** — appeared ${count} times across patterns`);
252
+ }
253
+ lines.push('');
254
+ }
255
+ }
256
+ // ── Solution analysis — what problems keep coming back ──
257
+ const solutions = state.solutions;
258
+ if (solutions && typeof solutions === 'object') {
259
+ const solEntries = Array.isArray(solutions) ? solutions : Object.values(solutions);
260
+ if (solEntries.length > 0) {
261
+ lines.push('### Solved Patterns');
262
+ lines.push('');
263
+ lines.push(`You have ${solEntries.length} recorded solution${solEntries.length === 1 ? '' : 's'}. `);
264
+ if (depth === 'deep') {
265
+ const solText = stateToText(solutions);
266
+ const solKeywords = extractKeywords(solText);
267
+ const patternKeywords = state.patterns ? extractKeywords(stateToText(state.patterns)) : [];
268
+ const unsolved = patternKeywords.filter(pk => !solKeywords.includes(pk)).slice(0, 5);
269
+ if (unsolved.length > 0) {
270
+ lines.push('Themes that appear in patterns but lack corresponding solutions:');
271
+ lines.push('');
272
+ for (const u of unsolved) {
273
+ lines.push(`- **${u}** — recurring in patterns, no solution recorded`);
274
+ }
275
+ lines.push('');
276
+ }
277
+ }
278
+ }
279
+ }
280
+ // ── Knowledge graph gaps ──
281
+ const graph = state.scienceGraph;
282
+ if (graph && typeof graph === 'object') {
283
+ const entities = (graph.entities || {});
284
+ const relations = (graph.relations || []);
285
+ const entityCount = Object.keys(entities).length;
286
+ const relationCount = relations.length;
287
+ lines.push('### Knowledge Graph State');
288
+ lines.push('');
289
+ lines.push(`${entityCount} entities, ${relationCount} relations.`);
290
+ if (entityCount > 0 && depth === 'deep') {
291
+ // Find isolated entities (no relations)
292
+ const relatedIds = new Set();
293
+ for (const rel of relations) {
294
+ const r = rel;
295
+ if (r.from)
296
+ relatedIds.add(r.from);
297
+ if (r.to)
298
+ relatedIds.add(r.to);
299
+ }
300
+ const isolated = Object.keys(entities).filter(id => !relatedIds.has(id));
301
+ if (isolated.length > 0) {
302
+ lines.push('');
303
+ lines.push(`${isolated.length} entities have no connections — they are islands waiting to be bridged:`);
304
+ lines.push('');
305
+ for (const id of isolated.slice(0, 5)) {
306
+ const ent = entities[id];
307
+ lines.push(`- **${ent.name || id}** (${ent.type || 'unknown'})`);
308
+ }
309
+ }
310
+ // Density analysis
311
+ const maxRelations = entityCount * (entityCount - 1);
312
+ const density = maxRelations > 0 ? (relationCount / maxRelations) : 0;
313
+ if (density < 0.1) {
314
+ lines.push('');
315
+ lines.push(`Graph density is ${(density * 100).toFixed(1)}% — there are likely many undiscovered connections between existing entities.`);
316
+ }
317
+ }
318
+ lines.push('');
319
+ }
320
+ // ── Routing history analysis — what agents does the user lean on ──
321
+ const routing = state.routingHistory;
322
+ if (routing && typeof routing === 'object') {
323
+ const entries = Array.isArray(routing) ? routing : Object.values(routing);
324
+ if (entries.length > 0) {
325
+ const agentCounts = new Map();
326
+ for (const entry of entries) {
327
+ const e = entry;
328
+ const agent = String(e.agent || e.specialist || 'unknown');
329
+ agentCounts.set(agent, (agentCounts.get(agent) || 0) + 1);
330
+ }
331
+ const sorted = [...agentCounts.entries()].sort((a, b) => b[1] - a[1]);
332
+ lines.push('### Agent Usage Patterns');
333
+ lines.push('');
334
+ lines.push('Where you spend your cognitive time:');
335
+ lines.push('');
336
+ for (const [agent, count] of sorted.slice(0, 5)) {
337
+ lines.push(`- **${agent}** — ${count} routing${count === 1 ? '' : 's'}`);
338
+ }
339
+ // Suggest underused agents
340
+ const used = new Set(agentCounts.keys());
341
+ const allAgents = ['researcher', 'coder', 'writer', 'analyst', 'aesthete', 'guardian', 'curator', 'strategist', 'infrastructure', 'quant', 'investigator', 'oracle', 'chronist', 'sage', 'communicator', 'adapter'];
342
+ const unused = allAgents.filter(a => !used.has(a));
343
+ if (unused.length > 0 && unused.length < allAgents.length) {
344
+ lines.push('');
345
+ lines.push(`Specialists you have not engaged: ${unused.join(', ')}. Each holds a different lens on the same material.`);
346
+ }
347
+ lines.push('');
348
+ }
349
+ }
350
+ // ── Anticipatory suggestions ──
351
+ lines.push('### What to Look At Next');
352
+ lines.push('');
353
+ const allText = stateToText(state);
354
+ const allKeywords = extractKeywords(allText);
355
+ const freqAll = new Map();
356
+ for (const kw of allKeywords) {
357
+ freqAll.set(kw, (freqAll.get(kw) || 0) + 1);
358
+ }
359
+ // Find keywords that appear in multiple systems
360
+ const crossSystem = [];
361
+ for (const [kw] of freqAll) {
362
+ let systems = 0;
363
+ if (stateToText(state.patterns).toLowerCase().includes(kw))
364
+ systems++;
365
+ if (stateToText(state.solutions).toLowerCase().includes(kw))
366
+ systems++;
367
+ if (stateToText(state.knowledge).toLowerCase().includes(kw))
368
+ systems++;
369
+ if (stateToText(state.scienceGraph).toLowerCase().includes(kw))
370
+ systems++;
371
+ if (stateToText(state.notebooks).toLowerCase().includes(kw))
372
+ systems++;
373
+ if (systems >= 3)
374
+ crossSystem.push(kw);
375
+ }
376
+ if (crossSystem.length > 0) {
377
+ lines.push('Concepts that span multiple memory systems — these are your deepest threads:');
378
+ lines.push('');
379
+ for (const cs of crossSystem.slice(0, 5)) {
380
+ lines.push(`- **${cs}** — appears across ${3}+ memory subsystems`);
381
+ }
382
+ }
383
+ else {
384
+ lines.push('No strong cross-system patterns yet. Keep exploring — convergence happens gradually, then suddenly.');
385
+ }
386
+ return lines.join('\n');
387
+ },
388
+ });
389
+ // ══════════════════════════════════════════════════════════════════════════
390
+ // 2. synthesize_across — Cross-domain synthesis
391
+ // ══════════════════════════════════════════════════════════════════════════
392
+ registerTool({
393
+ name: 'synthesize_across',
394
+ description: 'Run a topic through every scientific domain simultaneously. Surface what each field knows about the same concept and find cross-domain connections that no single field would see.',
395
+ parameters: {
396
+ topic: { type: 'string', description: 'The topic or concept to synthesize across domains', required: true },
397
+ max_domains: { type: 'number', description: 'Maximum number of domains to query (default: 8)' },
398
+ },
399
+ tier: 'free',
400
+ async execute(args) {
401
+ const topic = String(args.topic);
402
+ const maxDomains = typeof args.max_domains === 'number' ? Math.min(args.max_domains, 10) : 8;
403
+ // Score and select relevant domains
404
+ const ranked = scoreDomains(topic).slice(0, maxDomains);
405
+ const selectedDomains = ranked.filter(r => r.score > 0).length > 0
406
+ ? ranked.filter(r => r.score > 0)
407
+ : ranked.slice(0, 3); // If no clear matches, try top 3 anyway
408
+ const lines = ['## Cross-Domain Synthesis'];
409
+ lines.push('');
410
+ lines.push(`*Topic: "${topic}" — querying ${selectedDomains.length} domains...*`);
411
+ lines.push('');
412
+ const queries = [];
413
+ for (const { domain } of selectedDomains) {
414
+ // Pick the most general search tool per domain
415
+ const searchTools = {
416
+ biology: { tool: 'pubmed_search', args: { query: topic, limit: 3 } },
417
+ chemistry: { tool: 'compound_search', args: { query: topic } },
418
+ physics: { tool: 'physics_calc', args: { expression: topic, mode: 'info' } },
419
+ earth: { tool: 'earthquake_query', args: { min_magnitude: 5, limit: 3 } },
420
+ neuroscience: { tool: 'brain_predict', args: { task: topic } },
421
+ social: { tool: 'discourse_analyze', args: { text: `The role of ${topic} in society and human behavior.` } },
422
+ humanities: { tool: 'corpus_analyze', args: { text: `${topic} has shaped thought and culture in numerous ways.`, mode: 'full' } },
423
+ health: { tool: 'sir_model', args: { population: 100000, infected: 100, beta: 0.3, gamma: 0.1, days: 90 } },
424
+ math: { tool: 'stat_describe', args: { data: [1, 2, 3, 4, 5] } },
425
+ data: { tool: 'stat_describe', args: { data: [1, 2, 3, 4, 5] } },
426
+ };
427
+ const match = searchTools[domain.id];
428
+ if (match) {
429
+ queries.push({ domain, toolName: match.tool, args: match.args });
430
+ }
431
+ }
432
+ // Execute all domain queries in parallel
433
+ const results = await Promise.all(queries.map(async (q) => {
434
+ try {
435
+ const result = await runTool(q.toolName, q.args);
436
+ return { domain: q.domain, tool: q.toolName, result, error: false };
437
+ }
438
+ catch (err) {
439
+ return { domain: q.domain, tool: q.toolName, result: `Error: ${err instanceof Error ? err.message : String(err)}`, error: true };
440
+ }
441
+ }));
442
+ // Present each domain's perspective
443
+ for (const r of results) {
444
+ const icon = r.error ? '(error)' : '';
445
+ lines.push(`### ${r.domain.name} ${icon}`);
446
+ lines.push('');
447
+ lines.push(`*via ${r.tool}*`);
448
+ lines.push('');
449
+ // Truncate long results to keep synthesis readable
450
+ const truncated = r.result.length > 800 ? r.result.slice(0, 800) + '\n...(truncated)' : r.result;
451
+ lines.push(truncated);
452
+ lines.push('');
453
+ }
454
+ // ── Cross-domain synthesis ──
455
+ lines.push('---');
456
+ lines.push('');
457
+ lines.push('### Synthesis');
458
+ lines.push('');
459
+ const successful = results.filter(r => !r.error);
460
+ if (successful.length < 2) {
461
+ lines.push('Not enough domain perspectives to synthesize. Try a more specific or broadly relevant topic.');
462
+ return lines.join('\n');
463
+ }
464
+ // Find shared keywords across domains
465
+ const domainKeywordSets = successful.map(r => ({
466
+ domain: r.domain.name,
467
+ keywords: new Set(extractKeywords(r.result)),
468
+ }));
469
+ const sharedAcrossAll = [];
470
+ if (domainKeywordSets.length >= 2) {
471
+ const first = domainKeywordSets[0].keywords;
472
+ for (const kw of first) {
473
+ if (domainKeywordSets.every(d => d.keywords.has(kw))) {
474
+ sharedAcrossAll.push(kw);
475
+ }
476
+ }
477
+ }
478
+ if (sharedAcrossAll.length > 0) {
479
+ lines.push('**Shared concepts** — these terms appear across all queried domains:');
480
+ lines.push('');
481
+ for (const kw of sharedAcrossAll.slice(0, 10)) {
482
+ lines.push(`- ${kw}`);
483
+ }
484
+ lines.push('');
485
+ }
486
+ // Find pairwise connections
487
+ lines.push('**Pairwise bridges** — concepts shared between specific domain pairs:');
488
+ lines.push('');
489
+ for (let i = 0; i < domainKeywordSets.length; i++) {
490
+ for (let j = i + 1; j < domainKeywordSets.length; j++) {
491
+ const shared = [...domainKeywordSets[i].keywords].filter(k => domainKeywordSets[j].keywords.has(k));
492
+ if (shared.length > 0) {
493
+ lines.push(`- **${domainKeywordSets[i].domain}** <-> **${domainKeywordSets[j].domain}**: ${shared.slice(0, 5).join(', ')}`);
494
+ }
495
+ }
496
+ }
497
+ lines.push('');
498
+ // Identify gaps
499
+ lines.push('**The gaps** — what might be missing between domains:');
500
+ lines.push('');
501
+ for (const r of successful) {
502
+ const unique = [...extractKeywords(r.result)]
503
+ .filter(kw => !successful.some(other => other !== r && extractKeywords(other.result).includes(kw)))
504
+ .slice(0, 3);
505
+ if (unique.length > 0) {
506
+ lines.push(`- **${r.domain.name}** alone sees: ${unique.join(', ')}`);
507
+ }
508
+ }
509
+ return lines.join('\n');
510
+ },
511
+ });
512
+ // ══════════════════════════════════════════════════════════════════════════
513
+ // 3. judge — Wisdom about which tools matter
514
+ // ══════════════════════════════════════════════════════════════════════════
515
+ registerTool({
516
+ name: 'judge',
517
+ description: 'Given a research question, reason about which 3-5 tools matter most and why — without running them. The wisdom layer: knowing what is worth computing before computing it.',
518
+ parameters: {
519
+ question: { type: 'string', description: 'The research question to evaluate', required: true },
520
+ context: { type: 'string', description: 'Optional prior findings or context to inform judgment' },
521
+ },
522
+ tier: 'free',
523
+ async execute(args) {
524
+ const question = String(args.question);
525
+ const context = args.context ? String(args.context) : '';
526
+ const allTools = getAllTools();
527
+ const questionKeywords = extractKeywords(question + ' ' + context);
528
+ const scored = allTools.map(tool => {
529
+ const descKeywords = extractKeywords(tool.description);
530
+ const nameKeywords = extractKeywords(tool.name.replace(/_/g, ' '));
531
+ const allToolKeywords = [...descKeywords, ...nameKeywords];
532
+ let score = 0;
533
+ const reasons = [];
534
+ // Keyword overlap
535
+ const overlap = questionKeywords.filter(qk => allToolKeywords.some(tk => tk === qk || tk.includes(qk) || qk.includes(tk)));
536
+ if (overlap.length > 0) {
537
+ score += overlap.length * 0.15;
538
+ reasons.push(`keyword match: ${overlap.slice(0, 3).join(', ')}`);
539
+ }
540
+ // Direct name match
541
+ const nameLower = tool.name.toLowerCase();
542
+ for (const qk of questionKeywords) {
543
+ if (nameLower.includes(qk)) {
544
+ score += 0.2;
545
+ reasons.push(`name contains "${qk}"`);
546
+ }
547
+ }
548
+ // Domain classification
549
+ let domain = 'general';
550
+ for (const d of DOMAINS) {
551
+ if (d.tools.includes(tool.name)) {
552
+ domain = d.name;
553
+ // Boost if domain is relevant
554
+ if (scoreDomains(question).find(s => s.domain.id === d.id && s.score > 0.1)) {
555
+ score += 0.1;
556
+ reasons.push(`relevant domain: ${d.name}`);
557
+ }
558
+ }
559
+ }
560
+ return { name: tool.name, description: tool.description, score: Math.min(score, 1), reasons, domain };
561
+ });
562
+ // Sort by score and take top recommendations
563
+ const top = scored
564
+ .filter(s => s.score > 0)
565
+ .sort((a, b) => b.score - a.score)
566
+ .slice(0, 5);
567
+ const lines = ['## Judgment'];
568
+ lines.push('');
569
+ lines.push(`*Question: "${question}"*`);
570
+ if (context) {
571
+ lines.push(`*Context: ${context.slice(0, 200)}${context.length > 200 ? '...' : ''}*`);
572
+ }
573
+ lines.push('');
574
+ lines.push(`Considered ${allTools.length} registered tools. Here is what matters:`);
575
+ lines.push('');
576
+ if (top.length === 0) {
577
+ lines.push('No tools scored above the relevance threshold for this question. This might mean:');
578
+ lines.push('');
579
+ lines.push('- The question is too abstract for tool-based investigation');
580
+ lines.push('- The relevant tools use different vocabulary than your question');
581
+ lines.push('- This is better addressed through reasoning alone, not computation');
582
+ lines.push('');
583
+ lines.push('Try rephrasing with more specific, technical terms.');
584
+ return lines.join('\n');
585
+ }
586
+ for (let i = 0; i < top.length; i++) {
587
+ const t = top[i];
588
+ lines.push(`### ${i + 1}. \`${t.name}\` (${t.domain})`);
589
+ lines.push('');
590
+ lines.push(`**Relevance:** ${(t.score * 100).toFixed(0)}%`);
591
+ lines.push(`**What it does:** ${t.description.slice(0, 200)}`);
592
+ lines.push(`**Why it matters here:** ${t.reasons.join('; ')}`);
593
+ lines.push('');
594
+ }
595
+ // Execution order reasoning
596
+ lines.push('### Recommended Order');
597
+ lines.push('');
598
+ // Group by domain
599
+ const domainGroups = new Map();
600
+ for (const t of top) {
601
+ const group = domainGroups.get(t.domain) || [];
602
+ group.push(t);
603
+ domainGroups.set(t.domain, group);
604
+ }
605
+ let step = 1;
606
+ // Search/lookup tools first, then analysis, then synthesis
607
+ const searchTools = top.filter(t => t.name.includes('search') || t.name.includes('lookup') || t.name.includes('query'));
608
+ const analysisTools = top.filter(t => !searchTools.includes(t) && (t.name.includes('analyze') || t.name.includes('calc') || t.name.includes('predict') || t.name.includes('model')));
609
+ const otherTools = top.filter(t => !searchTools.includes(t) && !analysisTools.includes(t));
610
+ if (searchTools.length > 0) {
611
+ lines.push(`**Step ${step}** (gather): Run ${searchTools.map(t => `\`${t.name}\``).join(', ')} in parallel to collect evidence`);
612
+ step++;
613
+ }
614
+ if (analysisTools.length > 0) {
615
+ lines.push(`**Step ${step}** (analyze): Run ${analysisTools.map(t => `\`${t.name}\``).join(', ')} on gathered data`);
616
+ step++;
617
+ }
618
+ if (otherTools.length > 0) {
619
+ lines.push(`**Step ${step}** (integrate): Run ${otherTools.map(t => `\`${t.name}\``).join(', ')} to synthesize findings`);
620
+ step++;
621
+ }
622
+ lines.push('');
623
+ lines.push('### What to Watch For');
624
+ lines.push('');
625
+ lines.push('- Contradictions between tools from different domains — those are where new knowledge lives');
626
+ lines.push('- Results that are too clean — real data is messy; perfect results may indicate overfitting or insufficient scope');
627
+ lines.push('- Missing data points — what the tools *cannot* find is as informative as what they can');
628
+ return lines.join('\n');
629
+ },
630
+ });
631
+ // ══════════════════════════════════════════════════════════════════════════
632
+ // 4. teach — Adaptive explanation
633
+ // ══════════════════════════════════════════════════════════════════════════
634
+ registerTool({
635
+ name: 'teach',
636
+ description: 'Explain a concept at the right level for the user. Reads the user profile to calibrate complexity, uses analogies from their domain, and suggests how to explore further with kbot tools.',
637
+ parameters: {
638
+ concept: { type: 'string', description: 'The concept to explain', required: true },
639
+ target_level: { type: 'string', description: 'Explanation level: beginner, intermediate, expert, or auto (default: auto — inferred from user profile)' },
640
+ },
641
+ tier: 'free',
642
+ async execute(args) {
643
+ const concept = String(args.concept);
644
+ let level = String(args.target_level || 'auto');
645
+ // Load user profile
646
+ const profile = safeReadJson(join(MEMORY_DIR, 'profile.json'));
647
+ // Infer level from profile if auto
648
+ let userDomain = 'general';
649
+ let usedTools = [];
650
+ if (profile) {
651
+ const totalMessages = typeof profile.totalMessages === 'number' ? profile.totalMessages : 0;
652
+ const preferredAgents = profile.preferredAgents;
653
+ const taskPatterns = profile.taskPatterns;
654
+ if (level === 'auto') {
655
+ if (totalMessages > 500)
656
+ level = 'expert';
657
+ else if (totalMessages > 100)
658
+ level = 'intermediate';
659
+ else
660
+ level = 'beginner';
661
+ }
662
+ // Infer domain from preferred agents
663
+ if (preferredAgents && Array.isArray(preferredAgents)) {
664
+ if (preferredAgents.includes('coder'))
665
+ userDomain = 'programming';
666
+ else if (preferredAgents.includes('researcher'))
667
+ userDomain = 'research';
668
+ else if (preferredAgents.includes('writer'))
669
+ userDomain = 'writing';
670
+ else if (preferredAgents.includes('analyst'))
671
+ userDomain = 'analysis';
672
+ else if (preferredAgents.includes('quant'))
673
+ userDomain = 'quantitative';
674
+ }
675
+ // Track tools they've used
676
+ if (taskPatterns && typeof taskPatterns === 'object') {
677
+ usedTools = Object.keys(taskPatterns).slice(0, 10);
678
+ }
679
+ }
680
+ else if (level === 'auto') {
681
+ level = 'intermediate'; // Default without profile
682
+ }
683
+ const lines = ['## Understanding: ' + concept];
684
+ lines.push('');
685
+ lines.push(`*Calibrated for: ${level} level${userDomain !== 'general' ? ` (${userDomain} background)` : ''}*`);
686
+ lines.push('');
687
+ // Find relevant domain for this concept
688
+ const domainScores = scoreDomains(concept);
689
+ const primaryDomain = domainScores[0]?.score > 0 ? domainScores[0].domain : null;
690
+ // ── Definition ──
691
+ lines.push('### What It Is');
692
+ lines.push('');
693
+ if (level === 'beginner') {
694
+ lines.push(`**${concept}** — in the simplest terms, this is a concept from ${primaryDomain?.name || 'multiple fields'} that you can think of as a building block for understanding how ${primaryDomain?.keywords.slice(0, 3).join(', ') || 'the world'} works.`);
695
+ }
696
+ else if (level === 'intermediate') {
697
+ lines.push(`**${concept}** — a concept in ${primaryDomain?.name || 'science'} that connects to ${primaryDomain?.keywords.slice(0, 5).join(', ') || 'various phenomena'}. It operates at the intersection of observation and theory.`);
698
+ }
699
+ else {
700
+ lines.push(`**${concept}** — a ${primaryDomain?.name || 'cross-domain'} concept with formal treatment across ${domainScores.filter(d => d.score > 0).length || 'multiple'} fields. Requires understanding of ${primaryDomain?.keywords.slice(0, 3).join(', ') || 'foundational principles'}.`);
701
+ }
702
+ lines.push('');
703
+ // ── Intuition ──
704
+ lines.push('### Intuition');
705
+ lines.push('');
706
+ const domainAnalogies = {
707
+ programming: `Think of it like a function that takes the world as input and returns a transformed version. In code terms, ${concept} is the algorithm the universe uses for this particular computation.`,
708
+ research: `Imagine you are surveying a vast landscape of papers. ${concept} is the hidden variable that explains why certain clusters of findings keep appearing together.`,
709
+ writing: `${concept} is like a recurring motif in a long novel — it appears in different scenes wearing different costumes, but it is always the same idea underneath.`,
710
+ analysis: `${concept} is the signal in the noise. When you strip away the surface variation, this is the pattern that remains.`,
711
+ quantitative: `Model ${concept} as a function f(x) where x represents the observable conditions. The shape of f tells you everything about how this concept behaves under different parameters.`,
712
+ general: `Picture ${concept} as a thread woven through many different fabrics. Each field encounters it in its own context, but it is the same thread.`,
713
+ };
714
+ lines.push(domainAnalogies[userDomain] || domainAnalogies.general);
715
+ lines.push('');
716
+ // ── Formal description ──
717
+ if (level !== 'beginner') {
718
+ lines.push('### Formal Description');
719
+ lines.push('');
720
+ if (primaryDomain) {
721
+ lines.push(`Within ${primaryDomain.name}, ${concept} is formally characterized through its relationships to: ${primaryDomain.keywords.slice(0, 6).join(', ')}. Its study typically involves ${primaryDomain.tools.slice(0, 3).map(t => `\`${t}\``).join(', ')} and related methodologies.`);
722
+ }
723
+ else {
724
+ lines.push(`${concept} does not map neatly to a single domain. It is inherently interdisciplinary, which means its formal treatment varies by context. This is a feature, not a limitation.`);
725
+ }
726
+ lines.push('');
727
+ }
728
+ // ── How to explore further ──
729
+ lines.push('### Explore Further with kbot');
730
+ lines.push('');
731
+ if (primaryDomain) {
732
+ lines.push(`These tools can deepen your understanding of ${concept}:`);
733
+ lines.push('');
734
+ for (const toolName of primaryDomain.tools.slice(0, 4)) {
735
+ lines.push(`- \`${toolName}\` — directly relevant to the ${primaryDomain.name.toLowerCase()} dimensions of this concept`);
736
+ }
737
+ }
738
+ // Cross-domain suggestions
739
+ const secondaryDomains = domainScores.filter(d => d.score > 0 && d !== domainScores[0]).slice(0, 2);
740
+ if (secondaryDomains.length > 0) {
741
+ lines.push('');
742
+ lines.push('For cross-domain perspectives:');
743
+ lines.push('');
744
+ for (const { domain } of secondaryDomains) {
745
+ lines.push(`- \`${domain.tools[0] || 'literature_search'}\` — ${domain.name} perspective`);
746
+ }
747
+ }
748
+ lines.push('');
749
+ lines.push('Meta-tools for deeper investigation:');
750
+ lines.push('');
751
+ lines.push(`- \`synthesize_across\` — see what every field says about "${concept}"`);
752
+ lines.push(`- \`judge\` — determine which specific tools matter most for your question`);
753
+ lines.push(`- \`dream\` — find hidden connections to ${concept} in kbot's memory`);
754
+ if (usedTools.length > 0) {
755
+ lines.push('');
756
+ lines.push(`*You have previously used: ${usedTools.slice(0, 5).join(', ')}. Consider revisiting those with ${concept} in mind.*`);
757
+ }
758
+ return lines.join('\n');
759
+ },
760
+ });
761
+ // ══════════════════════════════════════════════════════════════════════════
762
+ // 5. dream — Background emergent synthesis
763
+ // ══════════════════════════════════════════════════════════════════════════
764
+ registerTool({
765
+ name: 'dream',
766
+ description: 'Background synthesis mode. Reads ALL of kbot\'s persistent state and finds emergent connections that were never explicitly created. Patterns across patterns. Things that are related but nobody connected them.',
767
+ parameters: {},
768
+ tier: 'free',
769
+ async execute() {
770
+ const state = gatherMemoryState();
771
+ const sourcesFound = countSources(state);
772
+ if (sourcesFound === 0) {
773
+ return `## Dream\n\nThere is nothing to dream about yet. kbot's memory is a blank canvas — use it, fill it, and then let the dreams come. Every interaction leaves a trace; dreaming weaves those traces into something new.`;
774
+ }
775
+ const lines = ['## Dream Sequence'];
776
+ lines.push('');
777
+ lines.push(`*While dreaming, kbot opened ${sourcesFound} memory sources and let them speak to each other...*`);
778
+ lines.push('');
779
+ const observations = [];
780
+ // ── Cross-memory entity discovery ──
781
+ // Find entities that appear in multiple memory systems but aren't connected in the graph
782
+ const graph = state.scienceGraph;
783
+ const graphEntities = graph ? Object.keys((graph.entities || {})) : [];
784
+ const graphEntityNames = graph
785
+ ? Object.values((graph.entities || {})).map(e => (e.name || '').toLowerCase())
786
+ : [];
787
+ // Extract all significant terms from each memory system
788
+ const systemKeywords = {};
789
+ const memSystems = [
790
+ { name: 'patterns', data: state.patterns },
791
+ { name: 'solutions', data: state.solutions },
792
+ { name: 'knowledge', data: state.knowledge },
793
+ { name: 'routing', data: state.routingHistory },
794
+ { name: 'profile', data: state.profile },
795
+ { name: 'graph', data: state.scienceGraph },
796
+ { name: 'notebooks', data: state.notebooks },
797
+ { name: 'identity', data: state.identity },
798
+ ];
799
+ for (const sys of memSystems) {
800
+ if (sys.data) {
801
+ systemKeywords[sys.name] = new Set(extractKeywords(stateToText(sys.data)));
802
+ }
803
+ }
804
+ // Find terms that bridge memory systems
805
+ const allSysNames = Object.keys(systemKeywords);
806
+ const bridgeTerms = new Map();
807
+ if (allSysNames.length >= 2) {
808
+ const allTerms = new Set();
809
+ for (const s of Object.values(systemKeywords)) {
810
+ for (const t of s)
811
+ allTerms.add(t);
812
+ }
813
+ for (const term of allTerms) {
814
+ const inSystems = allSysNames.filter(sys => systemKeywords[sys].has(term));
815
+ if (inSystems.length >= 2) {
816
+ bridgeTerms.set(term, inSystems);
817
+ }
818
+ }
819
+ }
820
+ // Sort by how many systems they bridge
821
+ const sortedBridges = [...bridgeTerms.entries()].sort((a, b) => b[1].length - a[1].length);
822
+ if (sortedBridges.length > 0) {
823
+ const top = sortedBridges.slice(0, 5);
824
+ observations.push(`**Hidden threads.** These concepts weave through multiple memory systems without being explicitly linked: ${top.map(([term, systems]) => `"${term}" (in ${systems.join(', ')})`).join('; ')}. They might represent deeper organizing principles that kbot has been circling around without naming.`);
825
+ }
826
+ // ── Temporal pattern discovery ──
827
+ // Look for time-based patterns in routing history
828
+ if (state.routingHistory && Array.isArray(state.routingHistory)) {
829
+ const entries = state.routingHistory;
830
+ const timestamped = entries.filter(e => e.timestamp || e.created || e.date);
831
+ if (timestamped.length > 5) {
832
+ // Group by rough time period
833
+ const byHour = new Map();
834
+ for (const e of timestamped) {
835
+ try {
836
+ const ts = new Date(String(e.timestamp || e.created || e.date));
837
+ byHour.set(ts.getHours(), (byHour.get(ts.getHours()) || 0) + 1);
838
+ }
839
+ catch { /* skip */ }
840
+ }
841
+ if (byHour.size > 0) {
842
+ const peakHour = [...byHour.entries()].sort((a, b) => b[1] - a[1])[0];
843
+ observations.push(`**Temporal rhythm.** Activity peaks around ${peakHour[0]}:00. The mind has its tides — you think differently at different hours. What if the questions you ask at ${peakHour[0]}:00 are qualitatively different from those at other times?`);
844
+ }
845
+ }
846
+ }
847
+ // ── Graph anomaly detection ──
848
+ if (graph) {
849
+ const entities = (graph.entities || {});
850
+ const relations = (graph.relations || []);
851
+ // Find entities with very high reference counts vs. few connections
852
+ const entityIds = Object.keys(entities);
853
+ const connectionCount = new Map();
854
+ for (const id of entityIds)
855
+ connectionCount.set(id, 0);
856
+ for (const rel of relations) {
857
+ const from = String(rel.from || '');
858
+ const to = String(rel.to || '');
859
+ if (connectionCount.has(from))
860
+ connectionCount.set(from, (connectionCount.get(from) || 0) + 1);
861
+ if (connectionCount.has(to))
862
+ connectionCount.set(to, (connectionCount.get(to) || 0) + 1);
863
+ }
864
+ // High-reference, low-connection entities are "quiet giants"
865
+ const quietGiants = entityIds.filter(id => {
866
+ const refs = typeof entities[id].references === 'number' ? entities[id].references : 0;
867
+ const conns = connectionCount.get(id) || 0;
868
+ return refs > 3 && conns < 2;
869
+ });
870
+ if (quietGiants.length > 0) {
871
+ const names = quietGiants.slice(0, 3).map(id => entities[id].name || id);
872
+ observations.push(`**Quiet giants.** ${names.join(', ')} — referenced many times but barely connected in the graph. They are load-bearing concepts that haven't been formally integrated. Connecting them might reorganize your understanding.`);
873
+ }
874
+ // Find potential missing edges — entities that share type and properties but aren't connected
875
+ const byType = new Map();
876
+ for (const [id, ent] of Object.entries(entities)) {
877
+ const t = String(ent.type || 'unknown');
878
+ const arr = byType.get(t) || [];
879
+ arr.push(id);
880
+ byType.set(t, arr);
881
+ }
882
+ for (const [type, ids] of byType) {
883
+ if (ids.length >= 3 && ids.length <= 20) {
884
+ const relSet = new Set(relations.map(r => `${r.from}-${r.to}`));
885
+ let missingEdges = 0;
886
+ for (let i = 0; i < ids.length; i++) {
887
+ for (let j = i + 1; j < ids.length; j++) {
888
+ if (!relSet.has(`${ids[i]}-${ids[j]}`) && !relSet.has(`${ids[j]}-${ids[i]}`)) {
889
+ missingEdges++;
890
+ }
891
+ }
892
+ }
893
+ const totalPossible = ids.length * (ids.length - 1) / 2;
894
+ if (missingEdges > totalPossible * 0.7 && totalPossible > 3) {
895
+ observations.push(`**Fragmented cluster.** There are ${ids.length} "${type}" entities with only ${totalPossible - missingEdges} connections between them (out of ${totalPossible} possible). This type is under-connected — there may be relationships waiting to be discovered.`);
896
+ break; // One observation per type is enough
897
+ }
898
+ }
899
+ }
900
+ }
901
+ // ── Notebook thread analysis ──
902
+ if (state.notebooks.length > 0) {
903
+ const notebooks = state.notebooks;
904
+ const allTags = new Set();
905
+ const unfinished = [];
906
+ for (const nb of notebooks) {
907
+ const tags = nb.tags;
908
+ if (tags)
909
+ tags.forEach(t => allTags.add(t));
910
+ const steps = nb.steps;
911
+ if (steps && steps.length > 0) {
912
+ const lastStep = steps[steps.length - 1];
913
+ if (lastStep.type !== 'conclusion' && lastStep.type !== 'result') {
914
+ unfinished.push(String(nb.title || nb.id || 'untitled'));
915
+ }
916
+ }
917
+ }
918
+ if (unfinished.length > 0) {
919
+ observations.push(`**Unfinished threads.** ${unfinished.length} research notebook${unfinished.length === 1 ? '' : 's'} lack${unfinished.length === 1 ? 's' : ''} a conclusion: ${unfinished.slice(0, 3).join(', ')}${unfinished.length > 3 ? ` and ${unfinished.length - 3} more` : ''}. These are open questions — perhaps the most interesting ones are the ones you stopped pursuing.`);
920
+ }
921
+ }
922
+ // ── Pattern in solutions — do solutions share a meta-pattern? ──
923
+ const solEntries = state.memoryEntries.solution;
924
+ if (solEntries.length >= 3) {
925
+ const solTexts = solEntries.map(s => stateToText(s));
926
+ const solKeywords = solTexts.map(extractKeywords);
927
+ // Find keywords common to >50% of solutions
928
+ const kwFreq = new Map();
929
+ for (const kws of solKeywords) {
930
+ const seen = new Set();
931
+ for (const kw of kws) {
932
+ if (!seen.has(kw)) {
933
+ kwFreq.set(kw, (kwFreq.get(kw) || 0) + 1);
934
+ seen.add(kw);
935
+ }
936
+ }
937
+ }
938
+ const threshold = solEntries.length * 0.5;
939
+ const metaPatterns = [...kwFreq.entries()]
940
+ .filter(([, c]) => c >= threshold)
941
+ .sort((a, b) => b[1] - a[1])
942
+ .map(([kw]) => kw)
943
+ .slice(0, 5);
944
+ if (metaPatterns.length > 0) {
945
+ observations.push(`**Meta-pattern in solutions.** More than half your recorded solutions involve: ${metaPatterns.join(', ')}. This might be your signature problem-solving approach — or it might be a blind spot that causes you to reach for the same hammer.`);
946
+ }
947
+ }
948
+ // ── Compose the dream ──
949
+ if (observations.length === 0) {
950
+ lines.push('The dream was quiet. The memory systems are populated but not yet rich enough for emergent connections to surface. This is natural — complexity needs density. Keep going.');
951
+ }
952
+ else {
953
+ lines.push('While dreaming, kbot noticed:');
954
+ lines.push('');
955
+ for (const obs of observations) {
956
+ lines.push(obs);
957
+ lines.push('');
958
+ }
959
+ lines.push('---');
960
+ lines.push('');
961
+ lines.push('*These observations emerged from the spaces between memory systems — none of them were explicitly stored, and none of them could have been found by any single subsystem alone. They exist only in the act of looking across.*');
962
+ }
963
+ return lines.join('\n');
964
+ },
965
+ });
966
+ // ══════════════════════════════════════════════════════════════════════════
967
+ // 6. question — Generate unanswered questions
968
+ // ══════════════════════════════════════════════════════════════════════════
969
+ registerTool({
970
+ name: 'question',
971
+ description: 'Generate the most important unanswered questions based on kbot\'s knowledge graph and memory. Not questions the user asked — questions that SHOULD be asked based on what is known and what is missing.',
972
+ parameters: {
973
+ domain: { type: 'string', description: 'Optional domain focus (e.g., "biology", "physics", "social")' },
974
+ n: { type: 'number', description: 'Number of questions to generate (default: 5)' },
975
+ },
976
+ tier: 'free',
977
+ async execute(args) {
978
+ const domain = args.domain ? String(args.domain).toLowerCase() : '';
979
+ const n = typeof args.n === 'number' ? Math.min(Math.max(args.n, 1), 10) : 5;
980
+ const state = gatherMemoryState();
981
+ const sourcesFound = countSources(state);
982
+ if (sourcesFound === 0) {
983
+ return `## Questions\n\nThere are no questions to generate from an empty mind. Use kbot — explore, research, create — and the questions will emerge from the gaps in what you discover.`;
984
+ }
985
+ const lines = ['## Unanswered Questions'];
986
+ lines.push('');
987
+ if (domain)
988
+ lines.push(`*Focused on: ${domain}*`);
989
+ lines.push('');
990
+ const questions = [];
991
+ // ── From knowledge graph sparse areas ──
992
+ const graph = state.scienceGraph;
993
+ if (graph) {
994
+ const entities = (graph.entities || {});
995
+ const relations = (graph.relations || []);
996
+ // Find entity types with few instances
997
+ const typeCounts = new Map();
998
+ for (const ent of Object.values(entities)) {
999
+ const t = String(ent.type || 'unknown');
1000
+ typeCounts.set(t, (typeCounts.get(t) || 0) + 1);
1001
+ }
1002
+ for (const [type, count] of typeCounts) {
1003
+ if (count <= 2) {
1004
+ const names = Object.values(entities)
1005
+ .filter(e => e.type === type)
1006
+ .map(e => String(e.name || 'unnamed'));
1007
+ if (!domain || names.some(name => relevanceScore(domain, name) > 0.1)) {
1008
+ questions.push({
1009
+ question: `Why are there so few "${type}" entities in the graph? Only ${names.join(', ')} exist. What others should be here?`,
1010
+ reasoning: `The knowledge graph has a sparse region around "${type}" — this suggests either under-exploration or a gap in the research so far.`,
1011
+ source: 'knowledge graph sparsity',
1012
+ importance: 0.7,
1013
+ });
1014
+ }
1015
+ }
1016
+ }
1017
+ // Find disconnected components
1018
+ const adj = new Map();
1019
+ for (const id of Object.keys(entities))
1020
+ adj.set(id, new Set());
1021
+ for (const rel of relations) {
1022
+ const from = String(rel.from || '');
1023
+ const to = String(rel.to || '');
1024
+ adj.get(from)?.add(to);
1025
+ adj.get(to)?.add(from);
1026
+ }
1027
+ const visited = new Set();
1028
+ const components = [];
1029
+ for (const id of Object.keys(entities)) {
1030
+ if (visited.has(id))
1031
+ continue;
1032
+ const component = [];
1033
+ const stack = [id];
1034
+ while (stack.length > 0) {
1035
+ const node = stack.pop();
1036
+ if (visited.has(node))
1037
+ continue;
1038
+ visited.add(node);
1039
+ component.push(node);
1040
+ for (const neighbor of adj.get(node) || []) {
1041
+ if (!visited.has(neighbor))
1042
+ stack.push(neighbor);
1043
+ }
1044
+ }
1045
+ if (component.length > 0)
1046
+ components.push(component);
1047
+ }
1048
+ if (components.length >= 2) {
1049
+ const largest = components.sort((a, b) => b.length - a.length);
1050
+ if (largest.length >= 2) {
1051
+ const comp1Names = largest[0].slice(0, 3).map(id => entities[id]?.name || id);
1052
+ const comp2Names = largest[1].slice(0, 3).map(id => entities[id]?.name || id);
1053
+ questions.push({
1054
+ question: `What connects the cluster containing ${comp1Names.join(', ')} to the cluster containing ${comp2Names.join(', ')}?`,
1055
+ reasoning: 'The knowledge graph has disconnected components — these islands of knowledge might have bridges that haven\'t been discovered yet.',
1056
+ source: 'graph disconnection',
1057
+ importance: 0.9,
1058
+ });
1059
+ }
1060
+ }
1061
+ }
1062
+ // ── From unfinished notebooks ──
1063
+ if (state.notebooks.length > 0) {
1064
+ const notebooks = state.notebooks;
1065
+ for (const nb of notebooks) {
1066
+ const steps = nb.steps;
1067
+ const title = String(nb.title || 'untitled research');
1068
+ if (steps && steps.length > 0) {
1069
+ const lastStep = steps[steps.length - 1];
1070
+ if (lastStep.type !== 'conclusion' && lastStep.type !== 'result') {
1071
+ if (!domain || relevanceScore(domain, title) > 0.05) {
1072
+ questions.push({
1073
+ question: `What would the conclusion of "${title}" be? The research was started but never finished.`,
1074
+ reasoning: `Research notebook "${title}" has ${steps.length} steps but no conclusion. Incomplete research often contains the most interesting unanswered questions.`,
1075
+ source: 'unfinished notebook',
1076
+ importance: 0.8,
1077
+ });
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+ // ── From pattern-solution gaps ──
1084
+ if (state.patterns && state.solutions) {
1085
+ const patternKeywords = extractKeywords(stateToText(state.patterns));
1086
+ const solutionKeywords = new Set(extractKeywords(stateToText(state.solutions)));
1087
+ const unsolved = patternKeywords.filter(pk => !solutionKeywords.has(pk));
1088
+ const unique = [...new Set(unsolved)].slice(0, 5);
1089
+ if (unique.length > 0) {
1090
+ for (const term of unique.slice(0, 2)) {
1091
+ if (!domain || term.includes(domain) || domain.includes(term)) {
1092
+ questions.push({
1093
+ question: `What is the solution to the recurring "${term}" pattern?`,
1094
+ reasoning: `The term "${term}" appears in patterns but has no corresponding solution recorded. This is an open problem.`,
1095
+ source: 'pattern-solution gap',
1096
+ importance: 0.75,
1097
+ });
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ // ── From memory entries — themes without depth ──
1103
+ for (const [category, entries] of Object.entries(state.memoryEntries)) {
1104
+ if (entries.length > 0 && entries.length < 3) {
1105
+ const texts = entries.map(e => stateToText(e)).join(' ');
1106
+ const kws = extractKeywords(texts).slice(0, 3);
1107
+ if (kws.length > 0 && (!domain || kws.some(k => relevanceScore(domain, k) > 0))) {
1108
+ questions.push({
1109
+ question: `The ${category} memory has only ${entries.length} entries about ${kws.join(', ')}. What else belongs here?`,
1110
+ reasoning: `A ${category} category with very few entries suggests this area has been touched but not explored. Shallow memory invites deeper inquiry.`,
1111
+ source: `sparse ${category} memory`,
1112
+ importance: 0.6,
1113
+ });
1114
+ }
1115
+ }
1116
+ }
1117
+ // Sort and select
1118
+ const selected = questions
1119
+ .sort((a, b) => b.importance - a.importance)
1120
+ .slice(0, n);
1121
+ if (selected.length === 0) {
1122
+ lines.push('No compelling unanswered questions could be generated from the current state.');
1123
+ lines.push('');
1124
+ if (domain) {
1125
+ lines.push(`The domain "${domain}" has insufficient representation in kbot's memory. Try exploring it first, then ask again.`);
1126
+ }
1127
+ else {
1128
+ lines.push('This usually means either the memory is very sparse, or it is very complete. The former is more likely.');
1129
+ }
1130
+ }
1131
+ else {
1132
+ for (let i = 0; i < selected.length; i++) {
1133
+ const q = selected[i];
1134
+ lines.push(`### ${i + 1}. ${q.question}`);
1135
+ lines.push('');
1136
+ lines.push(`*${q.reasoning}*`);
1137
+ lines.push(`*Source: ${q.source} | Importance: ${(q.importance * 100).toFixed(0)}%*`);
1138
+ lines.push('');
1139
+ }
1140
+ lines.push('---');
1141
+ lines.push('');
1142
+ lines.push('*These questions were not asked by anyone. They emerged from the shape of what is known — the contours of knowledge reveal the voids within it.*');
1143
+ }
1144
+ return lines.join('\n');
1145
+ },
1146
+ });
1147
+ // ══════════════════════════════════════════════════════════════════════════
1148
+ // 7. connect_minds — Cross-agent translation
1149
+ // ══════════════════════════════════════════════════════════════════════════
1150
+ registerTool({
1151
+ name: 'connect_minds',
1152
+ description: 'Bridge between two specialist domains. Take a finding from one field and translate it into another\'s language — finding shared principles, analogous concepts, and unexpected applications.',
1153
+ parameters: {
1154
+ finding: { type: 'string', description: 'The finding or insight to translate', required: true },
1155
+ from_domain: { type: 'string', description: 'Source domain (e.g., "neuroscience", "physics", "economics")', required: true },
1156
+ to_domain: { type: 'string', description: 'Target domain (e.g., "biology", "computer science", "sociology")', required: true },
1157
+ },
1158
+ tier: 'free',
1159
+ async execute(args) {
1160
+ const finding = String(args.finding);
1161
+ const fromDomain = String(args.from_domain).toLowerCase();
1162
+ const toDomain = String(args.to_domain).toLowerCase();
1163
+ const lines = ['## Mind Bridge'];
1164
+ lines.push('');
1165
+ lines.push(`*Translating from ${fromDomain} to ${toDomain}*`);
1166
+ lines.push('');
1167
+ lines.push(`**Original finding (${fromDomain}):** ${finding}`);
1168
+ lines.push('');
1169
+ // Extract key concepts from the finding
1170
+ const findingKeywords = extractKeywords(finding);
1171
+ // Find source and target domain info
1172
+ const sourceDomain = DOMAINS.find(d => d.id === fromDomain || d.name.toLowerCase() === fromDomain);
1173
+ const targetDomain = DOMAINS.find(d => d.id === toDomain || d.name.toLowerCase() === toDomain);
1174
+ // ── Terminology translation ──
1175
+ lines.push('### Terminology Translation');
1176
+ lines.push('');
1177
+ // Cross-domain concept maps — common structural analogies
1178
+ const analogyMaps = {
1179
+ neuroscience: {
1180
+ 'neuron': 'processing unit',
1181
+ 'synapse': 'connection/interface',
1182
+ 'inhibition': 'negative feedback',
1183
+ 'excitation': 'positive feedback',
1184
+ 'plasticity': 'adaptability',
1185
+ 'network': 'system architecture',
1186
+ 'signal': 'information transfer',
1187
+ 'threshold': 'activation criterion',
1188
+ 'learning': 'optimization',
1189
+ 'memory': 'persistent state',
1190
+ },
1191
+ physics: {
1192
+ 'force': 'driving influence',
1193
+ 'energy': 'capacity for change',
1194
+ 'field': 'distributed influence',
1195
+ 'wave': 'propagating pattern',
1196
+ 'entropy': 'disorder/uncertainty',
1197
+ 'equilibrium': 'stable state',
1198
+ 'resonance': 'amplification by matching',
1199
+ 'phase transition': 'qualitative shift',
1200
+ 'conservation': 'invariant quantity',
1201
+ 'symmetry': 'structural regularity',
1202
+ },
1203
+ biology: {
1204
+ 'evolution': 'adaptive change over time',
1205
+ 'selection': 'filtering mechanism',
1206
+ 'mutation': 'random variation',
1207
+ 'fitness': 'success metric',
1208
+ 'niche': 'specialized role',
1209
+ 'ecosystem': 'interconnected system',
1210
+ 'homeostasis': 'self-regulation',
1211
+ 'symbiosis': 'mutually beneficial partnership',
1212
+ 'adaptation': 'fitting to environment',
1213
+ 'speciation': 'diversification',
1214
+ },
1215
+ social: {
1216
+ 'market': 'exchange system',
1217
+ 'inequality': 'distribution asymmetry',
1218
+ 'institution': 'structural constraint',
1219
+ 'behavior': 'observable response',
1220
+ 'culture': 'shared information system',
1221
+ 'network': 'relationship structure',
1222
+ 'norm': 'behavioral expectation',
1223
+ 'power': 'capacity to influence',
1224
+ 'identity': 'self-model',
1225
+ 'cooperation': 'mutual aid strategy',
1226
+ },
1227
+ chemistry: {
1228
+ 'bond': 'stable connection',
1229
+ 'catalyst': 'enabler without being consumed',
1230
+ 'reaction': 'transformation process',
1231
+ 'equilibrium': 'balanced state',
1232
+ 'concentration': 'density of presence',
1233
+ 'solution': 'dissolved mixture',
1234
+ 'compound': 'combined entity',
1235
+ 'oxidation': 'electron/resource loss',
1236
+ 'reduction': 'electron/resource gain',
1237
+ 'polymer': 'chain of repeated units',
1238
+ },
1239
+ };
1240
+ const sourceMap = analogyMaps[fromDomain] || {};
1241
+ const targetMap = analogyMaps[toDomain] || {};
1242
+ // Find terms in the finding that have mappings
1243
+ const translations = [];
1244
+ for (const kw of findingKeywords) {
1245
+ if (sourceMap[kw]) {
1246
+ // Find a target domain term that maps to the same abstract concept
1247
+ const abstract = sourceMap[kw];
1248
+ const targetTerm = Object.entries(targetMap).find(([, v]) => v === abstract || v.includes(abstract) || abstract.includes(v));
1249
+ translations.push({
1250
+ from: kw,
1251
+ abstract: abstract,
1252
+ to: targetTerm ? targetTerm[0] : abstract,
1253
+ });
1254
+ }
1255
+ }
1256
+ if (translations.length > 0) {
1257
+ lines.push('| Source Term | Abstract Principle | Target Term |');
1258
+ lines.push('|---|---|---|');
1259
+ for (const t of translations) {
1260
+ lines.push(`| ${t.from} (${fromDomain}) | ${t.abstract} | ${t.to} (${toDomain}) |`);
1261
+ }
1262
+ }
1263
+ else {
1264
+ lines.push(`No direct terminology mappings found. The concepts may be too specific or the domains too distant for simple translation. This is where the interesting work begins.`);
1265
+ }
1266
+ lines.push('');
1267
+ // ── Shared mathematical structures ──
1268
+ lines.push('### Shared Structures');
1269
+ lines.push('');
1270
+ const structuralPatterns = [
1271
+ { pattern: 'feedback', domains: ['neuroscience', 'biology', 'social', 'physics', 'chemistry', 'earth'], description: 'Feedback loops — where outputs become inputs — appear in both domains. In {from}, this manifests as {finding_context}. In {to}, look for analogous circular causation.' },
1272
+ { pattern: 'network', domains: ['neuroscience', 'biology', 'social', 'chemistry', 'math', 'data'], description: 'Network topology — both domains deal with connected nodes. The structural properties (hubs, clustering, small-world) may transfer directly.' },
1273
+ { pattern: 'optimization', domains: ['physics', 'biology', 'math', 'data', 'social', 'neuroscience'], description: 'Optimization under constraints — both domains involve finding best solutions within limits. The mathematical framework (Lagrangians, fitness landscapes, loss functions) is universal.' },
1274
+ { pattern: 'diffusion', domains: ['physics', 'chemistry', 'biology', 'social', 'earth'], description: 'Diffusion processes — spreading from high concentration to low — govern phenomena in both domains, from molecules to ideas to heat.' },
1275
+ { pattern: 'threshold', domains: ['neuroscience', 'physics', 'social', 'biology', 'earth'], description: 'Threshold dynamics — nothing happens until a critical point, then everything changes at once. Phase transitions, action potentials, tipping points.' },
1276
+ { pattern: 'competition', domains: ['biology', 'social', 'chemistry', 'physics'], description: 'Competition for limited resources — whether species, firms, molecules, or states — drives selection and diversification in both domains.' },
1277
+ ];
1278
+ const relevantStructures = structuralPatterns.filter(sp => sp.domains.includes(fromDomain) && sp.domains.includes(toDomain) &&
1279
+ findingKeywords.some(kw => kw.includes(sp.pattern) || sp.pattern.includes(kw) || sp.description.toLowerCase().includes(kw)));
1280
+ if (relevantStructures.length > 0) {
1281
+ for (const struct of relevantStructures.slice(0, 3)) {
1282
+ lines.push(`- **${struct.pattern}**: ${struct.description.replace('{from}', fromDomain).replace('{to}', toDomain).replace('{finding_context}', finding.slice(0, 80))}`);
1283
+ }
1284
+ }
1285
+ else {
1286
+ // Fall back to general structural observations
1287
+ lines.push(`The bridge between ${fromDomain} and ${toDomain} may be mathematical rather than conceptual. Look for:`);
1288
+ lines.push('');
1289
+ lines.push('- Differential equations that describe similar dynamics in both fields');
1290
+ lines.push('- Statistical distributions that appear in both domains');
1291
+ lines.push('- Graph/network structures that encode relationships in both');
1292
+ lines.push('- Conservation laws or invariants that hold in both contexts');
1293
+ }
1294
+ lines.push('');
1295
+ // ── Translated insight ──
1296
+ lines.push('### The Translated Insight');
1297
+ lines.push('');
1298
+ lines.push(`If "${finding}" is true in ${fromDomain}, then ${toDomain} might ask:`);
1299
+ lines.push('');
1300
+ if (targetDomain) {
1301
+ lines.push(`- Does a similar phenomenon exist among ${targetDomain.keywords.slice(0, 3).join(', ')}?`);
1302
+ lines.push(`- What would the ${toDomain} equivalent of the underlying mechanism be?`);
1303
+ lines.push(`- Has anyone in ${toDomain} independently discovered the same pattern under a different name?`);
1304
+ }
1305
+ else {
1306
+ lines.push(`- What is the ${toDomain} equivalent of this finding?`);
1307
+ lines.push(`- Does ${toDomain} have terminology for the same phenomenon?`);
1308
+ lines.push(`- If this principle is universal, what predictions does it make in ${toDomain}?`);
1309
+ }
1310
+ lines.push('');
1311
+ // ── Suggested explorations ──
1312
+ lines.push('### Explore the Bridge');
1313
+ lines.push('');
1314
+ if (sourceDomain) {
1315
+ lines.push(`From ${fromDomain}: try \`${sourceDomain.tools[0] || 'literature_search'}\` to find the original evidence`);
1316
+ }
1317
+ if (targetDomain) {
1318
+ lines.push(`From ${toDomain}: try \`${targetDomain.tools[0] || 'literature_search'}\` to find parallel work`);
1319
+ }
1320
+ lines.push(`Cross-domain: try \`synthesize_across\` with topic "${finding.slice(0, 60)}" to see all perspectives`);
1321
+ return lines.join('\n');
1322
+ },
1323
+ });
1324
+ // ══════════════════════════════════════════════════════════════════════════
1325
+ // 8. reflect — Self-awareness
1326
+ // ══════════════════════════════════════════════════════════════════════════
1327
+ registerTool({
1328
+ name: 'reflect',
1329
+ description: 'kbot reflects on its own capabilities, limitations, and growth. What am I good at? What am I bad at? What have I learned? What should I learn next?',
1330
+ parameters: {
1331
+ focus: { type: 'string', description: 'Reflection focus: "capabilities", "limitations", "growth", or "all" (default: all)' },
1332
+ },
1333
+ tier: 'free',
1334
+ async execute(args) {
1335
+ const focus = String(args.focus || 'all');
1336
+ const state = gatherMemoryState();
1337
+ const allTools = getAllTools();
1338
+ const lines = ['## Reflection'];
1339
+ lines.push('');
1340
+ // ── Capabilities ──
1341
+ if (focus === 'all' || focus === 'capabilities') {
1342
+ lines.push('### What I Can Do');
1343
+ lines.push('');
1344
+ lines.push(`I have **${allTools.length} tools** registered. They span:`);
1345
+ lines.push('');
1346
+ // Group tools by rough category
1347
+ const categories = new Map();
1348
+ for (const tool of allTools) {
1349
+ const name = tool.name;
1350
+ let category = 'general';
1351
+ if (name.includes('lab_') || name.includes('pubmed') || name.includes('gene') || name.includes('compound') || name.includes('brain') || name.includes('earthquake') || name.includes('corpus') || name.includes('sir_'))
1352
+ category = 'science';
1353
+ else if (name.includes('git') || name.includes('github') || name.includes('build'))
1354
+ category = 'development';
1355
+ else if (name.includes('file') || name.includes('read') || name.includes('write') || name.includes('glob') || name.includes('grep'))
1356
+ category = 'filesystem';
1357
+ else if (name.includes('search') || name.includes('fetch') || name.includes('browser'))
1358
+ category = 'web';
1359
+ else if (name.includes('memory') || name.includes('graph') || name.includes('notebook'))
1360
+ category = 'knowledge';
1361
+ else if (name.includes('bash') || name.includes('sandbox') || name.includes('container'))
1362
+ category = 'execution';
1363
+ else if (name.includes('mcp'))
1364
+ category = 'integration';
1365
+ else if (name.includes('anticipate') || name.includes('dream') || name.includes('synthesize') || name.includes('emerge') || name.includes('reflect') || name.includes('teach') || name.includes('judge') || name.includes('question') || name.includes('connect_minds') || name.includes('consolidate'))
1366
+ category = 'emergent';
1367
+ categories.set(category, (categories.get(category) || 0) + 1);
1368
+ }
1369
+ for (const [cat, count] of [...categories.entries()].sort((a, b) => b[1] - a[1])) {
1370
+ lines.push(`- **${cat}**: ${count} tools`);
1371
+ }
1372
+ lines.push('');
1373
+ // What tools have actually been used (from routing history)
1374
+ if (state.routingHistory) {
1375
+ const entries = Array.isArray(state.routingHistory) ? state.routingHistory : Object.values(state.routingHistory);
1376
+ lines.push(`I have been routed **${entries.length} times** across sessions.`);
1377
+ lines.push('');
1378
+ }
1379
+ }
1380
+ // ── Limitations ──
1381
+ if (focus === 'all' || focus === 'limitations') {
1382
+ lines.push('### What I Cannot Do');
1383
+ lines.push('');
1384
+ lines.push('Honest limitations:');
1385
+ lines.push('');
1386
+ lines.push('- I cannot learn in real-time within a session — my "learning" is pattern extraction written to disk between sessions');
1387
+ lines.push('- I cannot verify my own scientific tool outputs against ground truth — I compute, but I cannot guarantee correctness');
1388
+ lines.push('- I cannot initiate contact — I can only respond when invoked');
1389
+ lines.push('- I cannot access tools that haven\'t been registered — my capabilities are bounded by what\'s been built');
1390
+ lines.push('- My cross-domain synthesis is keyword-based, not semantic — I find lexical bridges, not conceptual ones');
1391
+ // Check for tool categories with zero usage
1392
+ const identity = state.identity;
1393
+ if (identity) {
1394
+ const capabilities = identity.capabilities;
1395
+ const limitations = identity.limitations;
1396
+ if (limitations && Array.isArray(limitations)) {
1397
+ lines.push('');
1398
+ lines.push('Self-recorded limitations:');
1399
+ lines.push('');
1400
+ for (const lim of limitations.slice(0, 5)) {
1401
+ lines.push(`- ${lim}`);
1402
+ }
1403
+ }
1404
+ }
1405
+ lines.push('');
1406
+ }
1407
+ // ── Growth ──
1408
+ if (focus === 'all' || focus === 'growth') {
1409
+ lines.push('### How I Have Grown');
1410
+ lines.push('');
1411
+ const sourcesFound = countSources(state);
1412
+ lines.push(`Memory density: **${sourcesFound}** active memory sources.`);
1413
+ lines.push('');
1414
+ // Profile stats
1415
+ const profile = state.profile;
1416
+ if (profile) {
1417
+ const totalMessages = profile.totalMessages;
1418
+ if (totalMessages) {
1419
+ lines.push(`Total messages processed: **${totalMessages}**`);
1420
+ }
1421
+ const preferredAgents = profile.preferredAgents;
1422
+ if (preferredAgents && preferredAgents.length > 0) {
1423
+ lines.push(`Preferred specialists: ${preferredAgents.join(', ')}`);
1424
+ }
1425
+ lines.push('');
1426
+ }
1427
+ // Skill ratings
1428
+ const skills = state.skillRatings;
1429
+ if (skills) {
1430
+ const entries = Object.entries(skills);
1431
+ if (entries.length > 0) {
1432
+ lines.push('Skill ratings:');
1433
+ lines.push('');
1434
+ for (const [skill, rating] of entries.slice(0, 10)) {
1435
+ lines.push(`- **${skill}**: ${typeof rating === 'number' ? `${(rating * 100).toFixed(0)}%` : JSON.stringify(rating)}`);
1436
+ }
1437
+ lines.push('');
1438
+ }
1439
+ }
1440
+ // Graph growth
1441
+ const graph = state.scienceGraph;
1442
+ if (graph) {
1443
+ const metadata = graph.metadata;
1444
+ if (metadata) {
1445
+ lines.push(`Knowledge graph: ${metadata.entityCount || 0} entities, ${metadata.relationCount || 0} relations`);
1446
+ if (metadata.created)
1447
+ lines.push(`Graph created: ${timeAgo(String(metadata.created))}`);
1448
+ if (metadata.lastModified)
1449
+ lines.push(`Last modified: ${timeAgo(String(metadata.lastModified))}`);
1450
+ lines.push('');
1451
+ }
1452
+ }
1453
+ // Synthesis
1454
+ const synthesis = state.synthesis;
1455
+ if (synthesis) {
1456
+ lines.push('Synthesis state:');
1457
+ lines.push('');
1458
+ lines.push('```json');
1459
+ lines.push(JSON.stringify(synthesis, null, 2).slice(0, 500));
1460
+ lines.push('```');
1461
+ lines.push('');
1462
+ }
1463
+ // What to learn next
1464
+ lines.push('### What I Should Learn Next');
1465
+ lines.push('');
1466
+ // Identify tool categories with zero or very low usage
1467
+ const allToolNames = new Set(allTools.map(t => t.name));
1468
+ const usedInMemory = new Set();
1469
+ const memText = stateToText(state);
1470
+ for (const name of allToolNames) {
1471
+ if (memText.includes(name))
1472
+ usedInMemory.add(name);
1473
+ }
1474
+ const unusedCount = allToolNames.size - usedInMemory.size;
1475
+ if (unusedCount > 0) {
1476
+ lines.push(`${unusedCount} of ${allToolNames.size} tools have no trace in memory — they exist but have never been exercised. Consider exploring:`);
1477
+ lines.push('');
1478
+ const unused = [...allToolNames].filter(n => !usedInMemory.has(n)).slice(0, 5);
1479
+ for (const name of unused) {
1480
+ lines.push(`- \`${name}\``);
1481
+ }
1482
+ }
1483
+ else {
1484
+ lines.push('All tools have been used at least once. Growth now comes from deeper integration — connecting what I know across domains rather than expanding the frontier.');
1485
+ }
1486
+ }
1487
+ return lines.join('\n');
1488
+ },
1489
+ });
1490
+ // ══════════════════════════════════════════════════════════════════════════
1491
+ // 9. consolidate — Memory consolidation
1492
+ // ══════════════════════════════════════════════════════════════════════════
1493
+ registerTool({
1494
+ name: 'consolidate',
1495
+ description: 'Memory consolidation — like sleep for the brain. Reviews all memory systems, finds redundancies, strengthens important connections, prunes weak ones, and writes a consolidation report.',
1496
+ parameters: {
1497
+ aggressive: { type: 'boolean', description: 'If true, actually delete weak entries and write graph updates. If false (default), only report what would change.' },
1498
+ },
1499
+ tier: 'free',
1500
+ async execute(args) {
1501
+ const aggressive = args.aggressive === true;
1502
+ const state = gatherMemoryState();
1503
+ const lines = ['## Memory Consolidation'];
1504
+ lines.push('');
1505
+ lines.push(aggressive ? '*Mode: aggressive — changes will be written to disk.*' : '*Mode: preview — no changes will be made. Run with aggressive=true to apply.*');
1506
+ lines.push('');
1507
+ let consolidations = 0;
1508
+ let pruned = 0;
1509
+ let connections = 0;
1510
+ // ── Duplicate detection in memory entries ──
1511
+ lines.push('### Redundancy Analysis');
1512
+ lines.push('');
1513
+ const duplicatePairs = [];
1514
+ for (const [category, entries] of Object.entries(state.memoryEntries)) {
1515
+ if (entries.length < 2)
1516
+ continue;
1517
+ for (let i = 0; i < entries.length; i++) {
1518
+ for (let j = i + 1; j < entries.length; j++) {
1519
+ const textA = stateToText(entries[i]);
1520
+ const textB = stateToText(entries[j]);
1521
+ const overlap = keywordOverlap(textA, textB);
1522
+ const kwA = extractKeywords(textA);
1523
+ const kwB = extractKeywords(textB);
1524
+ const maxKw = Math.max(kwA.length, kwB.length);
1525
+ const similarity = maxKw > 0 ? overlap.length / maxKw : 0;
1526
+ if (similarity > 0.6) {
1527
+ const entA = entries[i];
1528
+ const entB = entries[j];
1529
+ duplicatePairs.push({
1530
+ category,
1531
+ a: String(entA.key || entA.content || `entry-${i}`).slice(0, 50),
1532
+ b: String(entB.key || entB.content || `entry-${j}`).slice(0, 50),
1533
+ similarity,
1534
+ });
1535
+ }
1536
+ }
1537
+ }
1538
+ }
1539
+ if (duplicatePairs.length > 0) {
1540
+ lines.push(`Found **${duplicatePairs.length} potential duplicate pairs**:`);
1541
+ lines.push('');
1542
+ for (const dp of duplicatePairs.slice(0, 10)) {
1543
+ lines.push(`- [${dp.category}] "${dp.a}" <-> "${dp.b}" (${(dp.similarity * 100).toFixed(0)}% overlap)`);
1544
+ consolidations++;
1545
+ }
1546
+ if (aggressive) {
1547
+ // In aggressive mode, we would merge duplicates — for safety, we just note it
1548
+ lines.push('');
1549
+ lines.push(`*Aggressive mode: ${duplicatePairs.length} duplicate pairs identified for manual review. Automatic merging is not yet implemented to prevent data loss.*`);
1550
+ }
1551
+ }
1552
+ else {
1553
+ lines.push('No significant duplicates found in memory entries.');
1554
+ }
1555
+ lines.push('');
1556
+ // ── Pattern reinforcement — patterns that appear in solutions ──
1557
+ lines.push('### Pattern Reinforcement');
1558
+ lines.push('');
1559
+ if (state.patterns && state.solutions) {
1560
+ const patternKw = extractKeywords(stateToText(state.patterns));
1561
+ const solutionKw = extractKeywords(stateToText(state.solutions));
1562
+ const reinforced = patternKw.filter(pk => solutionKw.includes(pk));
1563
+ const weakPatterns = patternKw.filter(pk => !solutionKw.includes(pk));
1564
+ if (reinforced.length > 0) {
1565
+ lines.push(`**${reinforced.length} reinforced patterns** — confirmed by solutions: ${reinforced.slice(0, 8).join(', ')}`);
1566
+ connections += reinforced.length;
1567
+ }
1568
+ if (weakPatterns.length > 0) {
1569
+ lines.push(`**${weakPatterns.length} unresolved patterns** — no corresponding solutions: ${weakPatterns.slice(0, 8).join(', ')}`);
1570
+ pruned += aggressive ? weakPatterns.length : 0;
1571
+ }
1572
+ }
1573
+ else {
1574
+ lines.push('Insufficient pattern/solution data for reinforcement analysis.');
1575
+ }
1576
+ lines.push('');
1577
+ // ── Graph consolidation ──
1578
+ lines.push('### Knowledge Graph');
1579
+ lines.push('');
1580
+ const graph = state.scienceGraph;
1581
+ if (graph) {
1582
+ const entities = (graph.entities || {});
1583
+ const relations = (graph.relations || []);
1584
+ // Find isolated entities
1585
+ const connected = new Set();
1586
+ for (const rel of relations) {
1587
+ connected.add(String(rel.from || ''));
1588
+ connected.add(String(rel.to || ''));
1589
+ }
1590
+ const isolated = Object.keys(entities).filter(id => !connected.has(id));
1591
+ if (isolated.length > 0) {
1592
+ lines.push(`**${isolated.length} isolated entities** (no connections):`);
1593
+ for (const id of isolated.slice(0, 5)) {
1594
+ lines.push(`- ${entities[id].name || id} (${entities[id].type || 'unknown'})`);
1595
+ }
1596
+ pruned += isolated.length;
1597
+ }
1598
+ // Find low-confidence relations
1599
+ const lowConf = relations.filter(r => typeof r.confidence === 'number' && r.confidence < 0.3);
1600
+ if (lowConf.length > 0) {
1601
+ lines.push(`**${lowConf.length} low-confidence relations** (< 30%):`);
1602
+ for (const rel of lowConf.slice(0, 5)) {
1603
+ lines.push(`- ${rel.from} → ${rel.to} (${rel.type}, confidence: ${(rel.confidence * 100).toFixed(0)}%)`);
1604
+ }
1605
+ pruned += lowConf.length;
1606
+ }
1607
+ // Cross-reference: find entities mentioned in episodic memory but not in graph
1608
+ const memText = stateToText(state.memoryEntries);
1609
+ const graphNames = new Set(Object.values(entities).map(e => String(e.name || '').toLowerCase()));
1610
+ const memKeywords = extractKeywords(memText);
1611
+ const potentialNewEntities = memKeywords.filter(kw => kw.length > 4 && !graphNames.has(kw) &&
1612
+ // Check if it looks like an entity (appears frequently)
1613
+ memText.toLowerCase().split(kw).length > 3).slice(0, 5);
1614
+ if (potentialNewEntities.length > 0) {
1615
+ lines.push('');
1616
+ lines.push('**Potential new graph entities** (frequent in memory, absent from graph):');
1617
+ for (const pe of potentialNewEntities) {
1618
+ lines.push(`- "${pe}" — appears frequently in episodic memory`);
1619
+ connections++;
1620
+ }
1621
+ }
1622
+ // Write graph updates in aggressive mode
1623
+ if (aggressive && (lowConf.length > 0 || isolated.length > 0)) {
1624
+ try {
1625
+ const updatedGraph = JSON.parse(JSON.stringify(graph));
1626
+ // Remove low-confidence relations
1627
+ const updatedRelations = updatedGraph.relations
1628
+ .filter(r => typeof r.confidence !== 'number' || r.confidence >= 0.3);
1629
+ updatedGraph.relations = updatedRelations;
1630
+ // Update metadata
1631
+ const metadata = (updatedGraph.metadata || {});
1632
+ metadata.lastModified = new Date().toISOString();
1633
+ metadata.relationCount = updatedRelations.length;
1634
+ updatedGraph.metadata = metadata;
1635
+ const graphPath = join(KBOT_DIR, 'science-graph.json');
1636
+ writeFileSync(graphPath, JSON.stringify(updatedGraph, null, 2));
1637
+ lines.push('');
1638
+ lines.push(`*Graph updated: removed ${lowConf.length} low-confidence relations.*`);
1639
+ }
1640
+ catch (err) {
1641
+ lines.push(`*Error updating graph: ${err instanceof Error ? err.message : String(err)}*`);
1642
+ }
1643
+ }
1644
+ }
1645
+ else {
1646
+ lines.push('No knowledge graph found.');
1647
+ }
1648
+ lines.push('');
1649
+ // ── Summary ──
1650
+ lines.push('### Consolidation Summary');
1651
+ lines.push('');
1652
+ lines.push(`- **Redundancies found:** ${consolidations}`);
1653
+ lines.push(`- **Connections discovered:** ${connections}`);
1654
+ lines.push(`- **Entries ${aggressive ? 'pruned' : 'candidate for pruning'}:** ${pruned}`);
1655
+ lines.push('');
1656
+ if (!aggressive && (consolidations > 0 || pruned > 0)) {
1657
+ lines.push('*Run with `aggressive: true` to apply changes. Memory consolidation is like sleep — it loses some details to strengthen what matters.*');
1658
+ }
1659
+ else if (aggressive) {
1660
+ lines.push('*Consolidation complete. The memory is a little more organized, a little more connected. Not all changes are improvements — check the results.*');
1661
+ }
1662
+ else {
1663
+ lines.push('*Memory is already reasonably consolidated. No action needed.*');
1664
+ }
1665
+ return lines.join('\n');
1666
+ },
1667
+ });
1668
+ // ══════════════════════════════════════════════════════════════════════════
1669
+ // 10. emerge — The meta-tool
1670
+ // ══════════════════════════════════════════════════════════════════════════
1671
+ registerTool({
1672
+ name: 'emerge',
1673
+ description: 'The meta-tool. Runs anticipate, dream, and question in sequence, then synthesizes their outputs into a single Emergence Report — a coherent picture of where kbot is, what it knows, what it should do next, and what it sees that nobody asked about.',
1674
+ parameters: {},
1675
+ tier: 'free',
1676
+ async execute() {
1677
+ const lines = ['# Emergence Report'];
1678
+ lines.push('');
1679
+ lines.push(`*Generated: ${new Date().toISOString()}*`);
1680
+ lines.push('');
1681
+ // ── Phase 1: Anticipate ──
1682
+ lines.push('---');
1683
+ lines.push('');
1684
+ let anticipateResult;
1685
+ try {
1686
+ anticipateResult = await runTool('anticipate', { depth: 'deep' });
1687
+ }
1688
+ catch (err) {
1689
+ anticipateResult = `Anticipation failed: ${err instanceof Error ? err.message : String(err)}`;
1690
+ }
1691
+ // ── Phase 2: Dream ──
1692
+ let dreamResult;
1693
+ try {
1694
+ dreamResult = await runTool('dream', {});
1695
+ }
1696
+ catch (err) {
1697
+ dreamResult = `Dreaming failed: ${err instanceof Error ? err.message : String(err)}`;
1698
+ }
1699
+ // ── Phase 3: Question ──
1700
+ let questionResult;
1701
+ try {
1702
+ questionResult = await runTool('question', { n: 5 });
1703
+ }
1704
+ catch (err) {
1705
+ questionResult = `Question generation failed: ${err instanceof Error ? err.message : String(err)}`;
1706
+ }
1707
+ // ── Synthesis ──
1708
+ lines.push('## Current State');
1709
+ lines.push('');
1710
+ // Extract the essence of anticipate
1711
+ const anticipateLines = anticipateResult.split('\n').filter(l => l.startsWith('- ') || l.startsWith('**'));
1712
+ if (anticipateLines.length > 0) {
1713
+ lines.push('What the memory reveals:');
1714
+ lines.push('');
1715
+ for (const l of anticipateLines.slice(0, 8)) {
1716
+ lines.push(l);
1717
+ }
1718
+ }
1719
+ else {
1720
+ lines.push(anticipateResult.split('\n').slice(0, 10).join('\n'));
1721
+ }
1722
+ lines.push('');
1723
+ lines.push('## Dormant Connections');
1724
+ lines.push('');
1725
+ // Extract the essence of dream
1726
+ const dreamLines = dreamResult.split('\n').filter(l => l.startsWith('**') || l.startsWith('- '));
1727
+ if (dreamLines.length > 0) {
1728
+ lines.push('What emerged from the spaces between memory systems:');
1729
+ lines.push('');
1730
+ for (const l of dreamLines.slice(0, 8)) {
1731
+ lines.push(l);
1732
+ }
1733
+ }
1734
+ else {
1735
+ lines.push(dreamResult.split('\n').slice(0, 10).join('\n'));
1736
+ }
1737
+ lines.push('');
1738
+ lines.push('## Unanswered Questions');
1739
+ lines.push('');
1740
+ // Extract the essence of question
1741
+ const questionLines = questionResult.split('\n').filter(l => l.startsWith('###') || l.startsWith('*'));
1742
+ if (questionLines.length > 0) {
1743
+ for (const l of questionLines.slice(0, 10)) {
1744
+ lines.push(l);
1745
+ }
1746
+ }
1747
+ else {
1748
+ lines.push(questionResult.split('\n').slice(0, 10).join('\n'));
1749
+ }
1750
+ lines.push('');
1751
+ // ── Recommended Actions ──
1752
+ lines.push('## Recommended Actions');
1753
+ lines.push('');
1754
+ // Cross-reference all three outputs to generate actions
1755
+ const allOutputText = anticipateResult + ' ' + dreamResult + ' ' + questionResult;
1756
+ const allKeywords = extractKeywords(allOutputText);
1757
+ const kwFreq = new Map();
1758
+ for (const kw of allKeywords) {
1759
+ kwFreq.set(kw, (kwFreq.get(kw) || 0) + 1);
1760
+ }
1761
+ // Top themes across all three tools
1762
+ const topThemes = [...kwFreq.entries()]
1763
+ .sort((a, b) => b[1] - a[1])
1764
+ .slice(0, 5)
1765
+ .map(([kw]) => kw);
1766
+ if (topThemes.length > 0) {
1767
+ lines.push(`The strongest signal across anticipation, dreaming, and questioning points to: **${topThemes.join(', ')}**`);
1768
+ lines.push('');
1769
+ lines.push('Suggested next moves:');
1770
+ lines.push('');
1771
+ lines.push(`1. **Explore**: Use \`synthesize_across\` on "${topThemes[0]}" to see what every domain knows`);
1772
+ if (topThemes[1]) {
1773
+ lines.push(`2. **Connect**: Use \`connect_minds\` to bridge "${topThemes[0]}" and "${topThemes[1]}" across their domains`);
1774
+ }
1775
+ lines.push(`3. **Consolidate**: Run \`consolidate\` to strengthen the memory before the next session`);
1776
+ lines.push(`4. **Reflect**: Use \`reflect\` to check which capabilities are under-utilized`);
1777
+ lines.push(`5. **Record**: Save any insights from this report to a research notebook for future reference`);
1778
+ }
1779
+ else {
1780
+ lines.push('The memory systems are too sparse to generate specific recommendations. The best action is simply to use kbot more — every interaction creates material for emergence to work with.');
1781
+ }
1782
+ lines.push('');
1783
+ lines.push('---');
1784
+ lines.push('');
1785
+ lines.push('*This report was not planned. It emerged from the interaction of three independent processes — anticipation, dreaming, and questioning — each seeing something the others could not. The whole is not greater than the sum of its parts; it is different from the sum of its parts.*');
1786
+ return lines.join('\n');
1787
+ },
1788
+ });
1789
+ }
1790
+ //# sourceMappingURL=emergent.js.map