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.
@@ -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
+ };