@kernel.chat/kbot 1.3.0 → 2.3.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/README.md +94 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +576 -119
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts +20 -35
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +236 -66
- package/dist/auth.js.map +1 -1
- package/dist/auth.test.d.ts +2 -0
- package/dist/auth.test.d.ts.map +1 -0
- package/dist/auth.test.js +89 -0
- package/dist/auth.test.js.map +1 -0
- package/dist/build-targets.d.ts +37 -0
- package/dist/build-targets.d.ts.map +1 -0
- package/dist/build-targets.js +507 -0
- package/dist/build-targets.js.map +1 -0
- package/dist/cli.js +1211 -131
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +72 -22
- package/dist/context.js.map +1 -1
- package/dist/hooks.d.ts +27 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +145 -0
- package/dist/hooks.js.map +1 -0
- package/dist/ide/acp-server.d.ts +6 -0
- package/dist/ide/acp-server.d.ts.map +1 -0
- package/dist/ide/acp-server.js +319 -0
- package/dist/ide/acp-server.js.map +1 -0
- package/dist/ide/bridge.d.ts +128 -0
- package/dist/ide/bridge.d.ts.map +1 -0
- package/dist/ide/bridge.js +185 -0
- package/dist/ide/bridge.js.map +1 -0
- package/dist/ide/index.d.ts +5 -0
- package/dist/ide/index.d.ts.map +1 -0
- package/dist/ide/index.js +11 -0
- package/dist/ide/index.js.map +1 -0
- package/dist/ide/lsp-bridge.d.ts +27 -0
- package/dist/ide/lsp-bridge.d.ts.map +1 -0
- package/dist/ide/lsp-bridge.js +267 -0
- package/dist/ide/lsp-bridge.js.map +1 -0
- package/dist/ide/mcp-server.d.ts +7 -0
- package/dist/ide/mcp-server.d.ts.map +1 -0
- package/dist/ide/mcp-server.js +451 -0
- package/dist/ide/mcp-server.js.map +1 -0
- package/dist/learning.d.ts +179 -0
- package/dist/learning.d.ts.map +1 -0
- package/dist/learning.js +829 -0
- package/dist/learning.js.map +1 -0
- package/dist/learning.test.d.ts +2 -0
- package/dist/learning.test.d.ts.map +1 -0
- package/dist/learning.test.js +115 -0
- package/dist/learning.test.js.map +1 -0
- package/dist/matrix.d.ts +49 -0
- package/dist/matrix.d.ts.map +1 -0
- package/dist/matrix.js +302 -0
- package/dist/matrix.js.map +1 -0
- package/dist/memory.d.ts +11 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +54 -2
- package/dist/memory.js.map +1 -1
- package/dist/multimodal.d.ts +57 -0
- package/dist/multimodal.d.ts.map +1 -0
- package/dist/multimodal.js +206 -0
- package/dist/multimodal.js.map +1 -0
- package/dist/permissions.d.ts +21 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +122 -0
- package/dist/permissions.js.map +1 -0
- package/dist/planner.d.ts +54 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +298 -0
- package/dist/planner.js.map +1 -0
- package/dist/plugins.d.ts +30 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +135 -0
- package/dist/plugins.js.map +1 -0
- package/dist/sessions.d.ts +38 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +177 -0
- package/dist/sessions.js.map +1 -0
- package/dist/streaming.d.ts +88 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +317 -0
- package/dist/streaming.js.map +1 -0
- package/dist/tools/background.d.ts +2 -0
- package/dist/tools/background.d.ts.map +1 -0
- package/dist/tools/background.js +163 -0
- package/dist/tools/background.js.map +1 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +26 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/browser.js +7 -7
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/build-matrix.d.ts +2 -0
- package/dist/tools/build-matrix.d.ts.map +1 -0
- package/dist/tools/build-matrix.js +463 -0
- package/dist/tools/build-matrix.js.map +1 -0
- package/dist/tools/computer.js +5 -5
- package/dist/tools/computer.js.map +1 -1
- package/dist/tools/fetch.d.ts +2 -0
- package/dist/tools/fetch.d.ts.map +1 -0
- package/dist/tools/fetch.js +106 -0
- package/dist/tools/fetch.js.map +1 -0
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +112 -6
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/git.js +3 -3
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/github.d.ts +2 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/github.js +196 -0
- package/dist/tools/github.js.map +1 -0
- package/dist/tools/index.d.ts +29 -5
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +136 -20
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/index.test.d.ts +2 -0
- package/dist/tools/index.test.d.ts.map +1 -0
- package/dist/tools/index.test.js +162 -0
- package/dist/tools/index.test.js.map +1 -0
- package/dist/tools/matrix.d.ts +2 -0
- package/dist/tools/matrix.d.ts.map +1 -0
- package/dist/tools/matrix.js +79 -0
- package/dist/tools/matrix.js.map +1 -0
- package/dist/tools/mcp-client.d.ts +2 -0
- package/dist/tools/mcp-client.d.ts.map +1 -0
- package/dist/tools/mcp-client.js +295 -0
- package/dist/tools/mcp-client.js.map +1 -0
- package/dist/tools/notebook.d.ts +2 -0
- package/dist/tools/notebook.d.ts.map +1 -0
- package/dist/tools/notebook.js +207 -0
- package/dist/tools/notebook.js.map +1 -0
- package/dist/tools/openclaw.d.ts +2 -0
- package/dist/tools/openclaw.d.ts.map +1 -0
- package/dist/tools/openclaw.js +187 -0
- package/dist/tools/openclaw.js.map +1 -0
- package/dist/tools/parallel.d.ts +2 -0
- package/dist/tools/parallel.d.ts.map +1 -0
- package/dist/tools/parallel.js +60 -0
- package/dist/tools/parallel.js.map +1 -0
- package/dist/tools/sandbox.d.ts +2 -0
- package/dist/tools/sandbox.d.ts.map +1 -0
- package/dist/tools/sandbox.js +352 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +135 -28
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/subagent.d.ts +4 -0
- package/dist/tools/subagent.d.ts.map +1 -0
- package/dist/tools/subagent.js +260 -0
- package/dist/tools/subagent.js.map +1 -0
- package/dist/tools/tasks.d.ts +14 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +210 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/worktree.d.ts +2 -0
- package/dist/tools/worktree.d.ts.map +1 -0
- package/dist/tools/worktree.js +223 -0
- package/dist/tools/worktree.js.map +1 -0
- package/dist/tui.d.ts +73 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +257 -0
- package/dist/tui.js.map +1 -0
- package/dist/ui.d.ts +11 -19
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +143 -171
- package/dist/ui.js.map +1 -1
- package/dist/updater.d.ts +3 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +70 -0
- package/dist/updater.js.map +1 -0
- package/install.sh +5 -7
- package/package.json +8 -4
package/dist/learning.js
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
// K:BOT Recursive Language Learning Engine
|
|
2
|
+
//
|
|
3
|
+
// GOAL: Reduce token and message usage over time by learning from interactions.
|
|
4
|
+
//
|
|
5
|
+
// Three systems:
|
|
6
|
+
// 1. PATTERN CACHE — Cache successful tool sequences so repeat problems skip the API
|
|
7
|
+
// 2. SOLUTION INDEX — Extract reusable solutions from past conversations
|
|
8
|
+
// 3. USER PROFILE — Learn user preferences, style, and common workflows
|
|
9
|
+
//
|
|
10
|
+
// Everything persists to ~/.kbot/memory/ as JSON files.
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, writeFile, mkdirSync } from 'node:fs';
|
|
14
|
+
const LEARN_DIR = join(homedir(), '.kbot', 'memory');
|
|
15
|
+
const PATTERNS_FILE = join(LEARN_DIR, 'patterns.json');
|
|
16
|
+
const SOLUTIONS_FILE = join(LEARN_DIR, 'solutions.json');
|
|
17
|
+
const PROFILE_FILE = join(LEARN_DIR, 'profile.json');
|
|
18
|
+
const STATS_FILE = join(LEARN_DIR, 'stats.json');
|
|
19
|
+
const KNOWLEDGE_FILE = join(LEARN_DIR, 'knowledge.json');
|
|
20
|
+
const CORRECTIONS_FILE = join(LEARN_DIR, 'corrections.json');
|
|
21
|
+
const PROJECTS_FILE = join(LEARN_DIR, 'projects.json');
|
|
22
|
+
function ensureDir() {
|
|
23
|
+
if (!existsSync(LEARN_DIR))
|
|
24
|
+
mkdirSync(LEARN_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
function loadJSON(path, fallback) {
|
|
27
|
+
ensureDir();
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return fallback;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Debounced async file writer — batches multiple writes into one per file */
|
|
38
|
+
const pendingWrites = new Map();
|
|
39
|
+
const WRITE_DEBOUNCE_MS = 500;
|
|
40
|
+
function saveJSON(path, data) {
|
|
41
|
+
ensureDir();
|
|
42
|
+
// Cancel any pending write for this file
|
|
43
|
+
const existing = pendingWrites.get(path);
|
|
44
|
+
if (existing)
|
|
45
|
+
clearTimeout(existing);
|
|
46
|
+
// Debounce — only write after 500ms of no new saves to this file
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
pendingWrites.delete(path);
|
|
49
|
+
writeFile(path, JSON.stringify(data, null, 2), (err) => {
|
|
50
|
+
if (err) { /* non-critical — learning data can be regenerated */ }
|
|
51
|
+
});
|
|
52
|
+
}, WRITE_DEBOUNCE_MS);
|
|
53
|
+
pendingWrites.set(path, timer);
|
|
54
|
+
}
|
|
55
|
+
/** Synchronous save for critical data (config, not learning) */
|
|
56
|
+
function saveJSONSync(path, data) {
|
|
57
|
+
ensureDir();
|
|
58
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
59
|
+
}
|
|
60
|
+
/** Flush all pending writes immediately (call on exit) */
|
|
61
|
+
export function flushPendingWrites() {
|
|
62
|
+
for (const [path, timer] of pendingWrites.entries()) {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
pendingWrites.delete(path);
|
|
65
|
+
// Read the latest data reference — we need to save what's in memory
|
|
66
|
+
}
|
|
67
|
+
// Save current state of all mutable data
|
|
68
|
+
try {
|
|
69
|
+
writeFileSync(PATTERNS_FILE, JSON.stringify(patterns, null, 2));
|
|
70
|
+
writeFileSync(SOLUTIONS_FILE, JSON.stringify(solutions, null, 2));
|
|
71
|
+
writeFileSync(PROFILE_FILE, JSON.stringify(profile, null, 2));
|
|
72
|
+
writeFileSync(KNOWLEDGE_FILE, JSON.stringify(knowledge, null, 2));
|
|
73
|
+
writeFileSync(CORRECTIONS_FILE, JSON.stringify(corrections, null, 2));
|
|
74
|
+
writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2));
|
|
75
|
+
}
|
|
76
|
+
catch { /* best-effort */ }
|
|
77
|
+
}
|
|
78
|
+
let patterns = loadJSON(PATTERNS_FILE, []);
|
|
79
|
+
/** Normalize a message into an intent signature — preserves word order for context */
|
|
80
|
+
function normalizeIntent(message) {
|
|
81
|
+
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
|
82
|
+
'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
|
|
83
|
+
'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
|
|
84
|
+
'from', 'it', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'you', 'your',
|
|
85
|
+
'he', 'she', 'they', 'them', 'and', 'or', 'but', 'not', 'so', 'if', 'then', 'please']);
|
|
86
|
+
// Preserve word order (don't sort) — order carries intent context
|
|
87
|
+
return message.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
89
|
+
.split(/\s+/)
|
|
90
|
+
.filter(w => w.length > 2 && !stopWords.has(w))
|
|
91
|
+
.join(' ');
|
|
92
|
+
}
|
|
93
|
+
/** Extract keywords from a message */
|
|
94
|
+
export function extractKeywords(message) {
|
|
95
|
+
const techTerms = new Set(['react', 'typescript', 'node', 'python', 'rust', 'go', 'docker',
|
|
96
|
+
'api', 'database', 'test', 'deploy', 'build', 'fix', 'bug', 'error', 'component',
|
|
97
|
+
'function', 'class', 'import', 'export', 'async', 'await', 'fetch', 'route', 'auth',
|
|
98
|
+
'css', 'html', 'json', 'sql', 'git', 'npm', 'install', 'config', 'env', 'server',
|
|
99
|
+
'client', 'hook', 'state', 'redux', 'zustand', 'supabase', 'stripe', 'vite', 'webpack']);
|
|
100
|
+
return message.toLowerCase()
|
|
101
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
102
|
+
.split(/\s+/)
|
|
103
|
+
.filter(w => w.length > 2 && techTerms.has(w));
|
|
104
|
+
}
|
|
105
|
+
/** Find a matching cached pattern (similarity > 0.6) */
|
|
106
|
+
export function findPattern(message) {
|
|
107
|
+
const intent = normalizeIntent(message);
|
|
108
|
+
const keywords = extractKeywords(message);
|
|
109
|
+
if (!intent)
|
|
110
|
+
return null;
|
|
111
|
+
let bestMatch = null;
|
|
112
|
+
let bestScore = 0;
|
|
113
|
+
for (const p of patterns) {
|
|
114
|
+
// Jaccard similarity on intent words
|
|
115
|
+
const intentWords = new Set(intent.split(' '));
|
|
116
|
+
const patternWords = new Set(p.intent.split(' '));
|
|
117
|
+
const intersection = [...intentWords].filter(w => patternWords.has(w)).length;
|
|
118
|
+
const union = new Set([...intentWords, ...patternWords]).size;
|
|
119
|
+
const intentSim = union > 0 ? intersection / union : 0;
|
|
120
|
+
// Keyword overlap bonus
|
|
121
|
+
const kwOverlap = keywords.filter(k => p.keywords.includes(k)).length;
|
|
122
|
+
const kwBonus = keywords.length > 0 ? (kwOverlap / keywords.length) * 0.3 : 0;
|
|
123
|
+
// Frequency boost (well-tested patterns are more reliable)
|
|
124
|
+
const freqBoost = Math.min(p.hits / 10, 0.1);
|
|
125
|
+
const score = intentSim + kwBonus + freqBoost;
|
|
126
|
+
if (score > bestScore && score > 0.6 && p.successRate > 0.5) {
|
|
127
|
+
bestScore = score;
|
|
128
|
+
bestMatch = p;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return bestMatch;
|
|
132
|
+
}
|
|
133
|
+
/** Record a successful pattern */
|
|
134
|
+
export function recordPattern(message, toolSequence, tokensSaved = 0) {
|
|
135
|
+
if (toolSequence.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
const intent = normalizeIntent(message);
|
|
138
|
+
const keywords = extractKeywords(message);
|
|
139
|
+
if (!intent)
|
|
140
|
+
return;
|
|
141
|
+
const existing = patterns.find(p => p.intent === intent);
|
|
142
|
+
if (existing) {
|
|
143
|
+
existing.hits++;
|
|
144
|
+
existing.successRate = (existing.successRate * (existing.hits - 1) + 1) / existing.hits;
|
|
145
|
+
existing.lastUsed = new Date().toISOString();
|
|
146
|
+
existing.avgTokensSaved = (existing.avgTokensSaved * (existing.hits - 1) + tokensSaved) / existing.hits;
|
|
147
|
+
// Update tool sequence if this one is shorter (more efficient)
|
|
148
|
+
if (toolSequence.length < existing.toolSequence.length) {
|
|
149
|
+
existing.toolSequence = toolSequence;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
patterns.push({
|
|
154
|
+
intent, keywords, toolSequence,
|
|
155
|
+
hits: 1, successRate: 1.0,
|
|
156
|
+
lastUsed: new Date().toISOString(),
|
|
157
|
+
avgTokensSaved: tokensSaved,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// Keep top 100 patterns by score (hits * successRate)
|
|
161
|
+
patterns.sort((a, b) => (b.hits * b.successRate) - (a.hits * a.successRate));
|
|
162
|
+
patterns = patterns.slice(0, 100);
|
|
163
|
+
saveJSON(PATTERNS_FILE, patterns);
|
|
164
|
+
}
|
|
165
|
+
/** Record a failed pattern */
|
|
166
|
+
export function recordPatternFailure(message) {
|
|
167
|
+
const intent = normalizeIntent(message);
|
|
168
|
+
const existing = patterns.find(p => p.intent === intent);
|
|
169
|
+
if (existing) {
|
|
170
|
+
existing.successRate = (existing.successRate * existing.hits) / (existing.hits + 1);
|
|
171
|
+
existing.hits++;
|
|
172
|
+
saveJSON(PATTERNS_FILE, patterns);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let solutions = loadJSON(SOLUTIONS_FILE, []);
|
|
176
|
+
/** Find relevant cached solutions for a message */
|
|
177
|
+
export function findSolutions(message, maxResults = 3) {
|
|
178
|
+
const keywords = extractKeywords(message);
|
|
179
|
+
const messageWords = new Set(message.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
|
|
180
|
+
const scored = solutions.map(s => {
|
|
181
|
+
const kwOverlap = keywords.filter(k => s.keywords.includes(k)).length;
|
|
182
|
+
const kwScore = keywords.length > 0 ? kwOverlap / keywords.length : 0;
|
|
183
|
+
// Word overlap with question
|
|
184
|
+
const qWords = new Set(s.question.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
|
|
185
|
+
const overlap = [...messageWords].filter(w => qWords.has(w)).length;
|
|
186
|
+
const wordScore = qWords.size > 0 ? overlap / qWords.size : 0;
|
|
187
|
+
const score = kwScore * 0.4 + wordScore * 0.4 + s.confidence * 0.2;
|
|
188
|
+
return { solution: s, score };
|
|
189
|
+
});
|
|
190
|
+
const results = scored
|
|
191
|
+
.filter(s => s.score > 0.3)
|
|
192
|
+
.sort((a, b) => b.score - a.score)
|
|
193
|
+
.slice(0, maxResults)
|
|
194
|
+
.map(s => s.solution);
|
|
195
|
+
// Update reuse counts separately (don't mutate during search)
|
|
196
|
+
if (results.length > 0) {
|
|
197
|
+
for (const s of results)
|
|
198
|
+
s.reuses++;
|
|
199
|
+
saveJSON(SOLUTIONS_FILE, solutions);
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
/** Cache a solution from a successful interaction */
|
|
204
|
+
export function cacheSolution(question, solution) {
|
|
205
|
+
if (solution.length < 20 || solution.length > 5000)
|
|
206
|
+
return;
|
|
207
|
+
const keywords = extractKeywords(question);
|
|
208
|
+
const normalized = normalizeIntent(question);
|
|
209
|
+
// Don't duplicate similar solutions
|
|
210
|
+
const existing = solutions.find(s => normalizeIntent(s.question) === normalized);
|
|
211
|
+
if (existing) {
|
|
212
|
+
existing.confidence = Math.min(1, existing.confidence + 0.1);
|
|
213
|
+
existing.solution = solution; // Update with latest
|
|
214
|
+
saveJSON(SOLUTIONS_FILE, solutions);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
solutions.push({
|
|
218
|
+
question: question.slice(0, 200),
|
|
219
|
+
keywords,
|
|
220
|
+
solution: solution.slice(0, 3000),
|
|
221
|
+
confidence: 0.7,
|
|
222
|
+
reuses: 0,
|
|
223
|
+
created: new Date().toISOString(),
|
|
224
|
+
});
|
|
225
|
+
// Keep top 200 solutions
|
|
226
|
+
solutions.sort((a, b) => (b.confidence * (b.reuses + 1)) - (a.confidence * (a.reuses + 1)));
|
|
227
|
+
solutions = solutions.slice(0, 200);
|
|
228
|
+
saveJSON(SOLUTIONS_FILE, solutions);
|
|
229
|
+
}
|
|
230
|
+
let profile = loadJSON(PROFILE_FILE, {
|
|
231
|
+
responseStyle: 'auto',
|
|
232
|
+
techStack: [],
|
|
233
|
+
taskPatterns: {},
|
|
234
|
+
preferredAgents: {},
|
|
235
|
+
totalMessages: 0,
|
|
236
|
+
totalTokens: 0,
|
|
237
|
+
tokensSaved: 0,
|
|
238
|
+
avgTokensPerMessage: 0,
|
|
239
|
+
sessions: 0,
|
|
240
|
+
});
|
|
241
|
+
/** Tech stack usage frequency for decay-based ranking */
|
|
242
|
+
let techStackFrequency = loadJSON(join(LEARN_DIR, 'tech-freq.json'), {});
|
|
243
|
+
export function getProfile() {
|
|
244
|
+
return profile;
|
|
245
|
+
}
|
|
246
|
+
/** Update profile after each interaction */
|
|
247
|
+
export function updateProfile(opts) {
|
|
248
|
+
if (opts.tokens) {
|
|
249
|
+
profile.totalMessages++;
|
|
250
|
+
profile.totalTokens += opts.tokens;
|
|
251
|
+
profile.avgTokensPerMessage =
|
|
252
|
+
profile.totalTokens / profile.totalMessages;
|
|
253
|
+
}
|
|
254
|
+
if (opts.tokensSaved) {
|
|
255
|
+
profile.tokensSaved += opts.tokensSaved;
|
|
256
|
+
}
|
|
257
|
+
if (opts.agent && opts.agent !== 'local') {
|
|
258
|
+
profile.preferredAgents[opts.agent] = (profile.preferredAgents[opts.agent] || 0) + 1;
|
|
259
|
+
}
|
|
260
|
+
if (opts.taskType) {
|
|
261
|
+
profile.taskPatterns[opts.taskType] = (profile.taskPatterns[opts.taskType] || 0) + 1;
|
|
262
|
+
}
|
|
263
|
+
if (opts.techTerms && opts.techTerms.length > 0) {
|
|
264
|
+
// Tech stack with frequency tracking for decay
|
|
265
|
+
if (!techStackFrequency)
|
|
266
|
+
techStackFrequency = {};
|
|
267
|
+
for (const t of opts.techTerms) {
|
|
268
|
+
techStackFrequency[t] = (techStackFrequency[t] || 0) + 1;
|
|
269
|
+
}
|
|
270
|
+
// Rebuild techStack from frequency — most used terms first, with decay
|
|
271
|
+
profile.techStack = Object.entries(techStackFrequency)
|
|
272
|
+
.sort((a, b) => b[1] - a[1])
|
|
273
|
+
.slice(0, 20)
|
|
274
|
+
.map(([term]) => term);
|
|
275
|
+
}
|
|
276
|
+
saveJSON(PROFILE_FILE, profile);
|
|
277
|
+
}
|
|
278
|
+
export function incrementSessions() {
|
|
279
|
+
profile.sessions++;
|
|
280
|
+
saveJSON(PROFILE_FILE, profile);
|
|
281
|
+
return profile.sessions;
|
|
282
|
+
}
|
|
283
|
+
// ═══ 4. LEARNING CONTEXT BUILDER ════════════════════════════════
|
|
284
|
+
// Builds the most efficient context for each message by combining
|
|
285
|
+
// cached patterns, relevant solutions, and user profile.
|
|
286
|
+
// This replaces dumping the entire memory file into every prompt.
|
|
287
|
+
export function buildLearningContext(message) {
|
|
288
|
+
const parts = [];
|
|
289
|
+
// A. Pattern hint — if we've solved this type of problem before
|
|
290
|
+
const pattern = findPattern(message);
|
|
291
|
+
if (pattern) {
|
|
292
|
+
parts.push(`[Learned Pattern — ${pattern.hits}x success, ${Math.round(pattern.successRate * 100)}% rate]`, `Similar tasks were solved with: ${pattern.toolSequence.join(' → ')}`, `Hint: follow this tool sequence to solve efficiently in fewer steps.`);
|
|
293
|
+
}
|
|
294
|
+
// B. Relevant solutions — inject only matching ones, not entire history
|
|
295
|
+
const relevant = findSolutions(message, 2);
|
|
296
|
+
if (relevant.length > 0) {
|
|
297
|
+
parts.push('[Cached Solutions]');
|
|
298
|
+
for (const s of relevant) {
|
|
299
|
+
parts.push(`Q: ${s.question}\nA: ${s.solution}\n`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// C. User profile hints — help the AI match user expectations
|
|
303
|
+
if (profile.totalMessages > 5) {
|
|
304
|
+
const hints = [];
|
|
305
|
+
if (profile.techStack.length > 0) {
|
|
306
|
+
hints.push(`User's stack: ${profile.techStack.join(', ')}`);
|
|
307
|
+
}
|
|
308
|
+
if (profile.responseStyle !== 'auto') {
|
|
309
|
+
hints.push(`Preferred style: ${profile.responseStyle}`);
|
|
310
|
+
}
|
|
311
|
+
// Most common task type
|
|
312
|
+
const topTask = Object.entries(profile.taskPatterns)
|
|
313
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
314
|
+
if (topTask && topTask[1] > 3) {
|
|
315
|
+
hints.push(`Most common task: ${topTask[0]}`);
|
|
316
|
+
}
|
|
317
|
+
if (hints.length > 0) {
|
|
318
|
+
parts.push(`[User Profile]\n${hints.join('\n')}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return parts.length > 0 ? parts.join('\n\n') : '';
|
|
322
|
+
}
|
|
323
|
+
export function getStats() {
|
|
324
|
+
// Baseline: assume 2000 tokens per message without learning
|
|
325
|
+
const baseline = 2000;
|
|
326
|
+
const actual = profile.avgTokensPerMessage || baseline;
|
|
327
|
+
const efficiencyPct = actual < baseline
|
|
328
|
+
? Math.round((1 - actual / baseline) * 100)
|
|
329
|
+
: 0;
|
|
330
|
+
return {
|
|
331
|
+
patternsCount: patterns.length,
|
|
332
|
+
solutionsCount: solutions.length,
|
|
333
|
+
totalTokensSaved: profile.tokensSaved,
|
|
334
|
+
avgTokensPerMsg: Math.round(profile.avgTokensPerMessage),
|
|
335
|
+
totalMessages: profile.totalMessages,
|
|
336
|
+
sessions: profile.sessions,
|
|
337
|
+
efficiency: efficiencyPct > 0 ? `${efficiencyPct}% more efficient` : 'Baseline (learning...)',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** Classify task type from message */
|
|
341
|
+
export function classifyTask(message) {
|
|
342
|
+
const lower = message.toLowerCase();
|
|
343
|
+
if (/\b(fix|bug|error|issue|broken|crash|fail)\b/.test(lower))
|
|
344
|
+
return 'debug';
|
|
345
|
+
if (/\b(build|create|scaffold|generate|new|init|setup)\b/.test(lower))
|
|
346
|
+
return 'build';
|
|
347
|
+
if (/\b(refactor|clean|reorganize|restructure|simplify)\b/.test(lower))
|
|
348
|
+
return 'refactor';
|
|
349
|
+
if (/\b(test|spec|coverage|assert)\b/.test(lower))
|
|
350
|
+
return 'test';
|
|
351
|
+
if (/\b(deploy|ship|release|publish)\b/.test(lower))
|
|
352
|
+
return 'deploy';
|
|
353
|
+
if (/\b(explain|what|how|why|describe)\b/.test(lower))
|
|
354
|
+
return 'explain';
|
|
355
|
+
if (/\b(review|audit|check|inspect)\b/.test(lower))
|
|
356
|
+
return 'review';
|
|
357
|
+
if (/\b(search|find|grep|locate|where)\b/.test(lower))
|
|
358
|
+
return 'search';
|
|
359
|
+
return 'general';
|
|
360
|
+
}
|
|
361
|
+
let knowledge = loadJSON(KNOWLEDGE_FILE, []);
|
|
362
|
+
/** Store a knowledge entry (user teaches kbot something) */
|
|
363
|
+
export function learnFact(fact, category = 'fact', source = 'user-taught') {
|
|
364
|
+
if (!fact || fact.length < 5)
|
|
365
|
+
return;
|
|
366
|
+
const keywords = fact.toLowerCase()
|
|
367
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
368
|
+
.split(/\s+/)
|
|
369
|
+
.filter(w => w.length > 2);
|
|
370
|
+
// Check for duplicate or similar (use spread to avoid mutating stored arrays)
|
|
371
|
+
const normalized = [...keywords].sort().join(' ');
|
|
372
|
+
const existing = knowledge.find(k => {
|
|
373
|
+
const kNorm = [...k.keywords].sort().join(' ');
|
|
374
|
+
return kNorm === normalized;
|
|
375
|
+
});
|
|
376
|
+
if (existing) {
|
|
377
|
+
existing.fact = fact; // Update with latest wording
|
|
378
|
+
existing.confidence = Math.min(1, existing.confidence + 0.1);
|
|
379
|
+
existing.lastUsed = new Date().toISOString();
|
|
380
|
+
saveJSON(KNOWLEDGE_FILE, knowledge);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
knowledge.push({
|
|
384
|
+
fact,
|
|
385
|
+
category,
|
|
386
|
+
keywords,
|
|
387
|
+
source,
|
|
388
|
+
confidence: source === 'user-taught' ? 1.0 : 0.7,
|
|
389
|
+
references: 0,
|
|
390
|
+
created: new Date().toISOString(),
|
|
391
|
+
lastUsed: new Date().toISOString(),
|
|
392
|
+
});
|
|
393
|
+
// Keep top 500 knowledge entries
|
|
394
|
+
knowledge.sort((a, b) => (b.confidence * (b.references + 1)) - (a.confidence * (a.references + 1)));
|
|
395
|
+
knowledge = knowledge.slice(0, 500);
|
|
396
|
+
saveJSON(KNOWLEDGE_FILE, knowledge);
|
|
397
|
+
}
|
|
398
|
+
/** Find relevant knowledge for a message */
|
|
399
|
+
export function findKnowledge(message, maxResults = 5) {
|
|
400
|
+
const msgWords = new Set(message.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
|
|
401
|
+
const scored = knowledge.map(k => {
|
|
402
|
+
const overlap = k.keywords.filter(kw => msgWords.has(kw)).length;
|
|
403
|
+
const score = k.keywords.length > 0
|
|
404
|
+
? (overlap / k.keywords.length) * 0.6 + k.confidence * 0.3 + Math.min(k.references / 10, 0.1)
|
|
405
|
+
: 0;
|
|
406
|
+
return { entry: k, score };
|
|
407
|
+
});
|
|
408
|
+
const results = scored
|
|
409
|
+
.filter(s => s.score > 0.2)
|
|
410
|
+
.sort((a, b) => b.score - a.score)
|
|
411
|
+
.slice(0, maxResults)
|
|
412
|
+
.map(s => s.entry);
|
|
413
|
+
// Update reference counts separately (don't mutate during search)
|
|
414
|
+
if (results.length > 0) {
|
|
415
|
+
const now = new Date().toISOString();
|
|
416
|
+
for (const entry of results) {
|
|
417
|
+
entry.references++;
|
|
418
|
+
entry.lastUsed = now;
|
|
419
|
+
}
|
|
420
|
+
saveJSON(KNOWLEDGE_FILE, knowledge);
|
|
421
|
+
}
|
|
422
|
+
return results;
|
|
423
|
+
}
|
|
424
|
+
/** Extract knowledge from a conversation exchange — stricter matching to reduce false positives */
|
|
425
|
+
export function extractKnowledge(userMessage, assistantResponse) {
|
|
426
|
+
const lower = userMessage.toLowerCase().trim();
|
|
427
|
+
// Skip short messages and questions — they rarely contain teachable facts
|
|
428
|
+
if (lower.length < 15 || lower.endsWith('?'))
|
|
429
|
+
return;
|
|
430
|
+
// Only extract from messages that are clearly declarative/directive
|
|
431
|
+
// Require explicit teaching verbs at the START of the message or clause
|
|
432
|
+
const teachPatterns = [
|
|
433
|
+
/^(?:remember|note that|keep in mind)\s+(.{10,200})/i,
|
|
434
|
+
/^(?:always|never)\s+(.{10,200})/i,
|
|
435
|
+
/^(?:i (?:always |)(?:prefer|like|want|use|need))\s+(.{10,200})/i,
|
|
436
|
+
/^(?:my\s+\w+\s+(?:is|are|uses?|runs?|has))\s+(.{5,200})/i,
|
|
437
|
+
/^(?:we use|our\s+\w+\s+(?:is|are|uses?))\s+(.{5,200})/i,
|
|
438
|
+
// Also match after explicit "btw" / "fyi" / "also"
|
|
439
|
+
/(?:btw|fyi|also)[,:]?\s+(?:my|our|we|i)\s+(.{10,200})/i,
|
|
440
|
+
];
|
|
441
|
+
for (const pattern of teachPatterns) {
|
|
442
|
+
const match = lower.match(pattern);
|
|
443
|
+
if (match) {
|
|
444
|
+
const fact = match[0].charAt(0).toUpperCase() + match[0].slice(1);
|
|
445
|
+
const category = /(?:always|never|prefer|like|want)/.test(lower) ? 'preference' :
|
|
446
|
+
/(?:my|our|we)/.test(lower) ? 'context' : 'fact';
|
|
447
|
+
learnFact(fact, category, 'extracted');
|
|
448
|
+
return; // Only extract one fact per message to avoid noise
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Detect corrections — require explicit correction language
|
|
452
|
+
const correctionPrefixes = /^(?:no[,.]?\s+(?:it|that|you)|that'?s\s+(?:wrong|incorrect|not right)|actually[,]?\s+(?:it|you|that))/i;
|
|
453
|
+
if (correctionPrefixes.test(lower)) {
|
|
454
|
+
const correctionMatch = lower.match(/(?:no[,.]?\s+|actually[,]?\s+|instead[,]?\s+|should\s+(?:be|use)\s+)(.{10,200})/i);
|
|
455
|
+
if (correctionMatch) {
|
|
456
|
+
recordCorrection(userMessage, assistantResponse);
|
|
457
|
+
learnFact(correctionMatch[1], 'rule', 'extracted');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Detect project-specific knowledge — only from explicit project declarations
|
|
461
|
+
const projectPattern = /^(?:this (?:project|repo|app)|the codebase|our stack)\s+(?:is|uses?|has|runs?)\s+(.{5,200})/i;
|
|
462
|
+
const projectMatch = lower.match(projectPattern);
|
|
463
|
+
if (projectMatch) {
|
|
464
|
+
learnFact(projectMatch[0], 'project', 'extracted');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
let corrections = loadJSON(CORRECTIONS_FILE, []);
|
|
468
|
+
/** Record a user correction */
|
|
469
|
+
export function recordCorrection(userMessage, wrongResponse) {
|
|
470
|
+
const rule = userMessage.slice(0, 300);
|
|
471
|
+
// Deduplicate
|
|
472
|
+
const existing = corrections.find(c => normalizeIntent(c.userMessage) === normalizeIntent(userMessage));
|
|
473
|
+
if (existing) {
|
|
474
|
+
existing.occurrences++;
|
|
475
|
+
saveJSON(CORRECTIONS_FILE, corrections);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
corrections.push({
|
|
479
|
+
userMessage: userMessage.slice(0, 300),
|
|
480
|
+
wrongResponse: wrongResponse.slice(0, 300),
|
|
481
|
+
rule,
|
|
482
|
+
occurrences: 1,
|
|
483
|
+
created: new Date().toISOString(),
|
|
484
|
+
});
|
|
485
|
+
// Keep top 50 corrections
|
|
486
|
+
corrections.sort((a, b) => b.occurrences - a.occurrences);
|
|
487
|
+
corrections = corrections.slice(0, 50);
|
|
488
|
+
saveJSON(CORRECTIONS_FILE, corrections);
|
|
489
|
+
}
|
|
490
|
+
/** Get relevant corrections to avoid repeating mistakes */
|
|
491
|
+
export function getRelevantCorrections(message, max = 3) {
|
|
492
|
+
const msgWords = new Set(message.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
|
|
493
|
+
return corrections
|
|
494
|
+
.map(c => {
|
|
495
|
+
const cWords = new Set(c.userMessage.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2));
|
|
496
|
+
const overlap = [...msgWords].filter(w => cWords.has(w)).length;
|
|
497
|
+
const score = cWords.size > 0 ? overlap / cWords.size : 0;
|
|
498
|
+
return { correction: c, score };
|
|
499
|
+
})
|
|
500
|
+
.filter(s => s.score > 0.3)
|
|
501
|
+
.sort((a, b) => b.score - a.score)
|
|
502
|
+
.slice(0, max)
|
|
503
|
+
.map(s => s.correction);
|
|
504
|
+
}
|
|
505
|
+
let projects = loadJSON(PROJECTS_FILE, []);
|
|
506
|
+
/** Get or create project memory for current directory */
|
|
507
|
+
export function getProjectMemory(cwd) {
|
|
508
|
+
const project = projects.find(p => cwd.startsWith(p.path));
|
|
509
|
+
if (project) {
|
|
510
|
+
project.lastAccessed = new Date().toISOString();
|
|
511
|
+
saveJSON(PROJECTS_FILE, projects);
|
|
512
|
+
}
|
|
513
|
+
return project || null;
|
|
514
|
+
}
|
|
515
|
+
/** Record project information */
|
|
516
|
+
export function updateProjectMemory(cwd, data) {
|
|
517
|
+
let project = projects.find(p => p.path === cwd);
|
|
518
|
+
if (!project) {
|
|
519
|
+
project = {
|
|
520
|
+
path: cwd,
|
|
521
|
+
name: data.name || cwd.split('/').pop() || 'unknown',
|
|
522
|
+
stack: [],
|
|
523
|
+
frequentFiles: {},
|
|
524
|
+
notes: [],
|
|
525
|
+
lastAccessed: new Date().toISOString(),
|
|
526
|
+
};
|
|
527
|
+
projects.push(project);
|
|
528
|
+
}
|
|
529
|
+
if (data.name)
|
|
530
|
+
project.name = data.name;
|
|
531
|
+
if (data.stack) {
|
|
532
|
+
const existing = new Set(project.stack);
|
|
533
|
+
for (const s of data.stack) {
|
|
534
|
+
if (!existing.has(s)) {
|
|
535
|
+
project.stack.push(s);
|
|
536
|
+
existing.add(s);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
project.stack = project.stack.slice(0, 20);
|
|
540
|
+
}
|
|
541
|
+
if (data.file) {
|
|
542
|
+
project.frequentFiles[data.file] = (project.frequentFiles[data.file] || 0) + 1;
|
|
543
|
+
// Keep top 30 files
|
|
544
|
+
const sorted = Object.entries(project.frequentFiles).sort((a, b) => b[1] - a[1]).slice(0, 30);
|
|
545
|
+
project.frequentFiles = Object.fromEntries(sorted);
|
|
546
|
+
}
|
|
547
|
+
if (data.note && !project.notes.includes(data.note)) {
|
|
548
|
+
project.notes.push(data.note);
|
|
549
|
+
project.notes = project.notes.slice(-50);
|
|
550
|
+
}
|
|
551
|
+
project.lastAccessed = new Date().toISOString();
|
|
552
|
+
// Keep top 20 projects
|
|
553
|
+
projects.sort((a, b) => new Date(b.lastAccessed).getTime() - new Date(a.lastAccessed).getTime());
|
|
554
|
+
projects = projects.slice(0, 20);
|
|
555
|
+
saveJSON(PROJECTS_FILE, projects);
|
|
556
|
+
}
|
|
557
|
+
// ═══ ENHANCED CONTEXT BUILDER — Uses all learning systems ═══════
|
|
558
|
+
/** Override the original buildLearningContext with enhanced version */
|
|
559
|
+
// Enhance the existing buildLearningContext
|
|
560
|
+
export function buildFullLearningContext(message, cwd) {
|
|
561
|
+
const parts = [];
|
|
562
|
+
// A. Pattern hint
|
|
563
|
+
const pattern = findPattern(message);
|
|
564
|
+
if (pattern) {
|
|
565
|
+
parts.push(`[Learned Pattern — ${pattern.hits}x success]`, `Tool sequence: ${pattern.toolSequence.join(' → ')}`);
|
|
566
|
+
}
|
|
567
|
+
// B. Relevant solutions
|
|
568
|
+
const relevant = findSolutions(message, 2);
|
|
569
|
+
if (relevant.length > 0) {
|
|
570
|
+
parts.push('[Cached Solutions]');
|
|
571
|
+
for (const s of relevant) {
|
|
572
|
+
parts.push(`Q: ${s.question}\nA: ${s.solution}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// C. Relevant knowledge — things the user has taught
|
|
576
|
+
const knowledgeEntries = findKnowledge(message, 4);
|
|
577
|
+
if (knowledgeEntries.length > 0) {
|
|
578
|
+
parts.push('[User Knowledge]');
|
|
579
|
+
for (const k of knowledgeEntries) {
|
|
580
|
+
const tag = k.source === 'user-taught' ? '(user said)' : '(learned)';
|
|
581
|
+
parts.push(`• ${k.fact} ${tag}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// D. Corrections — avoid repeating past mistakes
|
|
585
|
+
const relevantCorrections = getRelevantCorrections(message, 2);
|
|
586
|
+
if (relevantCorrections.length > 0) {
|
|
587
|
+
parts.push('[Past Corrections — avoid these mistakes]');
|
|
588
|
+
for (const c of relevantCorrections) {
|
|
589
|
+
parts.push(`• User corrected: "${c.rule}"`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// E. User profile
|
|
593
|
+
if (profile.totalMessages > 3) {
|
|
594
|
+
const hints = [];
|
|
595
|
+
if (profile.techStack.length > 0) {
|
|
596
|
+
hints.push(`Stack: ${profile.techStack.join(', ')}`);
|
|
597
|
+
}
|
|
598
|
+
if (profile.responseStyle !== 'auto') {
|
|
599
|
+
hints.push(`Style: ${profile.responseStyle}`);
|
|
600
|
+
}
|
|
601
|
+
const topTask = Object.entries(profile.taskPatterns).sort((a, b) => b[1] - a[1])[0];
|
|
602
|
+
if (topTask && topTask[1] > 2) {
|
|
603
|
+
hints.push(`Common task: ${topTask[0]}`);
|
|
604
|
+
}
|
|
605
|
+
if (hints.length > 0) {
|
|
606
|
+
parts.push(`[User Profile] ${hints.join(' · ')}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// F. Project memory — if working in a known project
|
|
610
|
+
if (cwd) {
|
|
611
|
+
const project = getProjectMemory(cwd);
|
|
612
|
+
if (project) {
|
|
613
|
+
const projectHints = [`Project: ${project.name}`];
|
|
614
|
+
if (project.stack.length > 0)
|
|
615
|
+
projectHints.push(`Stack: ${project.stack.join(', ')}`);
|
|
616
|
+
if (project.notes.length > 0) {
|
|
617
|
+
projectHints.push('Notes:');
|
|
618
|
+
for (const note of project.notes.slice(-5)) {
|
|
619
|
+
projectHints.push(` • ${note}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const topFiles = Object.entries(project.frequentFiles)
|
|
623
|
+
.sort((a, b) => b[1] - a[1])
|
|
624
|
+
.slice(0, 5)
|
|
625
|
+
.map(([f]) => f);
|
|
626
|
+
if (topFiles.length > 0) {
|
|
627
|
+
projectHints.push(`Frequent files: ${topFiles.join(', ')}`);
|
|
628
|
+
}
|
|
629
|
+
parts.push(`[Project Context]\n${projectHints.join('\n')}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return parts.length > 0 ? parts.join('\n\n') : '';
|
|
633
|
+
}
|
|
634
|
+
// ═══ 9. CONVERSATION LEARNING — Post-interaction extraction ═════
|
|
635
|
+
// Called after each exchange to extract and store learnings.
|
|
636
|
+
export function learnFromExchange(userMessage, assistantResponse, toolsUsed, cwd) {
|
|
637
|
+
// Extract knowledge from what the user said
|
|
638
|
+
extractKnowledge(userMessage, assistantResponse);
|
|
639
|
+
// Track file usage in project memory
|
|
640
|
+
if (cwd && toolsUsed.length > 0) {
|
|
641
|
+
const fileTools = ['read_file', 'write_file', 'edit_file', 'multi_file_write'];
|
|
642
|
+
// Try to extract file paths from tool names (simplified)
|
|
643
|
+
for (const tool of toolsUsed) {
|
|
644
|
+
if (fileTools.includes(tool)) {
|
|
645
|
+
// The actual file paths would need to come from tool args — for now just track the tools
|
|
646
|
+
updateProjectMemory(cwd, { stack: extractKeywords(userMessage) });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// Detect style preference from response feedback
|
|
651
|
+
const lower = userMessage.toLowerCase();
|
|
652
|
+
if (/(?:too (?:long|verbose|detailed)|shorter|tldr|brief)/i.test(lower)) {
|
|
653
|
+
profile.responseStyle = 'concise';
|
|
654
|
+
saveJSON(PROFILE_FILE, profile);
|
|
655
|
+
}
|
|
656
|
+
if (/(?:more detail|elaborate|explain more|too short|too brief)/i.test(lower)) {
|
|
657
|
+
profile.responseStyle = 'detailed';
|
|
658
|
+
saveJSON(PROFILE_FILE, profile);
|
|
659
|
+
}
|
|
660
|
+
// Save periodic stats
|
|
661
|
+
saveJSON(KNOWLEDGE_FILE, knowledge);
|
|
662
|
+
}
|
|
663
|
+
// ═══ 10. LEARNING STATS — Extended ══════════════════════════════
|
|
664
|
+
// ═══ 11. SELF-TRAINING — Periodic knowledge review & synthesis ════
|
|
665
|
+
// kbot reviews its own knowledge base, prunes stale entries,
|
|
666
|
+
// synthesizes cross-pattern insights, and optimizes the learning engine.
|
|
667
|
+
const TRAINING_FILE = join(LEARN_DIR, 'training-log.json');
|
|
668
|
+
let trainingLog = loadJSON(TRAINING_FILE, {
|
|
669
|
+
lastRun: '',
|
|
670
|
+
runsTotal: 0,
|
|
671
|
+
entriesPruned: 0,
|
|
672
|
+
insightsSynthesized: 0,
|
|
673
|
+
patternsOptimized: 0,
|
|
674
|
+
});
|
|
675
|
+
/** Run self-training: prune stale knowledge, optimize patterns, synthesize insights */
|
|
676
|
+
export function selfTrain() {
|
|
677
|
+
let pruned = 0;
|
|
678
|
+
let optimized = 0;
|
|
679
|
+
let synthesized = 0;
|
|
680
|
+
const summaryParts = [];
|
|
681
|
+
// ── A. Prune stale patterns ──
|
|
682
|
+
// Remove patterns with low success rate or no recent hits
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
|
|
685
|
+
const beforePatterns = patterns.length;
|
|
686
|
+
patterns = patterns.filter(p => {
|
|
687
|
+
const lastUsed = new Date(p.lastUsed).getTime();
|
|
688
|
+
// Keep if: recent, high success rate, or frequently used
|
|
689
|
+
if (lastUsed > thirtyDaysAgo)
|
|
690
|
+
return true;
|
|
691
|
+
if (p.successRate > 0.8 && p.hits > 5)
|
|
692
|
+
return true;
|
|
693
|
+
if (p.hits > 10)
|
|
694
|
+
return true;
|
|
695
|
+
pruned++;
|
|
696
|
+
return false;
|
|
697
|
+
});
|
|
698
|
+
if (pruned > 0) {
|
|
699
|
+
saveJSON(PATTERNS_FILE, patterns);
|
|
700
|
+
summaryParts.push(`Pruned ${pruned} stale patterns (${beforePatterns} → ${patterns.length})`);
|
|
701
|
+
}
|
|
702
|
+
// ── B. Prune low-confidence knowledge ──
|
|
703
|
+
const beforeKnowledge = knowledge.length;
|
|
704
|
+
knowledge = knowledge.filter(k => {
|
|
705
|
+
// Keep all user-taught facts
|
|
706
|
+
if (k.source === 'user-taught')
|
|
707
|
+
return true;
|
|
708
|
+
// Keep high-confidence or frequently referenced
|
|
709
|
+
if (k.confidence > 0.5 && k.references > 0)
|
|
710
|
+
return true;
|
|
711
|
+
// Keep recent entries (< 7 days)
|
|
712
|
+
const created = new Date(k.created).getTime();
|
|
713
|
+
if (created > now - 7 * 24 * 60 * 60 * 1000)
|
|
714
|
+
return true;
|
|
715
|
+
pruned++;
|
|
716
|
+
return false;
|
|
717
|
+
});
|
|
718
|
+
if (knowledge.length < beforeKnowledge) {
|
|
719
|
+
saveJSON(KNOWLEDGE_FILE, knowledge);
|
|
720
|
+
summaryParts.push(`Pruned ${beforeKnowledge - knowledge.length} low-confidence knowledge entries`);
|
|
721
|
+
}
|
|
722
|
+
// ── C. Optimize patterns — merge similar ones ──
|
|
723
|
+
const mergedPatterns = new Map();
|
|
724
|
+
for (const p of patterns) {
|
|
725
|
+
const key = p.toolSequence.join(',');
|
|
726
|
+
const existing = mergedPatterns.get(key);
|
|
727
|
+
if (existing && p.intent !== existing.intent) {
|
|
728
|
+
// Same tool sequence, different intent — merge keywords
|
|
729
|
+
existing.keywords = [...new Set([...existing.keywords, ...p.keywords])];
|
|
730
|
+
existing.hits += p.hits;
|
|
731
|
+
existing.successRate = (existing.successRate + p.successRate) / 2;
|
|
732
|
+
optimized++;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
mergedPatterns.set(key, { ...p });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (optimized > 0) {
|
|
739
|
+
patterns = Array.from(mergedPatterns.values());
|
|
740
|
+
saveJSON(PATTERNS_FILE, patterns);
|
|
741
|
+
summaryParts.push(`Merged ${optimized} redundant patterns`);
|
|
742
|
+
}
|
|
743
|
+
// ── D. Synthesize cross-pattern insights ──
|
|
744
|
+
// Find common tool sequences across patterns to identify power workflows
|
|
745
|
+
const toolFrequency = {};
|
|
746
|
+
for (const p of patterns) {
|
|
747
|
+
for (const tool of p.toolSequence) {
|
|
748
|
+
toolFrequency[tool] = (toolFrequency[tool] || 0) + p.hits;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const topTools = Object.entries(toolFrequency)
|
|
752
|
+
.sort((a, b) => b[1] - a[1])
|
|
753
|
+
.slice(0, 5);
|
|
754
|
+
if (topTools.length > 0) {
|
|
755
|
+
const insight = `Most effective tools: ${topTools.map(([t, n]) => `${t}(${n}x)`).join(', ')}`;
|
|
756
|
+
const existingInsight = knowledge.find(k => k.fact.startsWith('Most effective tools:'));
|
|
757
|
+
if (existingInsight) {
|
|
758
|
+
existingInsight.fact = insight;
|
|
759
|
+
existingInsight.lastUsed = new Date().toISOString();
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
learnFact(insight, 'context', 'observed');
|
|
763
|
+
synthesized++;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// Synthesize user task preference insights
|
|
767
|
+
const topTasks = Object.entries(profile.taskPatterns)
|
|
768
|
+
.sort((a, b) => b[1] - a[1])
|
|
769
|
+
.slice(0, 3);
|
|
770
|
+
if (topTasks.length > 0 && profile.totalMessages > 10) {
|
|
771
|
+
const insight = `User primarily does: ${topTasks.map(([t, n]) => `${t}(${n}x)`).join(', ')}`;
|
|
772
|
+
const existing = knowledge.find(k => k.fact.startsWith('User primarily does:'));
|
|
773
|
+
if (existing) {
|
|
774
|
+
existing.fact = insight;
|
|
775
|
+
existing.lastUsed = new Date().toISOString();
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
learnFact(insight, 'context', 'observed');
|
|
779
|
+
synthesized++;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Synthesize solution success patterns
|
|
783
|
+
const highConfSolutions = solutions.filter(s => s.confidence > 0.9 && s.reuses > 2);
|
|
784
|
+
if (highConfSolutions.length > 0) {
|
|
785
|
+
summaryParts.push(`${highConfSolutions.length} battle-tested solutions (>90% confidence, 2+ reuses)`);
|
|
786
|
+
}
|
|
787
|
+
if (synthesized > 0) {
|
|
788
|
+
saveJSON(KNOWLEDGE_FILE, knowledge);
|
|
789
|
+
summaryParts.push(`Synthesized ${synthesized} new insights`);
|
|
790
|
+
}
|
|
791
|
+
// ── E. Update training log ──
|
|
792
|
+
trainingLog.lastRun = new Date().toISOString();
|
|
793
|
+
trainingLog.runsTotal++;
|
|
794
|
+
trainingLog.entriesPruned += pruned;
|
|
795
|
+
trainingLog.insightsSynthesized += synthesized;
|
|
796
|
+
trainingLog.patternsOptimized += optimized;
|
|
797
|
+
saveJSON(TRAINING_FILE, trainingLog);
|
|
798
|
+
const summary = summaryParts.length > 0
|
|
799
|
+
? summaryParts.join('\n')
|
|
800
|
+
: 'Knowledge base is clean. No changes needed.';
|
|
801
|
+
return { pruned, optimized, synthesized, summary };
|
|
802
|
+
}
|
|
803
|
+
/** Check if self-training should run (auto-trigger every 50 messages) */
|
|
804
|
+
export function shouldAutoTrain() {
|
|
805
|
+
if (!trainingLog.lastRun)
|
|
806
|
+
return profile.totalMessages >= 20;
|
|
807
|
+
const lastRun = new Date(trainingLog.lastRun).getTime();
|
|
808
|
+
const hoursSinceLastRun = (Date.now() - lastRun) / (1000 * 60 * 60);
|
|
809
|
+
// Auto-train if: > 24 hours since last run AND > 20 messages since
|
|
810
|
+
return hoursSinceLastRun > 24 && profile.totalMessages % 50 === 0;
|
|
811
|
+
}
|
|
812
|
+
/** Get training log for display */
|
|
813
|
+
export function getTrainingLog() {
|
|
814
|
+
return trainingLog;
|
|
815
|
+
}
|
|
816
|
+
export function getExtendedStats() {
|
|
817
|
+
const base = getStats();
|
|
818
|
+
return {
|
|
819
|
+
...base,
|
|
820
|
+
knowledgeCount: knowledge.length,
|
|
821
|
+
correctionsCount: corrections.length,
|
|
822
|
+
projectsCount: projects.length,
|
|
823
|
+
topKnowledge: knowledge
|
|
824
|
+
.sort((a, b) => b.references - a.references)
|
|
825
|
+
.slice(0, 5)
|
|
826
|
+
.map(k => k.fact.slice(0, 80)),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
//# sourceMappingURL=learning.js.map
|