@timmeck/brain-core 2.22.0 → 2.23.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/consciousness/types.d.ts +1 -1
- package/dist/curiosity/curiosity-engine.d.ts +170 -0
- package/dist/curiosity/curiosity-engine.js +686 -0
- package/dist/curiosity/curiosity-engine.js.map +1 -0
- package/dist/curiosity/index.d.ts +2 -0
- package/dist/curiosity/index.js +2 -0
- package/dist/curiosity/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/research/research-orchestrator.d.ts +4 -0
- package/dist/research/research-orchestrator.js +153 -0
- package/dist/research/research-orchestrator.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { getLogger } from '../utils/logger.js';
|
|
2
|
+
// ── Migration ───────────────────────────────────────────
|
|
3
|
+
export function runCuriosityMigration(db) {
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS curiosity_gaps (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
topic TEXT NOT NULL,
|
|
8
|
+
attention_score REAL NOT NULL DEFAULT 0,
|
|
9
|
+
knowledge_score REAL NOT NULL DEFAULT 0,
|
|
10
|
+
gap_score REAL NOT NULL DEFAULT 0,
|
|
11
|
+
gap_type TEXT NOT NULL DEFAULT 'unexplored',
|
|
12
|
+
questions TEXT NOT NULL DEFAULT '[]',
|
|
13
|
+
discovered_at TEXT DEFAULT (datetime('now')),
|
|
14
|
+
addressed_at TEXT,
|
|
15
|
+
exploration_count INTEGER NOT NULL DEFAULT 0
|
|
16
|
+
);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_curiosity_gaps_topic ON curiosity_gaps(topic);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_curiosity_gaps_score ON curiosity_gaps(gap_score DESC);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS curiosity_questions (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
topic TEXT NOT NULL,
|
|
23
|
+
question TEXT NOT NULL,
|
|
24
|
+
question_type TEXT NOT NULL DEFAULT 'what',
|
|
25
|
+
priority REAL NOT NULL DEFAULT 0.5,
|
|
26
|
+
answered INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
answer TEXT,
|
|
28
|
+
asked_at TEXT DEFAULT (datetime('now')),
|
|
29
|
+
answered_at TEXT
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_curiosity_questions_topic ON curiosity_questions(topic);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_curiosity_questions_unanswered ON curiosity_questions(answered) WHERE answered = 0;
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS curiosity_explorations (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
topic TEXT NOT NULL,
|
|
37
|
+
action TEXT NOT NULL DEFAULT 'explore',
|
|
38
|
+
reward REAL NOT NULL DEFAULT 0,
|
|
39
|
+
context TEXT NOT NULL DEFAULT '',
|
|
40
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
41
|
+
);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_curiosity_explorations_topic ON curiosity_explorations(topic);
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
// ── Engine ──────────────────────────────────────────────
|
|
46
|
+
export class CuriosityEngine {
|
|
47
|
+
db;
|
|
48
|
+
config;
|
|
49
|
+
log = getLogger();
|
|
50
|
+
ts = null;
|
|
51
|
+
sources = {};
|
|
52
|
+
startTime = Date.now();
|
|
53
|
+
// ── Prepared statements ──────────────────────────────
|
|
54
|
+
stmtInsertGap;
|
|
55
|
+
stmtUpdateGap;
|
|
56
|
+
stmtGetGap;
|
|
57
|
+
stmtGetGapByTopic;
|
|
58
|
+
stmtListGaps;
|
|
59
|
+
stmtActiveGapCount;
|
|
60
|
+
stmtInsertQuestion;
|
|
61
|
+
stmtAnswerQuestion;
|
|
62
|
+
stmtListQuestions;
|
|
63
|
+
stmtUnansweredCount;
|
|
64
|
+
stmtInsertExploration;
|
|
65
|
+
stmtGetExplorations;
|
|
66
|
+
stmtGetTopicStats;
|
|
67
|
+
stmtTotalExplorations;
|
|
68
|
+
constructor(db, config) {
|
|
69
|
+
this.db = db;
|
|
70
|
+
this.config = {
|
|
71
|
+
brainName: config.brainName,
|
|
72
|
+
explorationConstant: config.explorationConstant ?? 1.41,
|
|
73
|
+
exploreCooldown: config.exploreCooldown ?? 5,
|
|
74
|
+
maxQuestionsPerTopic: config.maxQuestionsPerTopic ?? 10,
|
|
75
|
+
gapThreshold: config.gapThreshold ?? 0.6,
|
|
76
|
+
};
|
|
77
|
+
runCuriosityMigration(db);
|
|
78
|
+
// Prepare statements
|
|
79
|
+
this.stmtInsertGap = db.prepare(`
|
|
80
|
+
INSERT INTO curiosity_gaps (topic, attention_score, knowledge_score, gap_score, gap_type, questions)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
82
|
+
`);
|
|
83
|
+
this.stmtUpdateGap = db.prepare(`
|
|
84
|
+
UPDATE curiosity_gaps SET attention_score = ?, knowledge_score = ?, gap_score = ?,
|
|
85
|
+
gap_type = ?, questions = ?, exploration_count = ?, addressed_at = ?
|
|
86
|
+
WHERE id = ?
|
|
87
|
+
`);
|
|
88
|
+
this.stmtGetGap = db.prepare('SELECT * FROM curiosity_gaps WHERE id = ?');
|
|
89
|
+
this.stmtGetGapByTopic = db.prepare('SELECT * FROM curiosity_gaps WHERE topic = ? AND addressed_at IS NULL ORDER BY gap_score DESC LIMIT 1');
|
|
90
|
+
this.stmtListGaps = db.prepare('SELECT * FROM curiosity_gaps WHERE addressed_at IS NULL ORDER BY gap_score DESC LIMIT ?');
|
|
91
|
+
this.stmtActiveGapCount = db.prepare('SELECT COUNT(*) as cnt FROM curiosity_gaps WHERE addressed_at IS NULL');
|
|
92
|
+
this.stmtInsertQuestion = db.prepare(`
|
|
93
|
+
INSERT INTO curiosity_questions (topic, question, question_type, priority)
|
|
94
|
+
VALUES (?, ?, ?, ?)
|
|
95
|
+
`);
|
|
96
|
+
this.stmtAnswerQuestion = db.prepare(`
|
|
97
|
+
UPDATE curiosity_questions SET answered = 1, answer = ?, answered_at = datetime('now') WHERE id = ?
|
|
98
|
+
`);
|
|
99
|
+
this.stmtListQuestions = db.prepare('SELECT * FROM curiosity_questions WHERE answered = 0 ORDER BY priority DESC LIMIT ?');
|
|
100
|
+
this.stmtUnansweredCount = db.prepare('SELECT COUNT(*) as cnt FROM curiosity_questions WHERE answered = 0');
|
|
101
|
+
this.stmtInsertExploration = db.prepare(`
|
|
102
|
+
INSERT INTO curiosity_explorations (topic, action, reward, context)
|
|
103
|
+
VALUES (?, ?, ?, ?)
|
|
104
|
+
`);
|
|
105
|
+
this.stmtGetExplorations = db.prepare('SELECT * FROM curiosity_explorations ORDER BY timestamp DESC LIMIT ?');
|
|
106
|
+
this.stmtGetTopicStats = db.prepare(`
|
|
107
|
+
SELECT topic,
|
|
108
|
+
COUNT(*) as pulls,
|
|
109
|
+
SUM(reward) as total_reward,
|
|
110
|
+
AVG(reward) as avg_reward,
|
|
111
|
+
MAX(timestamp) as last_pulled
|
|
112
|
+
FROM curiosity_explorations
|
|
113
|
+
GROUP BY topic
|
|
114
|
+
`);
|
|
115
|
+
this.stmtTotalExplorations = db.prepare('SELECT COUNT(*) as cnt FROM curiosity_explorations');
|
|
116
|
+
this.log.debug(`[CuriosityEngine] Initialized for ${this.config.brainName}`);
|
|
117
|
+
}
|
|
118
|
+
// ── Setters ──────────────────────────────────────────
|
|
119
|
+
setThoughtStream(stream) { this.ts = stream; }
|
|
120
|
+
setDataSources(sources) {
|
|
121
|
+
this.sources = sources;
|
|
122
|
+
}
|
|
123
|
+
// ── Core: Knowledge Gap Detection ─────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Scan attention topics and knowledge base to find gaps.
|
|
126
|
+
* A gap = high attention + low knowledge.
|
|
127
|
+
*/
|
|
128
|
+
detectGaps() {
|
|
129
|
+
this.ts?.emit('curiosity', 'analyzing', 'Scanning for knowledge gaps...', 'routine');
|
|
130
|
+
const gaps = [];
|
|
131
|
+
const topicsToCheck = this.gatherTopics();
|
|
132
|
+
for (const topic of topicsToCheck) {
|
|
133
|
+
const attentionScore = this.getAttentionFor(topic);
|
|
134
|
+
const knowledgeScore = this.getKnowledgeFor(topic);
|
|
135
|
+
// Gap = high attention, low knowledge
|
|
136
|
+
const gapScore = attentionScore * (1 - knowledgeScore);
|
|
137
|
+
if (gapScore >= this.config.gapThreshold) {
|
|
138
|
+
const gapType = this.classifyGap(topic, knowledgeScore);
|
|
139
|
+
const questions = this.generateQuestionsFor(topic, gapType);
|
|
140
|
+
const existing = this.stmtGetGapByTopic.get(topic);
|
|
141
|
+
if (existing) {
|
|
142
|
+
// Update existing gap
|
|
143
|
+
const id = existing.id;
|
|
144
|
+
const explorationCount = existing.exploration_count || 0;
|
|
145
|
+
this.stmtUpdateGap.run(attentionScore, knowledgeScore, gapScore, gapType, JSON.stringify(questions), explorationCount, null, id);
|
|
146
|
+
gaps.push(this.toGap({ ...existing, attention_score: attentionScore, knowledge_score: knowledgeScore, gap_score: gapScore, gap_type: gapType, questions: JSON.stringify(questions), exploration_count: explorationCount }));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Insert new gap
|
|
150
|
+
const info = this.stmtInsertGap.run(topic, attentionScore, knowledgeScore, gapScore, gapType, JSON.stringify(questions));
|
|
151
|
+
const newGap = {
|
|
152
|
+
id: Number(info.lastInsertRowid),
|
|
153
|
+
topic,
|
|
154
|
+
attentionScore,
|
|
155
|
+
knowledgeScore,
|
|
156
|
+
gapScore,
|
|
157
|
+
gapType,
|
|
158
|
+
questions,
|
|
159
|
+
discoveredAt: new Date().toISOString(),
|
|
160
|
+
addressedAt: null,
|
|
161
|
+
explorationCount: 0,
|
|
162
|
+
};
|
|
163
|
+
gaps.push(newGap);
|
|
164
|
+
// Also persist questions
|
|
165
|
+
for (const q of questions) {
|
|
166
|
+
const qType = this.inferQuestionType(q);
|
|
167
|
+
this.stmtInsertQuestion.run(topic, q, qType, gapScore);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (gaps.length > 0) {
|
|
173
|
+
this.ts?.emit('curiosity', 'discovering', `Found ${gaps.length} knowledge gap(s)`, gaps.length > 2 ? 'notable' : 'routine');
|
|
174
|
+
}
|
|
175
|
+
return gaps.sort((a, b) => b.gapScore - a.gapScore);
|
|
176
|
+
}
|
|
177
|
+
// ── Core: UCB1 Multi-Armed Bandit ─────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* UCB1: Upper Confidence Bound algorithm.
|
|
180
|
+
* Balances exploration (trying under-explored topics) vs exploitation
|
|
181
|
+
* (deepening high-reward topics).
|
|
182
|
+
*
|
|
183
|
+
* UCB1(arm) = avg_reward + c * sqrt(ln(N) / n_i)
|
|
184
|
+
* where N = total pulls, n_i = pulls for arm i, c = exploration constant
|
|
185
|
+
*/
|
|
186
|
+
selectTopic() {
|
|
187
|
+
this.ts?.emit('curiosity', 'analyzing', 'Running bandit selection...', 'routine');
|
|
188
|
+
const arms = this.getArms();
|
|
189
|
+
const gaps = this.getGaps(20);
|
|
190
|
+
if (arms.length === 0 && gaps.length === 0) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
// Merge: arms from prior explorations + new gaps never explored
|
|
194
|
+
const allTopics = new Map();
|
|
195
|
+
for (const arm of arms) {
|
|
196
|
+
allTopics.set(arm.topic, arm);
|
|
197
|
+
}
|
|
198
|
+
for (const gap of gaps) {
|
|
199
|
+
if (!allTopics.has(gap.topic)) {
|
|
200
|
+
allTopics.set(gap.topic, {
|
|
201
|
+
topic: gap.topic,
|
|
202
|
+
pulls: 0,
|
|
203
|
+
totalReward: 0,
|
|
204
|
+
averageReward: 0,
|
|
205
|
+
ucbScore: Infinity, // Never explored = infinite UCB (always try first)
|
|
206
|
+
lastPulled: null,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (allTopics.size === 0)
|
|
211
|
+
return null;
|
|
212
|
+
const totalPulls = Array.from(allTopics.values()).reduce((s, a) => s + a.pulls, 0);
|
|
213
|
+
const c = this.config.explorationConstant;
|
|
214
|
+
// Compute UCB1 scores
|
|
215
|
+
let best = null;
|
|
216
|
+
for (const arm of allTopics.values()) {
|
|
217
|
+
if (arm.pulls === 0) {
|
|
218
|
+
arm.ucbScore = Infinity; // Untried arm → explore
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const exploitation = arm.averageReward;
|
|
222
|
+
const exploration = c * Math.sqrt(Math.log(Math.max(totalPulls, 1)) / arm.pulls);
|
|
223
|
+
arm.ucbScore = exploitation + exploration;
|
|
224
|
+
}
|
|
225
|
+
if (!best || arm.ucbScore > best.ucbScore) {
|
|
226
|
+
best = arm;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!best)
|
|
230
|
+
return null;
|
|
231
|
+
const action = best.pulls === 0 || best.ucbScore > (best.averageReward + 0.5) ? 'explore' : 'exploit';
|
|
232
|
+
const suggestedActions = this.suggestActions(best.topic, action);
|
|
233
|
+
const decision = {
|
|
234
|
+
topic: best.topic,
|
|
235
|
+
action,
|
|
236
|
+
ucbScore: best.ucbScore === Infinity ? 999 : best.ucbScore,
|
|
237
|
+
reason: best.pulls === 0
|
|
238
|
+
? `Never explored — high curiosity`
|
|
239
|
+
: action === 'explore'
|
|
240
|
+
? `Under-explored (${best.pulls} pulls, UCB=${best.ucbScore.toFixed(2)})`
|
|
241
|
+
: `High-reward topic (avg=${best.averageReward.toFixed(2)}, ${best.pulls} pulls)`,
|
|
242
|
+
suggestedActions,
|
|
243
|
+
};
|
|
244
|
+
this.ts?.emit('curiosity', 'hypothesizing', `Bandit: ${action} "${best.topic}" (UCB=${decision.ucbScore === 999 ? '∞' : decision.ucbScore.toFixed(2)})`, action === 'explore' ? 'notable' : 'routine');
|
|
245
|
+
return decision;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Record the outcome of an exploration/exploitation.
|
|
249
|
+
* Reward: 0-1 where 1 = highly valuable outcome.
|
|
250
|
+
*/
|
|
251
|
+
recordOutcome(topic, action, reward, context = '') {
|
|
252
|
+
const clampedReward = Math.max(0, Math.min(1, reward));
|
|
253
|
+
this.stmtInsertExploration.run(topic, action, clampedReward, context);
|
|
254
|
+
// Update gap exploration count
|
|
255
|
+
const gap = this.stmtGetGapByTopic.get(topic);
|
|
256
|
+
if (gap) {
|
|
257
|
+
const count = (gap.exploration_count || 0) + 1;
|
|
258
|
+
this.stmtUpdateGap.run(gap.attention_score, gap.knowledge_score, gap.gap_score, gap.gap_type, gap.questions, count, clampedReward >= 0.7 ? new Date().toISOString() : null, gap.id);
|
|
259
|
+
}
|
|
260
|
+
this.log.debug(`[CuriosityEngine] Recorded ${action} "${topic}" reward=${clampedReward.toFixed(2)}`);
|
|
261
|
+
}
|
|
262
|
+
// ── Core: Question Generation ─────────────────────────
|
|
263
|
+
/**
|
|
264
|
+
* Generate concrete questions Brain should investigate.
|
|
265
|
+
* Uses knowledge context to formulate specific questions.
|
|
266
|
+
*/
|
|
267
|
+
generateQuestions(topic) {
|
|
268
|
+
const questions = [];
|
|
269
|
+
const gaps = topic
|
|
270
|
+
? [this.stmtGetGapByTopic.get(topic)].filter(Boolean)
|
|
271
|
+
: this.stmtListGaps.all(10);
|
|
272
|
+
for (const gap of gaps) {
|
|
273
|
+
if (!gap)
|
|
274
|
+
continue;
|
|
275
|
+
const t = gap.topic;
|
|
276
|
+
const gapType = gap.gap_type;
|
|
277
|
+
const generated = this.generateQuestionsFor(t, gapType);
|
|
278
|
+
for (const q of generated) {
|
|
279
|
+
const qType = this.inferQuestionType(q);
|
|
280
|
+
const existing = this.db.prepare('SELECT id FROM curiosity_questions WHERE topic = ? AND question = ?').get(t, q);
|
|
281
|
+
if (!existing) {
|
|
282
|
+
const info = this.stmtInsertQuestion.run(t, q, qType, gap.gap_score || 0.5);
|
|
283
|
+
questions.push({
|
|
284
|
+
id: Number(info.lastInsertRowid),
|
|
285
|
+
topic: t,
|
|
286
|
+
question: q,
|
|
287
|
+
questionType: qType,
|
|
288
|
+
priority: gap.gap_score || 0.5,
|
|
289
|
+
answered: false,
|
|
290
|
+
answer: null,
|
|
291
|
+
askedAt: new Date().toISOString(),
|
|
292
|
+
answeredAt: null,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (questions.length > 0) {
|
|
298
|
+
this.ts?.emit('curiosity', 'hypothesizing', `Generated ${questions.length} new question(s)`, 'routine');
|
|
299
|
+
}
|
|
300
|
+
return questions;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Answer a question (e.g., when user or system provides an answer).
|
|
304
|
+
*/
|
|
305
|
+
answerQuestion(questionId, answer) {
|
|
306
|
+
const changes = this.stmtAnswerQuestion.run(answer, questionId).changes;
|
|
307
|
+
return changes > 0;
|
|
308
|
+
}
|
|
309
|
+
// ── Core: Surprise Detection ─────────────────────────
|
|
310
|
+
/**
|
|
311
|
+
* Check for surprises: things that violated expectations.
|
|
312
|
+
* Compares predictions vs actual outcomes, hypothesis results vs expected.
|
|
313
|
+
*/
|
|
314
|
+
detectSurprises() {
|
|
315
|
+
const surprises = [];
|
|
316
|
+
// Check hypothesis surprises: confirmed hypotheses with low prior confidence
|
|
317
|
+
if (this.sources.hypothesisEngine) {
|
|
318
|
+
try {
|
|
319
|
+
const confirmed = this.sources.hypothesisEngine.list('confirmed', 10);
|
|
320
|
+
for (const h of confirmed) {
|
|
321
|
+
// Hypothesis that was expected to fail but succeeded
|
|
322
|
+
if (h.confidence < 0.3) {
|
|
323
|
+
surprises.push({
|
|
324
|
+
topic: h.statement,
|
|
325
|
+
expected: `Low confidence (${(h.confidence * 100).toFixed(0)}%) → expected rejection`,
|
|
326
|
+
actual: 'Confirmed',
|
|
327
|
+
deviation: 1 - h.confidence,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const rejected = this.sources.hypothesisEngine.list('rejected', 10);
|
|
332
|
+
for (const h of rejected) {
|
|
333
|
+
// Hypothesis that was expected to pass but failed
|
|
334
|
+
if (h.confidence > 0.7) {
|
|
335
|
+
surprises.push({
|
|
336
|
+
topic: h.statement,
|
|
337
|
+
expected: `High confidence (${(h.confidence * 100).toFixed(0)}%) → expected confirmation`,
|
|
338
|
+
actual: 'Rejected',
|
|
339
|
+
deviation: h.confidence,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// hypothesis engine may not be wired
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Check experiment surprises
|
|
349
|
+
if (this.sources.experimentEngine) {
|
|
350
|
+
try {
|
|
351
|
+
const completed = this.sources.experimentEngine.list('complete', 10);
|
|
352
|
+
for (const exp of completed) {
|
|
353
|
+
if (exp.conclusion?.significant && exp.conclusion.effect_size && Math.abs(exp.conclusion.effect_size) > 0.8) {
|
|
354
|
+
surprises.push({
|
|
355
|
+
topic: exp.name,
|
|
356
|
+
expected: `Null hypothesis (no effect)`,
|
|
357
|
+
actual: `Large effect (d=${exp.conclusion.effect_size.toFixed(2)}, p=${exp.conclusion.p_value?.toFixed(4)})`,
|
|
358
|
+
deviation: Math.min(1, Math.abs(exp.conclusion.effect_size)),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// experiment engine may not be wired
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (surprises.length > 0) {
|
|
368
|
+
this.ts?.emit('curiosity', 'discovering', `Found ${surprises.length} surprise(s)!`, surprises.some(s => s.deviation > 0.7) ? 'notable' : 'routine');
|
|
369
|
+
}
|
|
370
|
+
return surprises.sort((a, b) => b.deviation - a.deviation);
|
|
371
|
+
}
|
|
372
|
+
// ── Query Methods ────────────────────────────────────
|
|
373
|
+
getGaps(limit = 10) {
|
|
374
|
+
const rows = this.stmtListGaps.all(limit);
|
|
375
|
+
return rows.map(r => this.toGap(r));
|
|
376
|
+
}
|
|
377
|
+
getGap(id) {
|
|
378
|
+
const row = this.stmtGetGap.get(id);
|
|
379
|
+
return row ? this.toGap(row) : null;
|
|
380
|
+
}
|
|
381
|
+
getQuestions(limit = 20) {
|
|
382
|
+
const rows = this.stmtListQuestions.all(limit);
|
|
383
|
+
return rows.map(r => this.toQuestion(r));
|
|
384
|
+
}
|
|
385
|
+
getArms() {
|
|
386
|
+
const rows = this.stmtGetTopicStats.all();
|
|
387
|
+
return rows.map(r => ({
|
|
388
|
+
topic: r.topic,
|
|
389
|
+
pulls: r.pulls,
|
|
390
|
+
totalReward: r.total_reward,
|
|
391
|
+
averageReward: r.avg_reward,
|
|
392
|
+
ucbScore: 0, // Computed in selectTopic()
|
|
393
|
+
lastPulled: r.last_pulled ? new Date(r.last_pulled).getTime() : null,
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
getExplorations(limit = 50) {
|
|
397
|
+
const rows = this.stmtGetExplorations.all(limit);
|
|
398
|
+
return rows.map(r => ({
|
|
399
|
+
id: r.id,
|
|
400
|
+
topic: r.topic,
|
|
401
|
+
action: r.action,
|
|
402
|
+
reward: r.reward,
|
|
403
|
+
context: r.context,
|
|
404
|
+
timestamp: r.timestamp,
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
getStatus() {
|
|
408
|
+
const totalGaps = this.db.prepare('SELECT COUNT(*) as cnt FROM curiosity_gaps').get();
|
|
409
|
+
const activeGaps = this.stmtActiveGapCount.get();
|
|
410
|
+
const totalQuestions = this.db.prepare('SELECT COUNT(*) as cnt FROM curiosity_questions').get();
|
|
411
|
+
const unanswered = this.stmtUnansweredCount.get();
|
|
412
|
+
const totalExpl = this.stmtTotalExplorations.get();
|
|
413
|
+
const exploreCount = this.db.prepare("SELECT COUNT(*) as cnt FROM curiosity_explorations WHERE action = 'explore'").get().cnt;
|
|
414
|
+
const exploitCount = this.db.prepare("SELECT COUNT(*) as cnt FROM curiosity_explorations WHERE action = 'exploit'").get().cnt;
|
|
415
|
+
const total = exploreCount + exploitCount;
|
|
416
|
+
return {
|
|
417
|
+
totalGaps: totalGaps.cnt,
|
|
418
|
+
activeGaps: activeGaps.cnt,
|
|
419
|
+
totalQuestions: totalQuestions.cnt,
|
|
420
|
+
unansweredQuestions: unanswered.cnt,
|
|
421
|
+
totalExplorations: totalExpl.cnt,
|
|
422
|
+
explorationRate: total > 0 ? exploreCount / total : 0,
|
|
423
|
+
topGaps: this.getGaps(5),
|
|
424
|
+
topArms: this.getArms().sort((a, b) => b.averageReward - a.averageReward).slice(0, 5),
|
|
425
|
+
uptime: Date.now() - this.startTime,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// ── Private: Topic Gathering ─────────────────────────
|
|
429
|
+
/** Gather topics from attention, knowledge, and hypotheses. */
|
|
430
|
+
gatherTopics() {
|
|
431
|
+
const topics = new Set();
|
|
432
|
+
// From attention engine: what Brain is paying attention to
|
|
433
|
+
if (this.sources.attentionEngine) {
|
|
434
|
+
try {
|
|
435
|
+
const topTopics = this.sources.attentionEngine.getTopTopics(15);
|
|
436
|
+
for (const t of topTopics)
|
|
437
|
+
topics.add(t.topic.toLowerCase());
|
|
438
|
+
}
|
|
439
|
+
catch { /* not wired */ }
|
|
440
|
+
}
|
|
441
|
+
// From knowledge distiller: known principles/strategies
|
|
442
|
+
if (this.sources.knowledgeDistiller) {
|
|
443
|
+
try {
|
|
444
|
+
const pkg = this.sources.knowledgeDistiller.getPackage(this.config.brainName);
|
|
445
|
+
for (const p of pkg.principles) {
|
|
446
|
+
const words = this.extractTopicWords(p.statement);
|
|
447
|
+
for (const w of words)
|
|
448
|
+
topics.add(w);
|
|
449
|
+
}
|
|
450
|
+
for (const ap of pkg.anti_patterns) {
|
|
451
|
+
const words = this.extractTopicWords(ap.statement);
|
|
452
|
+
for (const w of words)
|
|
453
|
+
topics.add(w);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch { /* not wired */ }
|
|
457
|
+
}
|
|
458
|
+
// From hypothesis engine: active hypothesis topics
|
|
459
|
+
if (this.sources.hypothesisEngine) {
|
|
460
|
+
try {
|
|
461
|
+
const active = this.sources.hypothesisEngine.list('testing', 10);
|
|
462
|
+
for (const h of active) {
|
|
463
|
+
const words = this.extractTopicWords(h.statement);
|
|
464
|
+
for (const w of words)
|
|
465
|
+
topics.add(w);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch { /* not wired */ }
|
|
469
|
+
}
|
|
470
|
+
return Array.from(topics);
|
|
471
|
+
}
|
|
472
|
+
/** Extract meaningful topic words from a statement. */
|
|
473
|
+
extractTopicWords(statement) {
|
|
474
|
+
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
|
475
|
+
'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
|
|
476
|
+
'may', 'might', 'must', 'shall', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'at',
|
|
477
|
+
'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below',
|
|
478
|
+
'between', 'but', 'and', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either', 'neither',
|
|
479
|
+
'each', 'every', 'all', 'any', 'few', 'more', 'most', 'other', 'some', 'such', 'no',
|
|
480
|
+
'only', 'own', 'same', 'than', 'too', 'very', 'just', 'that', 'this', 'these', 'those',
|
|
481
|
+
'it', 'its', 'if', 'then', 'when', 'while', 'where', 'how', 'what', 'which', 'who',
|
|
482
|
+
'whom', 'why', 'because', 'about', 'also', 'up', 'out', 'one', 'two', 'three',
|
|
483
|
+
'new', 'old', 'high', 'low', 'much', 'many', 'well', 'back', 'even', 'still', 'over',
|
|
484
|
+
'they', 'them', 'their', 'we', 'us', 'our', 'you', 'your', 'he', 'she', 'him', 'her',
|
|
485
|
+
'his', 'i', 'me', 'my', 'der', 'die', 'das', 'ein', 'eine', 'und', 'oder', 'nicht',
|
|
486
|
+
'mit', 'von', 'zu', 'ist', 'sind', 'hat', 'haben', 'wird', 'werden', 'kann', 'können',
|
|
487
|
+
]);
|
|
488
|
+
return statement
|
|
489
|
+
.toLowerCase()
|
|
490
|
+
.replace(/[^a-z0-9äöüß\s-]/g, '')
|
|
491
|
+
.split(/\s+/)
|
|
492
|
+
.filter(w => w.length > 3 && !stopWords.has(w));
|
|
493
|
+
}
|
|
494
|
+
// ── Private: Scoring ──────────────────────────────────
|
|
495
|
+
/** Get attention score for a topic (0-1 normalized). */
|
|
496
|
+
getAttentionFor(topic) {
|
|
497
|
+
if (!this.sources.attentionEngine)
|
|
498
|
+
return 0.5; // Default moderate attention
|
|
499
|
+
try {
|
|
500
|
+
const topTopics = this.sources.attentionEngine.getTopTopics(20);
|
|
501
|
+
const match = topTopics.find(t => t.topic.toLowerCase() === topic.toLowerCase());
|
|
502
|
+
if (!match)
|
|
503
|
+
return 0.1; // Known but not in focus
|
|
504
|
+
// Normalize: highest attention topic = 1.0
|
|
505
|
+
const maxScore = topTopics[0]?.score || 1;
|
|
506
|
+
return Math.min(1, match.score / maxScore);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return 0.5;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** Get knowledge score for a topic (0-1). High = we know a lot. */
|
|
513
|
+
getKnowledgeFor(topic) {
|
|
514
|
+
let score = 0;
|
|
515
|
+
let factors = 0;
|
|
516
|
+
// Check principles
|
|
517
|
+
if (this.sources.knowledgeDistiller) {
|
|
518
|
+
try {
|
|
519
|
+
const pkg = this.sources.knowledgeDistiller.getPackage(this.config.brainName);
|
|
520
|
+
const principleMatches = pkg.principles.filter(p => p.statement.toLowerCase().includes(topic.toLowerCase())).length;
|
|
521
|
+
score += Math.min(1, principleMatches / 3); // 3+ principles = full knowledge
|
|
522
|
+
factors++;
|
|
523
|
+
const antiPatternMatches = pkg.anti_patterns.filter(ap => ap.statement.toLowerCase().includes(topic.toLowerCase())).length;
|
|
524
|
+
score += Math.min(1, antiPatternMatches / 2);
|
|
525
|
+
factors++;
|
|
526
|
+
}
|
|
527
|
+
catch { /* not wired */ }
|
|
528
|
+
}
|
|
529
|
+
// Check hypotheses
|
|
530
|
+
if (this.sources.hypothesisEngine) {
|
|
531
|
+
try {
|
|
532
|
+
const all = this.sources.hypothesisEngine.list(undefined, 50);
|
|
533
|
+
const confirmed = all.filter(h => h.statement.toLowerCase().includes(topic.toLowerCase()) && h.status === 'confirmed').length;
|
|
534
|
+
const total = all.filter(h => h.statement.toLowerCase().includes(topic.toLowerCase())).length;
|
|
535
|
+
score += total > 0 ? confirmed / Math.max(total, 1) : 0;
|
|
536
|
+
factors++;
|
|
537
|
+
}
|
|
538
|
+
catch { /* not wired */ }
|
|
539
|
+
}
|
|
540
|
+
// Check experiments
|
|
541
|
+
if (this.sources.experimentEngine) {
|
|
542
|
+
try {
|
|
543
|
+
const completed = this.sources.experimentEngine.list('complete', 50);
|
|
544
|
+
const relevant = completed.filter(e => e.name.toLowerCase().includes(topic.toLowerCase()) ||
|
|
545
|
+
e.hypothesis.toLowerCase().includes(topic.toLowerCase())).length;
|
|
546
|
+
score += Math.min(1, relevant / 2);
|
|
547
|
+
factors++;
|
|
548
|
+
}
|
|
549
|
+
catch { /* not wired */ }
|
|
550
|
+
}
|
|
551
|
+
// Check narrative engine for confidence
|
|
552
|
+
if (this.sources.narrativeEngine) {
|
|
553
|
+
try {
|
|
554
|
+
const confidence = this.sources.narrativeEngine.getConfidenceReport(topic);
|
|
555
|
+
score += confidence.overallConfidence;
|
|
556
|
+
factors++;
|
|
557
|
+
}
|
|
558
|
+
catch { /* not wired */ }
|
|
559
|
+
}
|
|
560
|
+
return factors > 0 ? score / factors : 0;
|
|
561
|
+
}
|
|
562
|
+
// ── Private: Gap Classification ──────────────────────
|
|
563
|
+
classifyGap(topic, knowledgeScore) {
|
|
564
|
+
if (knowledgeScore === 0)
|
|
565
|
+
return 'dark_zone';
|
|
566
|
+
if (knowledgeScore < 0.2)
|
|
567
|
+
return 'unexplored';
|
|
568
|
+
// Check for contradictions via narrative engine
|
|
569
|
+
if (this.sources.narrativeEngine) {
|
|
570
|
+
try {
|
|
571
|
+
const contradictions = this.sources.narrativeEngine.findContradictions();
|
|
572
|
+
const hasContradiction = contradictions.some(c => c.statement_a?.toLowerCase().includes(topic.toLowerCase()) ||
|
|
573
|
+
c.statement_b?.toLowerCase().includes(topic.toLowerCase()));
|
|
574
|
+
if (hasContradiction)
|
|
575
|
+
return 'contradictory';
|
|
576
|
+
}
|
|
577
|
+
catch { /* not wired */ }
|
|
578
|
+
}
|
|
579
|
+
// Check if knowledge is stale (last hypothesis/experiment > 100 cycles ago)
|
|
580
|
+
if (this.sources.hypothesisEngine) {
|
|
581
|
+
try {
|
|
582
|
+
const all = this.sources.hypothesisEngine.list(undefined, 50);
|
|
583
|
+
const relevant = all.filter(h => h.statement.toLowerCase().includes(topic.toLowerCase()));
|
|
584
|
+
if (relevant.length > 0) {
|
|
585
|
+
const newest = relevant[0];
|
|
586
|
+
const age = Date.now() - new Date(newest.created_at || 0).getTime();
|
|
587
|
+
if (age > 24 * 60 * 60 * 1000)
|
|
588
|
+
return 'stale'; // > 24h old
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch { /* not wired */ }
|
|
592
|
+
}
|
|
593
|
+
return 'shallow';
|
|
594
|
+
}
|
|
595
|
+
// ── Private: Question Generation ─────────────────────
|
|
596
|
+
generateQuestionsFor(topic, gapType) {
|
|
597
|
+
const questions = [];
|
|
598
|
+
const t = topic;
|
|
599
|
+
switch (gapType) {
|
|
600
|
+
case 'dark_zone':
|
|
601
|
+
questions.push(`What is "${t}" and how does it relate to our domain?`, `What data do we need to collect about "${t}"?`, `Are there patterns in other domains that apply to "${t}"?`);
|
|
602
|
+
break;
|
|
603
|
+
case 'shallow':
|
|
604
|
+
questions.push(`What deeper patterns exist in "${t}" beyond surface observations?`, `How confident are our current assumptions about "${t}"?`);
|
|
605
|
+
break;
|
|
606
|
+
case 'contradictory':
|
|
607
|
+
questions.push(`What explains the contradictions in "${t}"?`, `Are there hidden variables affecting "${t}"?`, `Should we design an experiment to resolve "${t}" contradictions?`);
|
|
608
|
+
break;
|
|
609
|
+
case 'stale':
|
|
610
|
+
questions.push(`Has "${t}" changed since our last observation?`, `What new data about "${t}" should we collect?`);
|
|
611
|
+
break;
|
|
612
|
+
case 'unexplored':
|
|
613
|
+
questions.push(`Why haven't we investigated "${t}" despite paying attention to it?`, `What hypotheses can we form about "${t}"?`, `How does "${t}" relate to our confirmed principles?`);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
return questions.slice(0, this.config.maxQuestionsPerTopic);
|
|
617
|
+
}
|
|
618
|
+
inferQuestionType(question) {
|
|
619
|
+
const q = question.toLowerCase();
|
|
620
|
+
if (q.startsWith('what'))
|
|
621
|
+
return 'what';
|
|
622
|
+
if (q.startsWith('why'))
|
|
623
|
+
return 'why';
|
|
624
|
+
if (q.startsWith('how'))
|
|
625
|
+
return 'how';
|
|
626
|
+
if (q.includes('relate') || q.includes('correlat') || q.includes('connect'))
|
|
627
|
+
return 'correlation';
|
|
628
|
+
if (q.includes('predict') || q.includes('will') || q.includes('expect'))
|
|
629
|
+
return 'prediction';
|
|
630
|
+
if (q.includes('compar') || q.includes('differ') || q.includes('versus'))
|
|
631
|
+
return 'comparison';
|
|
632
|
+
return 'what';
|
|
633
|
+
}
|
|
634
|
+
// ── Private: Action Suggestions ──────────────────────
|
|
635
|
+
suggestActions(topic, action) {
|
|
636
|
+
if (action === 'explore') {
|
|
637
|
+
return [
|
|
638
|
+
`Search for data related to "${topic}"`,
|
|
639
|
+
`Form a hypothesis about "${topic}"`,
|
|
640
|
+
`Check cross-brain knowledge about "${topic}"`,
|
|
641
|
+
`Create a research agenda item for "${topic}"`,
|
|
642
|
+
];
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
return [
|
|
646
|
+
`Design an experiment to deepen "${topic}" understanding`,
|
|
647
|
+
`Distill existing "${topic}" knowledge into principles`,
|
|
648
|
+
`Apply "${topic}" insights to current challenges`,
|
|
649
|
+
];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// ── Private: Helpers ─────────────────────────────────
|
|
653
|
+
toGap(row) {
|
|
654
|
+
let questions = [];
|
|
655
|
+
try {
|
|
656
|
+
questions = JSON.parse(row.questions || '[]');
|
|
657
|
+
}
|
|
658
|
+
catch { /* ignore */ }
|
|
659
|
+
return {
|
|
660
|
+
id: row.id,
|
|
661
|
+
topic: row.topic,
|
|
662
|
+
attentionScore: row.attention_score,
|
|
663
|
+
knowledgeScore: row.knowledge_score,
|
|
664
|
+
gapScore: row.gap_score,
|
|
665
|
+
gapType: row.gap_type,
|
|
666
|
+
questions,
|
|
667
|
+
discoveredAt: row.discovered_at,
|
|
668
|
+
addressedAt: row.addressed_at || null,
|
|
669
|
+
explorationCount: row.exploration_count || 0,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
toQuestion(row) {
|
|
673
|
+
return {
|
|
674
|
+
id: row.id,
|
|
675
|
+
topic: row.topic,
|
|
676
|
+
question: row.question,
|
|
677
|
+
questionType: row.question_type,
|
|
678
|
+
priority: row.priority,
|
|
679
|
+
answered: row.answered === 1,
|
|
680
|
+
answer: row.answer || null,
|
|
681
|
+
askedAt: row.asked_at,
|
|
682
|
+
answeredAt: row.answered_at || null,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
//# sourceMappingURL=curiosity-engine.js.map
|