lynkr 2.0.0 → 3.0.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 +226 -15
- package/docs/index.md +230 -11
- package/install.sh +260 -0
- package/package.json +4 -3
- package/src/clients/databricks.js +158 -0
- package/src/clients/routing.js +13 -1
- package/src/config/index.js +68 -1
- package/src/db/index.js +118 -0
- package/src/memory/extractor.js +350 -0
- package/src/memory/index.js +55 -0
- package/src/memory/retriever.js +266 -0
- package/src/memory/search.js +239 -0
- package/src/memory/store.js +411 -0
- package/src/memory/surprise.js +306 -0
- package/src/memory/tools.js +348 -0
- package/src/orchestrator/index.js +170 -0
- package/test/llamacpp-integration.test.js +686 -0
- package/test/memory/extractor.test.js +360 -0
- package/test/memory/retriever.test.js +583 -0
- package/test/memory/search.test.js +389 -0
- package/test/memory/store.test.js +312 -0
- package/test/memory/surprise.test.js +300 -0
- package/test/memory-performance.test.js +472 -0
- package/test/openai-integration.test.js +681 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate surprise score for new memory (Titans-inspired)
|
|
5
|
+
*
|
|
6
|
+
* Surprise factors (without neural networks):
|
|
7
|
+
* 1. Novelty: Is this entity/concept new? (0.30 weight)
|
|
8
|
+
* 2. Contradiction: Does this contradict existing memory? (0.40 weight)
|
|
9
|
+
* 3. Specificity: How specific/detailed is this? (0.15 weight)
|
|
10
|
+
* 4. User emphasis: Did user explicitly emphasize? (0.10 weight)
|
|
11
|
+
* 5. Context switch: Topic change? (0.05 weight)
|
|
12
|
+
*/
|
|
13
|
+
function calculateSurprise(newMemory, existingMemories, context = {}) {
|
|
14
|
+
try {
|
|
15
|
+
let surprise = 0.0;
|
|
16
|
+
|
|
17
|
+
// Factor 1: Novelty (0.0-0.3)
|
|
18
|
+
const noveltyScore = calculateNovelty(newMemory, existingMemories);
|
|
19
|
+
surprise += noveltyScore * 0.30;
|
|
20
|
+
|
|
21
|
+
// Factor 2: Contradiction (0.0-0.4)
|
|
22
|
+
const contradictionScore = detectContradiction(newMemory, existingMemories);
|
|
23
|
+
surprise += contradictionScore * 0.40;
|
|
24
|
+
|
|
25
|
+
// Factor 3: Specificity (0.0-0.15)
|
|
26
|
+
const specificityScore = measureSpecificity(newMemory.content);
|
|
27
|
+
surprise += specificityScore * 0.15;
|
|
28
|
+
|
|
29
|
+
// Factor 4: User emphasis (0.0-0.1)
|
|
30
|
+
const emphasisScore = detectEmphasis(context.userContent || '');
|
|
31
|
+
surprise += emphasisScore * 0.10;
|
|
32
|
+
|
|
33
|
+
// Factor 5: Context switch (0.0-0.05)
|
|
34
|
+
const contextSwitchScore = measureContextSwitch(newMemory, existingMemories);
|
|
35
|
+
surprise += contextSwitchScore * 0.05;
|
|
36
|
+
|
|
37
|
+
return Math.min(1.0, Math.max(0.0, surprise));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logger.warn({ err }, 'Surprise calculation failed');
|
|
40
|
+
return 0.5; // Default moderate surprise
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate novelty score - is this information new?
|
|
46
|
+
*/
|
|
47
|
+
function calculateNovelty(newMemory, existingMemories) {
|
|
48
|
+
if (!existingMemories || existingMemories.length === 0) {
|
|
49
|
+
return 1.0; // Everything is novel with no history
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const newEntities = extractSimpleEntities(newMemory.content);
|
|
53
|
+
const newKeywords = extractKeywords(newMemory.content);
|
|
54
|
+
|
|
55
|
+
// Check if entities are new
|
|
56
|
+
let novelEntityCount = 0;
|
|
57
|
+
for (const entity of newEntities) {
|
|
58
|
+
const isNovel = !existingMemories.some(mem =>
|
|
59
|
+
mem.content.toLowerCase().includes(entity.toLowerCase())
|
|
60
|
+
);
|
|
61
|
+
if (isNovel) novelEntityCount++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entityNovelty = newEntities.length > 0
|
|
65
|
+
? novelEntityCount / newEntities.length
|
|
66
|
+
: 0.5;
|
|
67
|
+
|
|
68
|
+
// Check if keywords are new
|
|
69
|
+
let novelKeywordCount = 0;
|
|
70
|
+
for (const keyword of newKeywords) {
|
|
71
|
+
const isNovel = !existingMemories.some(mem =>
|
|
72
|
+
mem.content.toLowerCase().includes(keyword.toLowerCase())
|
|
73
|
+
);
|
|
74
|
+
if (isNovel) novelKeywordCount++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const keywordNovelty = newKeywords.length > 0
|
|
78
|
+
? novelKeywordCount / newKeywords.length
|
|
79
|
+
: 0.5;
|
|
80
|
+
|
|
81
|
+
// Average entity and keyword novelty
|
|
82
|
+
return (entityNovelty + keywordNovelty) / 2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detect contradictions with existing memories
|
|
87
|
+
*/
|
|
88
|
+
function detectContradiction(newMemory, existingMemories) {
|
|
89
|
+
if (!existingMemories || existingMemories.length === 0) {
|
|
90
|
+
return 0.0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newLower = newMemory.content.toLowerCase();
|
|
94
|
+
|
|
95
|
+
// Negation patterns
|
|
96
|
+
const hasNegation = /\b(not|no|never|don't|doesn't|didn't|isn't|aren't|wasn't|weren't)\b/.test(newLower);
|
|
97
|
+
|
|
98
|
+
// Contradictory phrases
|
|
99
|
+
const contradictoryPhrases = [
|
|
100
|
+
/instead of/i,
|
|
101
|
+
/rather than/i,
|
|
102
|
+
/actually/i,
|
|
103
|
+
/correction/i,
|
|
104
|
+
/changed? (?:from|to)/i,
|
|
105
|
+
/replaced/i,
|
|
106
|
+
/no longer/i,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const hasContradictoryPhrase = contradictoryPhrases.some(pattern => pattern.test(newMemory.content));
|
|
110
|
+
|
|
111
|
+
if (!hasNegation && !hasContradictoryPhrase) {
|
|
112
|
+
return 0.0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Extract entities from new memory
|
|
116
|
+
const newEntities = extractSimpleEntities(newMemory.content);
|
|
117
|
+
|
|
118
|
+
// Look for similar memories with overlapping entities
|
|
119
|
+
let contradictionScore = 0.0;
|
|
120
|
+
for (const mem of existingMemories) {
|
|
121
|
+
const memEntities = extractSimpleEntities(mem.content);
|
|
122
|
+
|
|
123
|
+
// Check if memories share entities
|
|
124
|
+
const sharedEntities = newEntities.filter(e =>
|
|
125
|
+
memEntities.some(me => me.toLowerCase() === e.toLowerCase())
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (sharedEntities.length === 0) continue;
|
|
129
|
+
|
|
130
|
+
// Check for opposite sentiment/meaning
|
|
131
|
+
const memLower = mem.content.toLowerCase();
|
|
132
|
+
const memHasNegation = /\b(not|no|never|don't|doesn't)\b/.test(memLower);
|
|
133
|
+
|
|
134
|
+
if (hasNegation !== memHasNegation) {
|
|
135
|
+
// One has negation, one doesn't - likely contradiction
|
|
136
|
+
contradictionScore = Math.max(contradictionScore, 0.8);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (hasContradictoryPhrase) {
|
|
140
|
+
contradictionScore = Math.max(contradictionScore, 0.6);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return contradictionScore;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Measure specificity of content
|
|
149
|
+
*/
|
|
150
|
+
function measureSpecificity(content) {
|
|
151
|
+
let score = 0.0;
|
|
152
|
+
|
|
153
|
+
// Named entities (proper nouns with capitals)
|
|
154
|
+
const properNouns = content.match(/\b[A-Z][a-z]+(?:[A-Z][a-z]+)*\b/g) || [];
|
|
155
|
+
score += Math.min(0.3, properNouns.length * 0.05);
|
|
156
|
+
|
|
157
|
+
// Numeric values
|
|
158
|
+
const numbers = content.match(/\b\d+(?:\.\d+)?\b/g) || [];
|
|
159
|
+
score += Math.min(0.2, numbers.length * 0.05);
|
|
160
|
+
|
|
161
|
+
// Code references (backticks, file paths)
|
|
162
|
+
const codeRefs = content.match(/`[^`]+`|[A-Za-z0-9_]+\.[A-Za-z0-9_]+/g) || [];
|
|
163
|
+
score += Math.min(0.3, codeRefs.length * 0.1);
|
|
164
|
+
|
|
165
|
+
// Technical terms (words with camelCase or snake_case)
|
|
166
|
+
const technicalTerms = content.match(/\b[a-z]+[A-Z][a-zA-Z]*\b|\b[a-z]+_[a-z_]+\b/g) || [];
|
|
167
|
+
score += Math.min(0.2, technicalTerms.length * 0.05);
|
|
168
|
+
|
|
169
|
+
return Math.min(1.0, score);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Detect user emphasis in message
|
|
174
|
+
*/
|
|
175
|
+
function detectEmphasis(userContent) {
|
|
176
|
+
if (!userContent) return 0.0;
|
|
177
|
+
|
|
178
|
+
const lower = userContent.toLowerCase();
|
|
179
|
+
let score = 0.0;
|
|
180
|
+
|
|
181
|
+
// Emphasis keywords
|
|
182
|
+
const emphasisKeywords = [
|
|
183
|
+
'important',
|
|
184
|
+
'critical',
|
|
185
|
+
'crucial',
|
|
186
|
+
'essential',
|
|
187
|
+
'must',
|
|
188
|
+
'need to',
|
|
189
|
+
'remember',
|
|
190
|
+
'note that',
|
|
191
|
+
'pay attention',
|
|
192
|
+
'make sure',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
for (const keyword of emphasisKeywords) {
|
|
196
|
+
if (lower.includes(keyword)) {
|
|
197
|
+
score += 0.2;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Exclamation marks
|
|
202
|
+
const exclamations = (userContent.match(/!/g) || []).length;
|
|
203
|
+
score += Math.min(0.3, exclamations * 0.15);
|
|
204
|
+
|
|
205
|
+
// All caps words
|
|
206
|
+
const capsWords = userContent.match(/\b[A-Z]{2,}\b/g) || [];
|
|
207
|
+
score += Math.min(0.2, capsWords.length * 0.1);
|
|
208
|
+
|
|
209
|
+
// Repetition (e.g., "very very important")
|
|
210
|
+
const words = lower.split(/\s+/);
|
|
211
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
212
|
+
if (words[i] === words[i + 1]) {
|
|
213
|
+
score += 0.15;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return Math.min(1.0, score);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Measure context switch (topic change)
|
|
223
|
+
*/
|
|
224
|
+
function measureContextSwitch(newMemory, existingMemories) {
|
|
225
|
+
if (!existingMemories || existingMemories.length === 0) {
|
|
226
|
+
return 0.0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get most recent memories (last 5)
|
|
230
|
+
const recentMemories = existingMemories
|
|
231
|
+
.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
|
|
232
|
+
.slice(0, 5);
|
|
233
|
+
|
|
234
|
+
if (recentMemories.length === 0) return 0.0;
|
|
235
|
+
|
|
236
|
+
const newKeywords = extractKeywords(newMemory.content);
|
|
237
|
+
const recentKeywords = new Set();
|
|
238
|
+
|
|
239
|
+
for (const mem of recentMemories) {
|
|
240
|
+
const keywords = extractKeywords(mem.content);
|
|
241
|
+
keywords.forEach(k => recentKeywords.add(k));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Calculate keyword overlap
|
|
245
|
+
const overlappingKeywords = newKeywords.filter(k => recentKeywords.has(k));
|
|
246
|
+
const overlapRatio = newKeywords.length > 0
|
|
247
|
+
? overlappingKeywords.length / newKeywords.length
|
|
248
|
+
: 0;
|
|
249
|
+
|
|
250
|
+
// Low overlap = high context switch
|
|
251
|
+
return 1.0 - overlapRatio;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Extract simple entities (capitalized words, code references)
|
|
256
|
+
*/
|
|
257
|
+
function extractSimpleEntities(text) {
|
|
258
|
+
const entities = new Set();
|
|
259
|
+
|
|
260
|
+
// Proper nouns
|
|
261
|
+
const properNouns = text.match(/\b[A-Z][a-z]+\b/g) || [];
|
|
262
|
+
properNouns.forEach(e => entities.add(e));
|
|
263
|
+
|
|
264
|
+
// Code identifiers
|
|
265
|
+
const codeIds = text.match(/\b[a-z_][a-z0-9_]*\b/gi) || [];
|
|
266
|
+
codeIds.forEach(e => {
|
|
267
|
+
if (e.length >= 4 && e.length <= 50) {
|
|
268
|
+
entities.add(e);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// File names
|
|
273
|
+
const files = text.match(/[a-z0-9_-]+\.[a-z]{2,4}/gi) || [];
|
|
274
|
+
files.forEach(e => entities.add(e));
|
|
275
|
+
|
|
276
|
+
return Array.from(entities);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Extract keywords (similar to search.js but simplified)
|
|
281
|
+
*/
|
|
282
|
+
function extractKeywords(text) {
|
|
283
|
+
const stopwords = new Set([
|
|
284
|
+
'the', 'is', 'at', 'which', 'on', 'and', 'or', 'not', 'this', 'that',
|
|
285
|
+
'with', 'from', 'for', 'to', 'in', 'of', 'a', 'an', 'are', 'was', 'were',
|
|
286
|
+
'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will',
|
|
287
|
+
'would', 'should', 'could', 'may', 'might', 'must', 'can', 'it', 'its',
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
return text
|
|
291
|
+
.toLowerCase()
|
|
292
|
+
.split(/\s+/)
|
|
293
|
+
.map(word => word.replace(/[^\w]/g, ''))
|
|
294
|
+
.filter(word => word.length > 3 && !stopwords.has(word));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
calculateSurprise,
|
|
299
|
+
calculateNovelty,
|
|
300
|
+
detectContradiction,
|
|
301
|
+
measureSpecificity,
|
|
302
|
+
detectEmphasis,
|
|
303
|
+
measureContextSwitch,
|
|
304
|
+
extractSimpleEntities,
|
|
305
|
+
extractKeywords,
|
|
306
|
+
};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
const store = require("./store");
|
|
2
|
+
const search = require("./search");
|
|
3
|
+
const retriever = require("./retriever");
|
|
4
|
+
const logger = require("../logger");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Memory tools for explicit memory management
|
|
8
|
+
* These can be registered as tools for the model to use
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tool: memory_search
|
|
13
|
+
* Search long-term memories for relevant facts
|
|
14
|
+
*/
|
|
15
|
+
async function memory_search(args, context = {}) {
|
|
16
|
+
const { query, limit = 10, type, category } = args;
|
|
17
|
+
|
|
18
|
+
if (!query || typeof query !== 'string') {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
content: JSON.stringify({ error: 'Query parameter is required and must be a string' }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const results = search.searchMemories({
|
|
27
|
+
query,
|
|
28
|
+
limit,
|
|
29
|
+
types: type ? [type] : undefined,
|
|
30
|
+
categories: category ? [category] : undefined,
|
|
31
|
+
sessionId: context.session?.id,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const formatted = results.map((mem, idx) => ({
|
|
35
|
+
index: idx + 1,
|
|
36
|
+
type: mem.type,
|
|
37
|
+
content: mem.content,
|
|
38
|
+
importance: mem.importance,
|
|
39
|
+
age: retriever.formatAge(Date.now() - mem.createdAt),
|
|
40
|
+
category: mem.category,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
content: JSON.stringify({
|
|
46
|
+
query,
|
|
47
|
+
resultCount: results.length,
|
|
48
|
+
memories: formatted,
|
|
49
|
+
}, null, 2),
|
|
50
|
+
metadata: { resultCount: results.length },
|
|
51
|
+
};
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error({ err, query }, 'Memory search failed');
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
content: JSON.stringify({ error: 'Memory search failed', message: err.message }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Tool: memory_add
|
|
63
|
+
* Manually add a fact to long-term memory
|
|
64
|
+
*/
|
|
65
|
+
async function memory_add(args, context = {}) {
|
|
66
|
+
const {
|
|
67
|
+
content,
|
|
68
|
+
type = 'fact',
|
|
69
|
+
category = 'general',
|
|
70
|
+
importance = 0.5,
|
|
71
|
+
} = args;
|
|
72
|
+
|
|
73
|
+
if (!content || typeof content !== 'string') {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
content: JSON.stringify({ error: 'Content parameter is required and must be a string' }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!['fact', 'preference', 'decision', 'entity', 'relationship'].includes(type)) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
content: JSON.stringify({
|
|
84
|
+
error: 'Invalid type. Must be one of: fact, preference, decision, entity, relationship',
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof importance !== 'number' || importance < 0 || importance > 1) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
content: JSON.stringify({ error: 'Importance must be a number between 0 and 1' }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const memory = store.createMemory({
|
|
98
|
+
content,
|
|
99
|
+
type,
|
|
100
|
+
category,
|
|
101
|
+
sessionId: context.session?.id,
|
|
102
|
+
importance,
|
|
103
|
+
surpriseScore: 0.5, // Manual additions get moderate surprise
|
|
104
|
+
metadata: {
|
|
105
|
+
manual: true,
|
|
106
|
+
addedBy: 'user',
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
content: JSON.stringify({
|
|
114
|
+
message: 'Memory stored successfully',
|
|
115
|
+
memoryId: memory.id,
|
|
116
|
+
memory: {
|
|
117
|
+
id: memory.id,
|
|
118
|
+
type: memory.type,
|
|
119
|
+
content: memory.content,
|
|
120
|
+
importance: memory.importance,
|
|
121
|
+
category: memory.category,
|
|
122
|
+
},
|
|
123
|
+
}, null, 2),
|
|
124
|
+
metadata: { memoryId: memory.id },
|
|
125
|
+
};
|
|
126
|
+
} catch (err) {
|
|
127
|
+
logger.error({ err, content }, 'Memory add failed');
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
content: JSON.stringify({ error: 'Failed to add memory', message: err.message }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Tool: memory_forget
|
|
137
|
+
* Remove memories matching a query
|
|
138
|
+
*/
|
|
139
|
+
async function memory_forget(args, context = {}) {
|
|
140
|
+
const { query, confirm = false } = args;
|
|
141
|
+
|
|
142
|
+
if (!query || typeof query !== 'string') {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
content: JSON.stringify({ error: 'Query parameter is required and must be a string' }),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// Search for matching memories
|
|
151
|
+
const matches = search.searchMemories({
|
|
152
|
+
query,
|
|
153
|
+
limit: 50, // Check up to 50 matches
|
|
154
|
+
sessionId: context.session?.id,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (matches.length === 0) {
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
content: JSON.stringify({
|
|
161
|
+
message: 'No memories found matching the query',
|
|
162
|
+
query,
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!confirm) {
|
|
168
|
+
const preview = matches.slice(0, 5).map((mem, idx) => ({
|
|
169
|
+
index: idx + 1,
|
|
170
|
+
type: mem.type,
|
|
171
|
+
content: mem.content.substring(0, 100) + (mem.content.length > 100 ? '...' : ''),
|
|
172
|
+
age: retriever.formatAge(Date.now() - mem.createdAt),
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
content: JSON.stringify({
|
|
178
|
+
message: 'Found memories matching query. Set confirm=true to delete them.',
|
|
179
|
+
query,
|
|
180
|
+
matchCount: matches.length,
|
|
181
|
+
preview,
|
|
182
|
+
warning: 'This action cannot be undone',
|
|
183
|
+
}, null, 2),
|
|
184
|
+
metadata: { requiresConfirmation: true, matchCount: matches.length },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Delete all matching memories
|
|
189
|
+
let deletedCount = 0;
|
|
190
|
+
for (const memory of matches) {
|
|
191
|
+
const deleted = store.deleteMemory(memory.id);
|
|
192
|
+
if (deleted) deletedCount++;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
content: JSON.stringify({
|
|
198
|
+
message: `Deleted ${deletedCount} memories`,
|
|
199
|
+
query,
|
|
200
|
+
deletedCount,
|
|
201
|
+
}, null, 2),
|
|
202
|
+
metadata: { deletedCount },
|
|
203
|
+
};
|
|
204
|
+
} catch (err) {
|
|
205
|
+
logger.error({ err, query }, 'Memory forget failed');
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
content: JSON.stringify({ error: 'Failed to delete memories', message: err.message }),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Tool: memory_stats
|
|
215
|
+
* Get statistics about stored memories
|
|
216
|
+
*/
|
|
217
|
+
async function memory_stats(args, context = {}) {
|
|
218
|
+
try {
|
|
219
|
+
const stats = retriever.getMemoryStats(context.session?.id);
|
|
220
|
+
|
|
221
|
+
if (!stats) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
content: JSON.stringify({ error: 'Failed to retrieve memory statistics' }),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
content: JSON.stringify({
|
|
231
|
+
total: stats.total,
|
|
232
|
+
byType: stats.byType,
|
|
233
|
+
recentCount: stats.recentCount,
|
|
234
|
+
importantCount: stats.importantCount,
|
|
235
|
+
sessionId: stats.sessionId || 'global',
|
|
236
|
+
}, null, 2),
|
|
237
|
+
};
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logger.error({ err }, 'Memory stats failed');
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
content: JSON.stringify({ error: 'Failed to get statistics', message: err.message }),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Tool definitions for registration
|
|
248
|
+
const MEMORY_TOOLS = {
|
|
249
|
+
memory_search: {
|
|
250
|
+
name: 'memory_search',
|
|
251
|
+
description: 'Search long-term memories for relevant facts and information from previous conversations',
|
|
252
|
+
input_schema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
query: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
description: 'Search query to find relevant memories',
|
|
258
|
+
},
|
|
259
|
+
limit: {
|
|
260
|
+
type: 'integer',
|
|
261
|
+
description: 'Maximum number of results to return (default: 10)',
|
|
262
|
+
minimum: 1,
|
|
263
|
+
maximum: 50,
|
|
264
|
+
},
|
|
265
|
+
type: {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description: 'Filter by memory type',
|
|
268
|
+
enum: ['fact', 'preference', 'decision', 'entity', 'relationship'],
|
|
269
|
+
},
|
|
270
|
+
category: {
|
|
271
|
+
type: 'string',
|
|
272
|
+
description: 'Filter by category (code, user, project, general)',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
required: ['query'],
|
|
276
|
+
},
|
|
277
|
+
handler: memory_search,
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
memory_add: {
|
|
281
|
+
name: 'memory_add',
|
|
282
|
+
description: 'Manually add a fact or piece of information to long-term memory',
|
|
283
|
+
input_schema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
content: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
description: 'The fact or information to remember',
|
|
289
|
+
},
|
|
290
|
+
type: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'Type of memory',
|
|
293
|
+
enum: ['fact', 'preference', 'decision', 'entity', 'relationship'],
|
|
294
|
+
},
|
|
295
|
+
category: {
|
|
296
|
+
type: 'string',
|
|
297
|
+
description: 'Category: code, user, project, or general',
|
|
298
|
+
},
|
|
299
|
+
importance: {
|
|
300
|
+
type: 'number',
|
|
301
|
+
description: 'Importance score between 0 and 1 (default: 0.5)',
|
|
302
|
+
minimum: 0,
|
|
303
|
+
maximum: 1,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
required: ['content'],
|
|
307
|
+
},
|
|
308
|
+
handler: memory_add,
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
memory_forget: {
|
|
312
|
+
name: 'memory_forget',
|
|
313
|
+
description: 'Remove memories matching a search query',
|
|
314
|
+
input_schema: {
|
|
315
|
+
type: 'object',
|
|
316
|
+
properties: {
|
|
317
|
+
query: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Query to match memories to delete',
|
|
320
|
+
},
|
|
321
|
+
confirm: {
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
description: 'Set to true to confirm deletion (required for safety)',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ['query'],
|
|
327
|
+
},
|
|
328
|
+
handler: memory_forget,
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
memory_stats: {
|
|
332
|
+
name: 'memory_stats',
|
|
333
|
+
description: 'Get statistics about stored memories',
|
|
334
|
+
input_schema: {
|
|
335
|
+
type: 'object',
|
|
336
|
+
properties: {},
|
|
337
|
+
},
|
|
338
|
+
handler: memory_stats,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
memory_search,
|
|
344
|
+
memory_add,
|
|
345
|
+
memory_forget,
|
|
346
|
+
memory_stats,
|
|
347
|
+
MEMORY_TOOLS,
|
|
348
|
+
};
|