@kernel.chat/kbot 3.62.0 → 3.63.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/agent.js +4 -1
- package/dist/cli.js +75 -1
- package/dist/dream.d.ts +66 -0
- package/dist/dream.js +377 -0
- package/dist/tools/dream-tools.d.ts +2 -0
- package/dist/tools/dream-tools.js +159 -0
- package/dist/tools/index.js +1 -0
- package/ollama-manifest.json +1 -1
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -16,6 +16,7 @@ import { formatContextForPrompt } from './context.js';
|
|
|
16
16
|
import { getMatrixSystemPrompt } from './matrix.js';
|
|
17
17
|
import { buildFullLearningContext, findPattern, recordPattern, cacheSolution, updateProfile, classifyTask, extractKeywords, learnFromExchange, updateProjectMemory, shouldAutoTrain, selfTrain, } from './learning.js';
|
|
18
18
|
import { getMemoryPrompt, addTurn, getPreviousMessages, getHistory } from './memory.js';
|
|
19
|
+
import { getDreamPrompt, dreamAfterSession } from './dream.js';
|
|
19
20
|
import { autoCompact, compressToolResult } from './context-manager.js';
|
|
20
21
|
import { learnedRoute, recordRoute } from './learned-router.js';
|
|
21
22
|
import { buildCacheablePrompt, createPromptSections } from './prompt-cache.js';
|
|
@@ -965,7 +966,7 @@ Always quote file paths that contain spaces. Never reference internal system nam
|
|
|
965
966
|
persona: PERSONA,
|
|
966
967
|
matrixPrompt: matrixPrompt || undefined,
|
|
967
968
|
contextSnippet: (contextSnippet || '') + repoMapSnippet + graphSnippet + skillsSnippet + skillLibrarySnippet || undefined,
|
|
968
|
-
memorySnippet: (memorySnippet || '') + reflectionSnippet || undefined,
|
|
969
|
+
memorySnippet: (memorySnippet || '') + getDreamPrompt(8) + reflectionSnippet || undefined,
|
|
969
970
|
learningContext: ((learningContext || '') + (synthesisSnippet ? '\n\n' + synthesisSnippet : '') + (correctionsSnippet ? '\n\n' + correctionsSnippet : '')) || undefined,
|
|
970
971
|
});
|
|
971
972
|
const provider = byokProvider || 'anthropic';
|
|
@@ -1603,6 +1604,8 @@ Always quote file paths that contain spaces. Never reference internal system nam
|
|
|
1603
1604
|
reason: 'loop_exhausted',
|
|
1604
1605
|
});
|
|
1605
1606
|
telemetry.destroy().catch(() => { });
|
|
1607
|
+
// ── Dream Engine: consolidate session memories (non-blocking, $0 via Ollama) ──
|
|
1608
|
+
dreamAfterSession(sessionId);
|
|
1606
1609
|
const content = lastResponse?.content || 'Reached maximum tool iterations.';
|
|
1607
1610
|
return {
|
|
1608
1611
|
content,
|
package/dist/cli.js
CHANGED
|
@@ -3158,6 +3158,80 @@ async function main() {
|
|
|
3158
3158
|
const r = await executeTool({ id: 'db_health', name: 'db_health', arguments: {} });
|
|
3159
3159
|
console.log(r.result);
|
|
3160
3160
|
});
|
|
3161
|
+
// ── Dream Engine ──
|
|
3162
|
+
const dreamCmd = program
|
|
3163
|
+
.command('dream')
|
|
3164
|
+
.description('Memory consolidation — consolidate session knowledge into durable insights');
|
|
3165
|
+
dreamCmd
|
|
3166
|
+
.command('run')
|
|
3167
|
+
.description('Run a dream cycle now (uses local Ollama)')
|
|
3168
|
+
.action(async () => {
|
|
3169
|
+
const { dream } = await import('./dream.js');
|
|
3170
|
+
printInfo('Dreaming... consolidating session memories with local AI');
|
|
3171
|
+
const result = await dream();
|
|
3172
|
+
if (result.success) {
|
|
3173
|
+
printSuccess(`Dream cycle #${result.cycle} complete`);
|
|
3174
|
+
console.log(` New insights: ${result.newInsights}`);
|
|
3175
|
+
console.log(` Reinforced: ${result.reinforced}`);
|
|
3176
|
+
console.log(` Archived: ${result.archived} aged-out`);
|
|
3177
|
+
console.log(` Duration: ${result.duration}ms`);
|
|
3178
|
+
}
|
|
3179
|
+
else {
|
|
3180
|
+
printWarn(result.error || 'Dream cycle failed');
|
|
3181
|
+
if (result.archived > 0)
|
|
3182
|
+
console.log(` (Still archived ${result.archived} aged-out insights)`);
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
dreamCmd
|
|
3186
|
+
.command('status')
|
|
3187
|
+
.description('Show dream engine status and top insights')
|
|
3188
|
+
.action(async () => {
|
|
3189
|
+
const { getDreamStatus } = await import('./dream.js');
|
|
3190
|
+
const { state, insights, archiveCount } = getDreamStatus();
|
|
3191
|
+
console.log(chalk.bold('\nDream Engine'));
|
|
3192
|
+
console.log(chalk.dim('═══════════════════'));
|
|
3193
|
+
console.log(`Cycles: ${state.cycles}`);
|
|
3194
|
+
console.log(`Last: ${state.lastDream || 'never'}`);
|
|
3195
|
+
console.log(`Active: ${state.activeInsights} insights`);
|
|
3196
|
+
console.log(`Archived: ${state.totalArchived} (${archiveCount} files)`);
|
|
3197
|
+
if (insights.length > 0) {
|
|
3198
|
+
const avgRel = Math.round(insights.reduce((s, i) => s + i.relevance, 0) / insights.length * 100);
|
|
3199
|
+
console.log(`Avg relevance: ${avgRel}%`);
|
|
3200
|
+
console.log(chalk.bold('\nTop insights:'));
|
|
3201
|
+
for (const i of insights.slice(0, 8)) {
|
|
3202
|
+
const pct = Math.round(i.relevance * 100);
|
|
3203
|
+
console.log(` ${chalk.green(`${pct}%`)} [${chalk.cyan(i.category)}] ${i.content}`);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
else {
|
|
3207
|
+
console.log(chalk.dim('\nNo insights yet. Run: kbot dream run'));
|
|
3208
|
+
}
|
|
3209
|
+
});
|
|
3210
|
+
dreamCmd
|
|
3211
|
+
.command('search <query>')
|
|
3212
|
+
.description('Search dream insights by keyword')
|
|
3213
|
+
.action(async (query) => {
|
|
3214
|
+
const { searchDreams } = await import('./dream.js');
|
|
3215
|
+
const results = searchDreams(query);
|
|
3216
|
+
if (results.length === 0) {
|
|
3217
|
+
printInfo(`No insights match "${query}"`);
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
console.log(chalk.bold(`\n${results.length} insights matching "${query}":\n`));
|
|
3221
|
+
for (const i of results.slice(0, 15)) {
|
|
3222
|
+
const pct = Math.round(i.relevance * 100);
|
|
3223
|
+
console.log(` ${chalk.green(`${pct}%`)} [${chalk.cyan(i.category)}] ${i.content}`);
|
|
3224
|
+
console.log(chalk.dim(` ${i.keywords.join(', ')} | ${i.sessions} sessions | ${i.created.split('T')[0]}`));
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
dreamCmd
|
|
3228
|
+
.command('journal')
|
|
3229
|
+
.description('Print the full dream journal')
|
|
3230
|
+
.action(async () => {
|
|
3231
|
+
const { getDreamPrompt } = await import('./dream.js');
|
|
3232
|
+
const journal = getDreamPrompt(50);
|
|
3233
|
+
console.log(journal || chalk.dim('Dream journal is empty. Run: kbot dream run'));
|
|
3234
|
+
});
|
|
3161
3235
|
program.parse(process.argv);
|
|
3162
3236
|
const opts = program.opts();
|
|
3163
3237
|
const promptArgs = program.args;
|
|
@@ -3165,7 +3239,7 @@ async function main() {
|
|
|
3165
3239
|
if (opts.quiet)
|
|
3166
3240
|
setQuiet(true);
|
|
3167
3241
|
// If a sub-command was run, we're done
|
|
3168
|
-
if (['byok', 'auth', 'ide', 'local', 'ollama', 'kbot-local', 'pull', 'doctor', 'serve', 'agents', 'watch', 'voice', 'export', 'plugins', 'changelog', 'release', 'completions', 'automate', 'status', 'spec', 'a2a', 'init', 'email-agent', 'imessage-agent', 'consultation', 'observe', 'discovery', 'bench', 'lab', 'teach', 'sessions', 'admin', 'monitor', 'analytics', 'deploy', 'env', 'db'].includes(program.args[0]))
|
|
3242
|
+
if (['byok', 'auth', 'ide', 'local', 'ollama', 'kbot-local', 'pull', 'doctor', 'serve', 'agents', 'watch', 'voice', 'export', 'plugins', 'changelog', 'release', 'completions', 'automate', 'status', 'spec', 'a2a', 'init', 'email-agent', 'imessage-agent', 'consultation', 'observe', 'discovery', 'bench', 'lab', 'teach', 'sessions', 'admin', 'monitor', 'analytics', 'deploy', 'env', 'db', 'dream'].includes(program.args[0]))
|
|
3169
3243
|
return;
|
|
3170
3244
|
// ── Ollama Launch Integration ──
|
|
3171
3245
|
// Detect when kbot is started via `ollama launch kbot` or `kbot --ollama-launch`
|
package/dist/dream.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface DreamInsight {
|
|
2
|
+
/** Unique ID */
|
|
3
|
+
id: string;
|
|
4
|
+
/** The consolidated insight */
|
|
5
|
+
content: string;
|
|
6
|
+
/** Category: pattern | preference | skill | project | relationship */
|
|
7
|
+
category: DreamCategory;
|
|
8
|
+
/** Keywords for retrieval */
|
|
9
|
+
keywords: string[];
|
|
10
|
+
/** Relevance score (0-1), decays over time */
|
|
11
|
+
relevance: number;
|
|
12
|
+
/** How many sessions contributed to this insight */
|
|
13
|
+
sessions: number;
|
|
14
|
+
/** Created timestamp */
|
|
15
|
+
created: string;
|
|
16
|
+
/** Last reinforced (refreshed relevance) */
|
|
17
|
+
lastReinforced: string;
|
|
18
|
+
/** Source: which sessions/topics generated this */
|
|
19
|
+
source: string;
|
|
20
|
+
}
|
|
21
|
+
export type DreamCategory = 'pattern' | 'preference' | 'skill' | 'project' | 'relationship';
|
|
22
|
+
export interface DreamState {
|
|
23
|
+
/** Total dream cycles completed */
|
|
24
|
+
cycles: number;
|
|
25
|
+
/** Last dream timestamp */
|
|
26
|
+
lastDream: string | null;
|
|
27
|
+
/** Total insights ever created */
|
|
28
|
+
totalInsights: number;
|
|
29
|
+
/** Total insights archived (aged out) */
|
|
30
|
+
totalArchived: number;
|
|
31
|
+
/** Insights currently active */
|
|
32
|
+
activeInsights: number;
|
|
33
|
+
/** Last session turn count that was dreamed about */
|
|
34
|
+
lastSessionTurns: number;
|
|
35
|
+
}
|
|
36
|
+
/** Apply exponential decay to all insights based on time elapsed */
|
|
37
|
+
export declare function ageMemories(insights: DreamInsight[]): {
|
|
38
|
+
aged: DreamInsight[];
|
|
39
|
+
archived: DreamInsight[];
|
|
40
|
+
};
|
|
41
|
+
/** Run a full dream cycle — consolidate, reinforce, age */
|
|
42
|
+
export declare function dream(sessionId?: string): Promise<DreamResult>;
|
|
43
|
+
export interface DreamResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
newInsights: number;
|
|
46
|
+
reinforced: number;
|
|
47
|
+
archived: number;
|
|
48
|
+
cycle: number;
|
|
49
|
+
duration: number;
|
|
50
|
+
error: string | null;
|
|
51
|
+
}
|
|
52
|
+
/** Get dream insights for inclusion in system prompt */
|
|
53
|
+
export declare function getDreamPrompt(maxInsights?: number): string;
|
|
54
|
+
/** Get full dream status */
|
|
55
|
+
export declare function getDreamStatus(): {
|
|
56
|
+
state: DreamState;
|
|
57
|
+
insights: DreamInsight[];
|
|
58
|
+
archiveCount: number;
|
|
59
|
+
};
|
|
60
|
+
/** Search dream insights by keyword */
|
|
61
|
+
export declare function searchDreams(query: string): DreamInsight[];
|
|
62
|
+
/** Manually reinforce a specific insight (user confirms it's still relevant) */
|
|
63
|
+
export declare function reinforceInsight(insightId: string): boolean;
|
|
64
|
+
/** Run dream after session ends (non-blocking) */
|
|
65
|
+
export declare function dreamAfterSession(sessionId?: string): void;
|
|
66
|
+
//# sourceMappingURL=dream.d.ts.map
|
package/dist/dream.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// kbot Dream Engine — In-Process Memory Consolidation
|
|
2
|
+
//
|
|
3
|
+
// Inspired by Claude Code's autoDream system, but built kbot's way:
|
|
4
|
+
// - Uses local Ollama models ($0 cost) instead of cloud API
|
|
5
|
+
// - Runs post-session or on-demand via `kbot dream`
|
|
6
|
+
// - Ages memories with exponential decay scoring
|
|
7
|
+
// - Extracts cross-session insights ("dreams")
|
|
8
|
+
// - Produces a dream journal that feeds back into system prompts
|
|
9
|
+
//
|
|
10
|
+
// The metaphor: after a session, kbot "sleeps" — consolidating short-term
|
|
11
|
+
// session history into long-term durable insights, just like biological memory.
|
|
12
|
+
//
|
|
13
|
+
// Storage: ~/.kbot/memory/dreams/
|
|
14
|
+
// - journal.json — consolidated insights (the "dream journal")
|
|
15
|
+
// - state.json — last dream timestamp, cycle count, stats
|
|
16
|
+
// - archive/ — old dreams that aged out (kept for archaeology)
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
20
|
+
import { getHistory } from './memory.js';
|
|
21
|
+
import { loadMemory } from './memory.js';
|
|
22
|
+
// ── Constants ──
|
|
23
|
+
const DREAM_DIR = join(homedir(), '.kbot', 'memory', 'dreams');
|
|
24
|
+
const JOURNAL_FILE = join(DREAM_DIR, 'journal.json');
|
|
25
|
+
const STATE_FILE = join(DREAM_DIR, 'state.json');
|
|
26
|
+
const ARCHIVE_DIR = join(DREAM_DIR, 'archive');
|
|
27
|
+
const MAX_INSIGHTS = 100; // Keep top 100 insights
|
|
28
|
+
const DECAY_RATE = 0.03; // ~3% relevance loss per day
|
|
29
|
+
const MIN_RELEVANCE = 0.15; // Below this → archive
|
|
30
|
+
const CONSOLIDATION_MODEL = 'kernel:latest'; // Local Ollama model
|
|
31
|
+
const OLLAMA_URL = 'http://localhost:11434';
|
|
32
|
+
const OLLAMA_TIMEOUT = 60_000; // 60s per generation
|
|
33
|
+
// ── Helpers ──
|
|
34
|
+
function ensureDirs() {
|
|
35
|
+
for (const dir of [DREAM_DIR, ARCHIVE_DIR]) {
|
|
36
|
+
if (!existsSync(dir))
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function loadJournal() {
|
|
41
|
+
ensureDirs();
|
|
42
|
+
if (!existsSync(JOURNAL_FILE))
|
|
43
|
+
return [];
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(JOURNAL_FILE, 'utf-8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function saveJournal(insights) {
|
|
52
|
+
ensureDirs();
|
|
53
|
+
writeFileSync(JOURNAL_FILE, JSON.stringify(insights, null, 2));
|
|
54
|
+
}
|
|
55
|
+
function loadState() {
|
|
56
|
+
ensureDirs();
|
|
57
|
+
const defaults = {
|
|
58
|
+
cycles: 0, lastDream: null, totalInsights: 0,
|
|
59
|
+
totalArchived: 0, activeInsights: 0, lastSessionTurns: 0,
|
|
60
|
+
};
|
|
61
|
+
if (!existsSync(STATE_FILE))
|
|
62
|
+
return defaults;
|
|
63
|
+
try {
|
|
64
|
+
return { ...defaults, ...JSON.parse(readFileSync(STATE_FILE, 'utf-8')) };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return defaults;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function saveState(state) {
|
|
71
|
+
ensureDirs();
|
|
72
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
73
|
+
}
|
|
74
|
+
function generateId() {
|
|
75
|
+
return `dream_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
+
}
|
|
77
|
+
/** Calculate days between two ISO dates */
|
|
78
|
+
function daysBetween(a, b) {
|
|
79
|
+
return Math.abs(new Date(b).getTime() - new Date(a).getTime()) / (1000 * 60 * 60 * 24);
|
|
80
|
+
}
|
|
81
|
+
// ── Ollama Integration ──
|
|
82
|
+
async function ollamaGenerate(prompt, model = CONSOLIDATION_MODEL) {
|
|
83
|
+
try {
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const timer = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT);
|
|
86
|
+
const res = await fetch(`${OLLAMA_URL}/api/generate`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model,
|
|
91
|
+
prompt,
|
|
92
|
+
stream: false,
|
|
93
|
+
options: { temperature: 0.3, num_predict: 512 },
|
|
94
|
+
}),
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
});
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
if (!res.ok)
|
|
99
|
+
return null;
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
return data.response?.trim() || null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function isOllamaAvailable() {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
110
|
+
return res.ok;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Memory Aging ──
|
|
117
|
+
/** Apply exponential decay to all insights based on time elapsed */
|
|
118
|
+
export function ageMemories(insights) {
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
const aged = [];
|
|
121
|
+
const archived = [];
|
|
122
|
+
for (const insight of insights) {
|
|
123
|
+
const days = daysBetween(insight.lastReinforced, now);
|
|
124
|
+
// Exponential decay: relevance * e^(-rate * days)
|
|
125
|
+
const decayed = insight.relevance * Math.exp(-DECAY_RATE * days);
|
|
126
|
+
if (decayed < MIN_RELEVANCE) {
|
|
127
|
+
archived.push({ ...insight, relevance: decayed });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
aged.push({ ...insight, relevance: Math.round(decayed * 1000) / 1000 });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { aged, archived };
|
|
134
|
+
}
|
|
135
|
+
/** Archive old insights to disk */
|
|
136
|
+
function archiveInsights(insights) {
|
|
137
|
+
if (insights.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
ensureDirs();
|
|
140
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
141
|
+
const archivePath = join(ARCHIVE_DIR, `archived_${timestamp}.json`);
|
|
142
|
+
writeFileSync(archivePath, JSON.stringify(insights, null, 2));
|
|
143
|
+
}
|
|
144
|
+
// ── Consolidation Prompts ──
|
|
145
|
+
function buildConsolidationPrompt(sessionHistory, existingInsights, existingMemory) {
|
|
146
|
+
const historyText = sessionHistory
|
|
147
|
+
.map(t => `[${t.role}]: ${t.content.slice(0, 500)}`)
|
|
148
|
+
.join('\n');
|
|
149
|
+
const existingText = existingInsights.length > 0
|
|
150
|
+
? existingInsights.slice(0, 20).map(i => `- [${i.category}] ${i.content}`).join('\n')
|
|
151
|
+
: '(none yet)';
|
|
152
|
+
return `You are a memory consolidation system. Analyze this conversation session and extract durable insights.
|
|
153
|
+
|
|
154
|
+
EXISTING INSIGHTS:
|
|
155
|
+
${existingText}
|
|
156
|
+
|
|
157
|
+
EXISTING PERSISTENT MEMORY:
|
|
158
|
+
${existingMemory.slice(0, 2000) || '(empty)'}
|
|
159
|
+
|
|
160
|
+
SESSION TO CONSOLIDATE:
|
|
161
|
+
${historyText}
|
|
162
|
+
|
|
163
|
+
INSTRUCTIONS:
|
|
164
|
+
Extract 1-5 insights from this session. Each insight should be:
|
|
165
|
+
- Durable (useful beyond this session)
|
|
166
|
+
- Non-obvious (not derivable from reading code)
|
|
167
|
+
- About the USER, their preferences, patterns, or project context
|
|
168
|
+
|
|
169
|
+
Format each insight as a JSON array of objects:
|
|
170
|
+
[
|
|
171
|
+
{
|
|
172
|
+
"content": "the insight text",
|
|
173
|
+
"category": "pattern|preference|skill|project|relationship",
|
|
174
|
+
"keywords": ["keyword1", "keyword2"]
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
If the session is too short or trivial for insights, return: []
|
|
179
|
+
|
|
180
|
+
Respond ONLY with the JSON array, no other text.`;
|
|
181
|
+
}
|
|
182
|
+
function buildReinforcePrompt(sessionHistory, existingInsights) {
|
|
183
|
+
const historyText = sessionHistory
|
|
184
|
+
.map(t => `[${t.role}]: ${t.content.slice(0, 300)}`)
|
|
185
|
+
.join('\n');
|
|
186
|
+
const insightList = existingInsights
|
|
187
|
+
.map((i, idx) => `${idx}: [${i.category}] ${i.content}`)
|
|
188
|
+
.join('\n');
|
|
189
|
+
return `You are a memory reinforcement system. Given this conversation, which existing insights are confirmed/relevant?
|
|
190
|
+
|
|
191
|
+
EXISTING INSIGHTS (by index):
|
|
192
|
+
${insightList}
|
|
193
|
+
|
|
194
|
+
SESSION:
|
|
195
|
+
${historyText}
|
|
196
|
+
|
|
197
|
+
Return a JSON array of insight indices that this session reinforces. Example: [0, 3, 7]
|
|
198
|
+
If none are reinforced, return: []
|
|
199
|
+
|
|
200
|
+
Respond ONLY with the JSON array.`;
|
|
201
|
+
}
|
|
202
|
+
// ── Core Dream Functions ──
|
|
203
|
+
/** Run a full dream cycle — consolidate, reinforce, age */
|
|
204
|
+
export async function dream(sessionId = 'default') {
|
|
205
|
+
const result = {
|
|
206
|
+
success: false,
|
|
207
|
+
newInsights: 0,
|
|
208
|
+
reinforced: 0,
|
|
209
|
+
archived: 0,
|
|
210
|
+
cycle: 0,
|
|
211
|
+
duration: 0,
|
|
212
|
+
error: null,
|
|
213
|
+
};
|
|
214
|
+
const start = Date.now();
|
|
215
|
+
// Check Ollama availability
|
|
216
|
+
if (!(await isOllamaAvailable())) {
|
|
217
|
+
// Fallback: still do aging even without Ollama
|
|
218
|
+
const journal = loadJournal();
|
|
219
|
+
if (journal.length > 0) {
|
|
220
|
+
const { aged, archived } = ageMemories(journal);
|
|
221
|
+
archiveInsights(archived);
|
|
222
|
+
saveJournal(aged);
|
|
223
|
+
result.archived = archived.length;
|
|
224
|
+
}
|
|
225
|
+
result.error = 'Ollama not available — aging only (no new consolidation)';
|
|
226
|
+
result.duration = Date.now() - start;
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
const state = loadState();
|
|
230
|
+
let journal = loadJournal();
|
|
231
|
+
const history = getHistory(sessionId);
|
|
232
|
+
const memory = loadMemory();
|
|
233
|
+
// Skip if session too short
|
|
234
|
+
if (history.length < 4) {
|
|
235
|
+
result.error = 'Session too short for consolidation (< 4 turns)';
|
|
236
|
+
result.duration = Date.now() - start;
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
// Phase 1: Age existing memories
|
|
240
|
+
const { aged, archived } = ageMemories(journal);
|
|
241
|
+
archiveInsights(archived);
|
|
242
|
+
journal = aged;
|
|
243
|
+
result.archived = archived.length;
|
|
244
|
+
// Phase 2: Extract new insights from session
|
|
245
|
+
const consolidationPrompt = buildConsolidationPrompt(history, journal, memory);
|
|
246
|
+
const rawInsights = await ollamaGenerate(consolidationPrompt);
|
|
247
|
+
if (rawInsights) {
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(rawInsights);
|
|
250
|
+
if (Array.isArray(parsed)) {
|
|
251
|
+
const now = new Date().toISOString();
|
|
252
|
+
for (const p of parsed.slice(0, 5)) {
|
|
253
|
+
// Dedup: skip if very similar content exists
|
|
254
|
+
const isDupe = journal.some(j => j.content.toLowerCase().includes(p.content.toLowerCase().slice(0, 50)) ||
|
|
255
|
+
p.content.toLowerCase().includes(j.content.toLowerCase().slice(0, 50)));
|
|
256
|
+
if (isDupe)
|
|
257
|
+
continue;
|
|
258
|
+
journal.push({
|
|
259
|
+
id: generateId(),
|
|
260
|
+
content: p.content,
|
|
261
|
+
category: p.category || 'pattern',
|
|
262
|
+
keywords: p.keywords || [],
|
|
263
|
+
relevance: 0.9,
|
|
264
|
+
sessions: 1,
|
|
265
|
+
created: now,
|
|
266
|
+
lastReinforced: now,
|
|
267
|
+
source: `session_${state.cycles + 1}`,
|
|
268
|
+
});
|
|
269
|
+
result.newInsights++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// JSON parse failed — Ollama output wasn't clean
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Phase 3: Reinforce existing insights mentioned in session
|
|
278
|
+
if (journal.length > 0 && history.length >= 4) {
|
|
279
|
+
const reinforcePrompt = buildReinforcePrompt(history, journal);
|
|
280
|
+
const rawReinforce = await ollamaGenerate(reinforcePrompt);
|
|
281
|
+
if (rawReinforce) {
|
|
282
|
+
try {
|
|
283
|
+
const indices = JSON.parse(rawReinforce);
|
|
284
|
+
if (Array.isArray(indices)) {
|
|
285
|
+
const now = new Date().toISOString();
|
|
286
|
+
for (const idx of indices) {
|
|
287
|
+
if (idx >= 0 && idx < journal.length) {
|
|
288
|
+
journal[idx].relevance = Math.min(1, journal[idx].relevance + 0.15);
|
|
289
|
+
journal[idx].sessions++;
|
|
290
|
+
journal[idx].lastReinforced = now;
|
|
291
|
+
result.reinforced++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Reinforcement parse failed — non-critical
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Phase 4: Sort by relevance, trim to max
|
|
302
|
+
journal.sort((a, b) => b.relevance - a.relevance);
|
|
303
|
+
if (journal.length > MAX_INSIGHTS) {
|
|
304
|
+
const overflow = journal.slice(MAX_INSIGHTS);
|
|
305
|
+
archiveInsights(overflow);
|
|
306
|
+
journal = journal.slice(0, MAX_INSIGHTS);
|
|
307
|
+
result.archived += overflow.length;
|
|
308
|
+
}
|
|
309
|
+
// Save everything
|
|
310
|
+
saveJournal(journal);
|
|
311
|
+
state.cycles++;
|
|
312
|
+
state.lastDream = new Date().toISOString();
|
|
313
|
+
state.totalInsights += result.newInsights;
|
|
314
|
+
state.totalArchived += result.archived;
|
|
315
|
+
state.activeInsights = journal.length;
|
|
316
|
+
state.lastSessionTurns = history.length;
|
|
317
|
+
saveState(state);
|
|
318
|
+
result.success = true;
|
|
319
|
+
result.cycle = state.cycles;
|
|
320
|
+
result.duration = Date.now() - start;
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
// ── Query Functions ──
|
|
324
|
+
/** Get dream insights for inclusion in system prompt */
|
|
325
|
+
export function getDreamPrompt(maxInsights = 10) {
|
|
326
|
+
const journal = loadJournal();
|
|
327
|
+
if (journal.length === 0)
|
|
328
|
+
return '';
|
|
329
|
+
const top = journal
|
|
330
|
+
.filter(i => i.relevance > 0.3)
|
|
331
|
+
.slice(0, maxInsights);
|
|
332
|
+
if (top.length === 0)
|
|
333
|
+
return '';
|
|
334
|
+
const lines = top.map(i => `- [${i.category}] ${i.content} (relevance: ${Math.round(i.relevance * 100)}%)`);
|
|
335
|
+
return `\n[Dream Journal — Consolidated Insights]\n${lines.join('\n')}\n`;
|
|
336
|
+
}
|
|
337
|
+
/** Get full dream status */
|
|
338
|
+
export function getDreamStatus() {
|
|
339
|
+
const state = loadState();
|
|
340
|
+
const insights = loadJournal();
|
|
341
|
+
let archiveCount = 0;
|
|
342
|
+
if (existsSync(ARCHIVE_DIR)) {
|
|
343
|
+
archiveCount = readdirSync(ARCHIVE_DIR).filter(f => f.endsWith('.json')).length;
|
|
344
|
+
}
|
|
345
|
+
return { state, insights, archiveCount };
|
|
346
|
+
}
|
|
347
|
+
/** Search dream insights by keyword */
|
|
348
|
+
export function searchDreams(query) {
|
|
349
|
+
const journal = loadJournal();
|
|
350
|
+
const terms = query.toLowerCase().split(/\s+/);
|
|
351
|
+
return journal
|
|
352
|
+
.filter(i => {
|
|
353
|
+
const text = `${i.content} ${i.keywords.join(' ')} ${i.category}`.toLowerCase();
|
|
354
|
+
return terms.some(t => text.includes(t));
|
|
355
|
+
})
|
|
356
|
+
.sort((a, b) => b.relevance - a.relevance);
|
|
357
|
+
}
|
|
358
|
+
/** Manually reinforce a specific insight (user confirms it's still relevant) */
|
|
359
|
+
export function reinforceInsight(insightId) {
|
|
360
|
+
const journal = loadJournal();
|
|
361
|
+
const insight = journal.find(i => i.id === insightId);
|
|
362
|
+
if (!insight)
|
|
363
|
+
return false;
|
|
364
|
+
insight.relevance = Math.min(1, insight.relevance + 0.2);
|
|
365
|
+
insight.lastReinforced = new Date().toISOString();
|
|
366
|
+
insight.sessions++;
|
|
367
|
+
saveJournal(journal);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
/** Run dream after session ends (non-blocking) */
|
|
371
|
+
export function dreamAfterSession(sessionId = 'default') {
|
|
372
|
+
// Fire and forget — don't block the user
|
|
373
|
+
dream(sessionId).catch(() => {
|
|
374
|
+
// Dream failed silently — non-critical
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=dream.js.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// kbot Dream Tools — Agent-accessible memory consolidation
|
|
2
|
+
//
|
|
3
|
+
// Exposes the dream engine to kbot's tool system so agents can:
|
|
4
|
+
// - Trigger consolidation on demand
|
|
5
|
+
// - Query the dream journal for insights
|
|
6
|
+
// - Check dream system status
|
|
7
|
+
// - Search dreams by keyword
|
|
8
|
+
// - Reinforce insights that are still relevant
|
|
9
|
+
import { registerTool } from './index.js';
|
|
10
|
+
import { dream, getDreamStatus, getDreamPrompt, searchDreams, reinforceInsight } from '../dream.js';
|
|
11
|
+
export function registerDreamTools() {
|
|
12
|
+
// ── dream_now ──
|
|
13
|
+
// Trigger a full dream cycle: consolidate, reinforce, age
|
|
14
|
+
registerTool({
|
|
15
|
+
name: 'dream_now',
|
|
16
|
+
description: 'Run a dream cycle — consolidate session memories into durable insights using local AI. Ages old memories, extracts new patterns, reinforces confirmed knowledge. Uses Ollama ($0 cost). Call this at session end or when the user says "dream" or "consolidate".',
|
|
17
|
+
parameters: {
|
|
18
|
+
session_id: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Session ID to consolidate (default: "default")',
|
|
21
|
+
required: false,
|
|
22
|
+
default: 'default',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
tier: 'free',
|
|
26
|
+
timeout: 120_000, // 2 min — Ollama can be slow on first run
|
|
27
|
+
execute: async (args) => {
|
|
28
|
+
const sessionId = args.session_id || 'default';
|
|
29
|
+
const result = await dream(sessionId);
|
|
30
|
+
if (!result.success && result.error) {
|
|
31
|
+
return `Dream cycle (partial): ${result.error}\n` +
|
|
32
|
+
`Archived: ${result.archived} aged-out insights\n` +
|
|
33
|
+
`Duration: ${result.duration}ms`;
|
|
34
|
+
}
|
|
35
|
+
const lines = [
|
|
36
|
+
`Dream cycle #${result.cycle} complete`,
|
|
37
|
+
` New insights: ${result.newInsights}`,
|
|
38
|
+
` Reinforced: ${result.reinforced} existing insights`,
|
|
39
|
+
` Archived: ${result.archived} aged-out insights`,
|
|
40
|
+
` Duration: ${result.duration}ms`,
|
|
41
|
+
];
|
|
42
|
+
if (result.newInsights === 0) {
|
|
43
|
+
lines.push(' (Session may have been too short or trivial for new insights)');
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
// ── dream_status ──
|
|
49
|
+
// Check the dream system's current state
|
|
50
|
+
registerTool({
|
|
51
|
+
name: 'dream_status',
|
|
52
|
+
description: 'Show dream engine status — cycle count, active insights, archive size, last dream timestamp. Use this to check if the dream system is healthy and working.',
|
|
53
|
+
parameters: {},
|
|
54
|
+
tier: 'free',
|
|
55
|
+
execute: async () => {
|
|
56
|
+
const { state, insights, archiveCount } = getDreamStatus();
|
|
57
|
+
const categoryBreakdown = {};
|
|
58
|
+
for (const i of insights) {
|
|
59
|
+
categoryBreakdown[i.category] = (categoryBreakdown[i.category] || 0) + 1;
|
|
60
|
+
}
|
|
61
|
+
const avgRelevance = insights.length > 0
|
|
62
|
+
? Math.round(insights.reduce((sum, i) => sum + i.relevance, 0) / insights.length * 100)
|
|
63
|
+
: 0;
|
|
64
|
+
const lines = [
|
|
65
|
+
'Dream Engine Status',
|
|
66
|
+
'═══════════════════',
|
|
67
|
+
`Cycles completed: ${state.cycles}`,
|
|
68
|
+
`Last dream: ${state.lastDream || 'never'}`,
|
|
69
|
+
`Active insights: ${state.activeInsights}`,
|
|
70
|
+
`Total created: ${state.totalInsights}`,
|
|
71
|
+
`Total archived: ${state.totalArchived}`,
|
|
72
|
+
`Archive files: ${archiveCount}`,
|
|
73
|
+
`Average relevance: ${avgRelevance}%`,
|
|
74
|
+
'',
|
|
75
|
+
'Category breakdown:',
|
|
76
|
+
...Object.entries(categoryBreakdown).map(([cat, count]) => ` ${cat}: ${count}`),
|
|
77
|
+
];
|
|
78
|
+
if (insights.length > 0) {
|
|
79
|
+
lines.push('', 'Top 5 insights:');
|
|
80
|
+
for (const i of insights.slice(0, 5)) {
|
|
81
|
+
lines.push(` [${Math.round(i.relevance * 100)}%] [${i.category}] ${i.content}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
// ── dream_journal ──
|
|
88
|
+
// Get dream insights formatted for system prompt injection
|
|
89
|
+
registerTool({
|
|
90
|
+
name: 'dream_journal',
|
|
91
|
+
description: 'Retrieve the dream journal — consolidated insights from past sessions, ranked by relevance. Use this to understand accumulated knowledge about the user and their projects.',
|
|
92
|
+
parameters: {
|
|
93
|
+
max_insights: {
|
|
94
|
+
type: 'number',
|
|
95
|
+
description: 'Maximum insights to return (default: 15)',
|
|
96
|
+
required: false,
|
|
97
|
+
default: 15,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
tier: 'free',
|
|
101
|
+
execute: async (args) => {
|
|
102
|
+
const max = args.max_insights || 15;
|
|
103
|
+
const prompt = getDreamPrompt(max);
|
|
104
|
+
return prompt || 'Dream journal is empty — no consolidation cycles have run yet. Use dream_now to start.';
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
// ── dream_search ──
|
|
108
|
+
// Search dreams by keyword
|
|
109
|
+
registerTool({
|
|
110
|
+
name: 'dream_search',
|
|
111
|
+
description: 'Search dream insights by keyword. Returns matching insights sorted by relevance.',
|
|
112
|
+
parameters: {
|
|
113
|
+
query: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Search query (keywords)',
|
|
116
|
+
required: true,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
tier: 'free',
|
|
120
|
+
execute: async (args) => {
|
|
121
|
+
const query = args.query;
|
|
122
|
+
if (!query)
|
|
123
|
+
return 'Error: query is required';
|
|
124
|
+
const results = searchDreams(query);
|
|
125
|
+
if (results.length === 0)
|
|
126
|
+
return `No dream insights match "${query}"`;
|
|
127
|
+
const lines = [`Dream search: "${query}" — ${results.length} results`, ''];
|
|
128
|
+
for (const i of results.slice(0, 10)) {
|
|
129
|
+
lines.push(`[${i.id}] [${Math.round(i.relevance * 100)}%] [${i.category}] ${i.content}`);
|
|
130
|
+
lines.push(` Keywords: ${i.keywords.join(', ')} | Sessions: ${i.sessions} | Created: ${i.created.split('T')[0]}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
// ── dream_reinforce ──
|
|
136
|
+
// Manually boost an insight's relevance
|
|
137
|
+
registerTool({
|
|
138
|
+
name: 'dream_reinforce',
|
|
139
|
+
description: 'Reinforce a dream insight — boost its relevance when the user confirms it is still accurate/useful. Prevents important insights from aging out.',
|
|
140
|
+
parameters: {
|
|
141
|
+
insight_id: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'The dream insight ID to reinforce',
|
|
144
|
+
required: true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
tier: 'free',
|
|
148
|
+
execute: async (args) => {
|
|
149
|
+
const id = args.insight_id;
|
|
150
|
+
if (!id)
|
|
151
|
+
return 'Error: insight_id is required';
|
|
152
|
+
const success = reinforceInsight(id);
|
|
153
|
+
return success
|
|
154
|
+
? `Reinforced insight ${id} — relevance boosted, decay clock reset.`
|
|
155
|
+
: `Insight ${id} not found.`;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
} // end registerDreamTools
|
|
159
|
+
//# sourceMappingURL=dream-tools.js.map
|
package/dist/tools/index.js
CHANGED
|
@@ -298,6 +298,7 @@ const LAZY_MODULE_IMPORTS = [
|
|
|
298
298
|
{ path: './hacker-toolkit.js', registerFn: 'registerHackerToolkitTools' },
|
|
299
299
|
{ path: './dj-set-builder.js', registerFn: 'registerDjSetBuilderTools' },
|
|
300
300
|
{ path: './serum2-preset.js', registerFn: 'registerSerum2PresetTools' },
|
|
301
|
+
{ path: './dream-tools.js', registerFn: 'registerDreamTools' },
|
|
301
302
|
];
|
|
302
303
|
/** Track whether lazy tools have been registered */
|
|
303
304
|
let lazyToolsRegistered = false;
|
package/ollama-manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kbot",
|
|
3
3
|
"display_name": "kbot",
|
|
4
4
|
"description": "Open-source terminal AI agent — 670+ tools, 35 agents, science, finance, security, music production, and more.",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.62.0",
|
|
6
6
|
"homepage": "https://kernel.chat",
|
|
7
7
|
"repository": "https://github.com/isaacsight/kernel",
|
|
8
8
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernel.chat/kbot",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Open-source terminal AI agent.
|
|
3
|
+
"version": "3.63.0",
|
|
4
|
+
"description": "Open-source terminal AI agent. 676+ tools, 35 agents, 20 providers. Fully local, fully sovereign. MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|