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,350 @@
1
+ const store = require("./store");
2
+ const surprise = require("./surprise");
3
+ const config = require("../config");
4
+ const logger = require("../logger");
5
+
6
+ // Extraction patterns for different memory types
7
+ const EXTRACTION_PATTERNS = {
8
+ preference: [
9
+ /(?:I|user|they|you)\s+(?:prefer|like|want|need|always|never|usually|typically)\s+([^.!?\n]{10,100})/gi,
10
+ /(?:my|your|their|the user's)\s+(?:preference|choice|favorite)\s+(?:is|for|would be)\s+([^.!?\n]{10,100})/gi,
11
+ /(?:should|must|need to)\s+(?:use|implement|do|follow)\s+([^.!?\n]{10,100})/gi,
12
+ ],
13
+
14
+ decision: [
15
+ /(?:decided|choosing|going with|selected|picked|opted for)\s+([^.!?\n]{10,100})/gi,
16
+ /(?:the|our|my)\s+(?:approach|strategy|plan|solution)\s+(?:is|will be|should be)\s+([^.!?\n]{10,100})/gi,
17
+ /(?:let's|we'll|I'll)\s+(?:use|implement|go with|choose)\s+([^.!?\n]{10,100})/gi,
18
+ /(?:agreed|confirmed)\s+(?:to|that|on)\s+([^.!?\n]{10,100})/gi,
19
+ ],
20
+
21
+ fact: [
22
+ /(?:this|the)\s+(?:project|codebase|application|system)\s+(?:uses|is|has|implements|requires)\s+([^.!?\n]{10,150})/gi,
23
+ /(?:important|note|remember|keep in mind):\s*([^.!?\n]{10,150})/gi,
24
+ /(?:the|this)\s+(?:file|function|class|module|component)\s+(?:is|handles|manages|does)\s+([^.!?\n]{10,150})/gi,
25
+ /(?:currently|now|at the moment)\s+(?:using|implementing|running)\s+([^.!?\n]{10,100})/gi,
26
+ ],
27
+
28
+ entity: [
29
+ /(?:file|function|class|module|component|package|library)\s+['"`]?([A-Za-z0-9_./\-]+)['"`]?/gi,
30
+ /in\s+['"`]([A-Za-z0-9_./\-]+\.(?:js|ts|py|java|go|rs|cpp|c|h))['"`]/gi,
31
+ /`([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)`/g, // Code references
32
+ ],
33
+
34
+ relationship: [
35
+ /([A-Za-z0-9_]+)\s+(?:depends on|imports|uses|extends|implements|inherits from)\s+([A-Za-z0-9_]+)/gi,
36
+ /([A-Za-z0-9_./\-]+)\s+(?:calls|invokes|references)\s+([A-Za-z0-9_./\-]+)/gi,
37
+ ],
38
+ };
39
+
40
+ /**
41
+ * Extract memories from assistant response and conversation context
42
+ */
43
+ async function extractMemories(assistantResponse, conversationMessages, context = {}) {
44
+ if (!config.memory?.extraction?.enabled) {
45
+ return [];
46
+ }
47
+
48
+ const { sessionId = null } = context;
49
+ const memories = [];
50
+
51
+ try {
52
+ // Extract assistant message content
53
+ const assistantContent = extractContent(assistantResponse);
54
+ if (!assistantContent) return [];
55
+
56
+ // Get last user message for context
57
+ const lastUserMessage = conversationMessages
58
+ ?.filter(m => m.role === 'user')
59
+ ?.pop();
60
+ const userContent = lastUserMessage ? extractContent(lastUserMessage) : '';
61
+
62
+ // Extract different types of memories
63
+ const preferences = extractByType(assistantContent, 'preference');
64
+ const decisions = extractByType(assistantContent, 'decision');
65
+ const facts = extractByType(assistantContent, 'fact');
66
+ const entities = extractEntities(assistantContent);
67
+ const relationships = extractRelationships(assistantContent);
68
+
69
+ // Create memory objects with surprise scores
70
+ for (const content of preferences) {
71
+ const memory = await createMemoryWithSurprise({
72
+ content,
73
+ type: 'preference',
74
+ category: 'user',
75
+ sessionId,
76
+ userContent,
77
+ });
78
+ if (memory) memories.push(memory);
79
+ }
80
+
81
+ for (const content of decisions) {
82
+ const memory = await createMemoryWithSurprise({
83
+ content,
84
+ type: 'decision',
85
+ category: 'project',
86
+ sessionId,
87
+ userContent,
88
+ });
89
+ if (memory) memories.push(memory);
90
+ }
91
+
92
+ for (const content of facts) {
93
+ const memory = await createMemoryWithSurprise({
94
+ content,
95
+ type: 'fact',
96
+ category: classifyCategory(content),
97
+ sessionId,
98
+ userContent,
99
+ });
100
+ if (memory) memories.push(memory);
101
+ }
102
+
103
+ for (const entityName of entities) {
104
+ // Track entity
105
+ store.trackEntity('code', entityName, { source: 'extraction' });
106
+
107
+ const memory = await createMemoryWithSurprise({
108
+ content: `Entity: ${entityName}`,
109
+ type: 'entity',
110
+ category: 'code',
111
+ sessionId,
112
+ userContent,
113
+ metadata: { entityName },
114
+ });
115
+ if (memory) memories.push(memory);
116
+ }
117
+
118
+ for (const { from, to, relationship } of relationships) {
119
+ const memory = await createMemoryWithSurprise({
120
+ content: `${from} ${relationship} ${to}`,
121
+ type: 'relationship',
122
+ category: 'code',
123
+ sessionId,
124
+ userContent,
125
+ metadata: { from, to, relationship },
126
+ });
127
+ if (memory) memories.push(memory);
128
+ }
129
+
130
+ logger.debug({
131
+ sessionId,
132
+ memoriesExtracted: memories.length,
133
+ types: {
134
+ preference: preferences.length,
135
+ decision: decisions.length,
136
+ fact: facts.length,
137
+ entity: entities.length,
138
+ relationship: relationships.length,
139
+ },
140
+ }, 'Memory extraction completed');
141
+
142
+ return memories;
143
+ } catch (err) {
144
+ logger.error({ err, sessionId }, 'Memory extraction failed');
145
+ return [];
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Extract content from message (handle different formats)
151
+ */
152
+ function extractContent(message) {
153
+ if (!message) return '';
154
+
155
+ // Handle different message formats
156
+ if (typeof message === 'string') return message;
157
+
158
+ if (message.content) {
159
+ if (typeof message.content === 'string') return message.content;
160
+
161
+ // Handle array of content blocks
162
+ if (Array.isArray(message.content)) {
163
+ return message.content
164
+ .filter(block => block?.type === 'text' || typeof block === 'string')
165
+ .map(block => typeof block === 'string' ? block : block.text)
166
+ .join('\n');
167
+ }
168
+ }
169
+
170
+ // Handle choices array (from model responses)
171
+ if (message.choices && Array.isArray(message.choices)) {
172
+ const choice = message.choices[0];
173
+ if (choice?.message?.content) {
174
+ return extractContent(choice.message);
175
+ }
176
+ }
177
+
178
+ return '';
179
+ }
180
+
181
+ /**
182
+ * Extract memories by type using patterns
183
+ */
184
+ function extractByType(text, type) {
185
+ const patterns = EXTRACTION_PATTERNS[type] || [];
186
+ const matches = new Set();
187
+
188
+ for (const pattern of patterns) {
189
+ let match;
190
+ while ((match = pattern.exec(text)) !== null) {
191
+ const captured = match[1]?.trim();
192
+ if (captured && captured.length >= 10 && captured.length <= 200) {
193
+ matches.add(captured);
194
+ }
195
+ }
196
+ }
197
+
198
+ return Array.from(matches);
199
+ }
200
+
201
+ /**
202
+ * Extract entity references
203
+ */
204
+ function extractEntities(text) {
205
+ const entities = new Set();
206
+ const patterns = EXTRACTION_PATTERNS.entity;
207
+
208
+ for (const pattern of patterns) {
209
+ let match;
210
+ while ((match = pattern.exec(text)) !== null) {
211
+ const entityName = match[1]?.trim();
212
+ if (entityName && entityName.length >= 3 && entityName.length <= 100) {
213
+ entities.add(entityName);
214
+ }
215
+ }
216
+ }
217
+
218
+ return Array.from(entities);
219
+ }
220
+
221
+ /**
222
+ * Extract relationships between entities
223
+ */
224
+ function extractRelationships(text) {
225
+ const relationships = [];
226
+ const patterns = EXTRACTION_PATTERNS.relationship;
227
+
228
+ for (const pattern of patterns) {
229
+ let match;
230
+ while ((match = pattern.exec(text)) !== null) {
231
+ const from = match[1]?.trim();
232
+ const to = match[2]?.trim();
233
+ if (from && to) {
234
+ relationships.push({
235
+ from,
236
+ to,
237
+ relationship: 'depends_on', // Generalized relationship type
238
+ });
239
+ }
240
+ }
241
+ }
242
+
243
+ return relationships;
244
+ }
245
+
246
+ /**
247
+ * Classify content category
248
+ */
249
+ function classifyCategory(content) {
250
+ const lower = content.toLowerCase();
251
+
252
+ if (lower.includes('code') || lower.includes('function') || lower.includes('file') ||
253
+ lower.includes('class') || lower.includes('module')) {
254
+ return 'code';
255
+ }
256
+
257
+ if (lower.includes('project') || lower.includes('application') || lower.includes('system')) {
258
+ return 'project';
259
+ }
260
+
261
+ if (lower.includes('user') || lower.includes('prefer') || lower.includes('like')) {
262
+ return 'user';
263
+ }
264
+
265
+ return 'general';
266
+ }
267
+
268
+ /**
269
+ * Create memory with surprise score calculation
270
+ */
271
+ async function createMemoryWithSurprise(options) {
272
+ const { content, type, category, sessionId, userContent, metadata = {} } = options;
273
+
274
+ // Get existing memories for surprise calculation
275
+ const existingMemories = store.getRecentMemories({ limit: 100, sessionId });
276
+
277
+ // Calculate surprise score
278
+ const surpriseScore = surprise.calculateSurprise({
279
+ content,
280
+ type,
281
+ category,
282
+ }, existingMemories, { userContent });
283
+
284
+ // Only store if surprise score exceeds threshold
285
+ const threshold = config.memory?.surpriseThreshold ?? 0.3;
286
+ if (surpriseScore < threshold) {
287
+ logger.debug({ content, surpriseScore, threshold }, 'Memory filtered by surprise threshold');
288
+ return null;
289
+ }
290
+
291
+ // Calculate initial importance based on surprise and type
292
+ const importance = calculateInitialImportance(type, surpriseScore);
293
+
294
+ // Store memory
295
+ try {
296
+ const memory = store.createMemory({
297
+ sessionId,
298
+ content,
299
+ type,
300
+ category,
301
+ importance,
302
+ surpriseScore,
303
+ metadata: {
304
+ ...metadata,
305
+ extractedAt: Date.now(),
306
+ },
307
+ });
308
+
309
+ return memory;
310
+ } catch (err) {
311
+ logger.warn({ err, content }, 'Failed to store memory');
312
+ return null;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Calculate initial importance score
318
+ */
319
+ function calculateInitialImportance(type, surpriseScore) {
320
+ // Base importance by type
321
+ const baseImportance = {
322
+ preference: 0.7, // User preferences are important
323
+ decision: 0.8, // Decisions are very important
324
+ fact: 0.6, // Facts are moderately important
325
+ entity: 0.4, // Entities are less important individually
326
+ relationship: 0.5,
327
+ };
328
+
329
+ const base = baseImportance[type] ?? 0.5;
330
+
331
+ // Boost by surprise score (0-1 scale)
332
+ return Math.min(1.0, base + (surpriseScore * 0.3));
333
+ }
334
+
335
+ /**
336
+ * Parse entities from content
337
+ */
338
+ function parseEntities(content) {
339
+ return extractEntities(content);
340
+ }
341
+
342
+ module.exports = {
343
+ extractMemories,
344
+ extractContent,
345
+ extractByType,
346
+ extractEntities,
347
+ extractRelationships,
348
+ parseEntities,
349
+ classifyCategory,
350
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Titans-Inspired Long-Term Memory System
3
+ *
4
+ * This module provides long-term memory capabilities with:
5
+ * - Surprise-based memory updates (Titans core innovation)
6
+ * - Automatic memory extraction from conversations
7
+ * - FTS5 semantic search
8
+ * - Multi-signal memory retrieval
9
+ * - Memory management tools
10
+ */
11
+
12
+ const store = require("./store");
13
+ const search = require("./search");
14
+ const retriever = require("./retriever");
15
+ const extractor = require("./extractor");
16
+ const surprise = require("./surprise");
17
+ const tools = require("./tools");
18
+
19
+ module.exports = {
20
+ // Store operations
21
+ store,
22
+ createMemory: store.createMemory,
23
+ getMemory: store.getMemory,
24
+ updateMemory: store.updateMemory,
25
+ deleteMemory: store.deleteMemory,
26
+ getRecentMemories: store.getRecentMemories,
27
+ getMemoriesByImportance: store.getMemoriesByImportance,
28
+ pruneOldMemories: store.pruneOldMemories,
29
+ countMemories: store.countMemories,
30
+
31
+ // Search operations
32
+ search,
33
+ searchMemories: search.searchMemories,
34
+ searchWithExpansion: search.searchWithExpansion,
35
+ findSimilar: search.findSimilar,
36
+
37
+ // Retrieval
38
+ retriever,
39
+ retrieveRelevantMemories: retriever.retrieveRelevantMemories,
40
+ formatMemoriesForContext: retriever.formatMemoriesForContext,
41
+ injectMemoriesIntoSystem: retriever.injectMemoriesIntoSystem,
42
+ getMemoryStats: retriever.getMemoryStats,
43
+
44
+ // Extraction
45
+ extractor,
46
+ extractMemories: extractor.extractMemories,
47
+
48
+ // Surprise detection
49
+ surprise,
50
+ calculateSurprise: surprise.calculateSurprise,
51
+
52
+ // Tools
53
+ tools,
54
+ MEMORY_TOOLS: tools.MEMORY_TOOLS,
55
+ };
@@ -0,0 +1,266 @@
1
+ const store = require("./store");
2
+ const search = require("./search");
3
+ const logger = require("../logger");
4
+
5
+ /**
6
+ * Retrieve relevant memories using multi-signal ranking
7
+ *
8
+ * Scoring algorithm:
9
+ * - 30% Recency: Exponential decay based on last access
10
+ * - 40% Importance: Stored importance value
11
+ * - 30% Relevance: Keyword overlap with query
12
+ */
13
+ function retrieveRelevantMemories(query, options = {}) {
14
+ const {
15
+ limit = 10,
16
+ sessionId = null,
17
+ includeGlobal = true,
18
+ recencyWeight = 0.3,
19
+ importanceWeight = 0.4,
20
+ relevanceWeight = 0.3,
21
+ } = options;
22
+
23
+ try {
24
+ // 1. FTS5 search for keyword relevance
25
+ const ftsResults = search.searchMemories({
26
+ query,
27
+ limit: limit * 3, // Get more candidates
28
+ sessionId: includeGlobal ? null : sessionId,
29
+ });
30
+
31
+ // 2. Get recent memories (recency bias)
32
+ const recentMemories = store.getRecentMemories({
33
+ limit: limit * 2,
34
+ sessionId: includeGlobal ? null : sessionId,
35
+ });
36
+
37
+ // 3. Get high-importance memories
38
+ const importantMemories = store.getMemoriesByImportance({
39
+ limit: limit * 2,
40
+ sessionId: includeGlobal ? null : sessionId,
41
+ });
42
+
43
+ // 4. Merge and deduplicate
44
+ const candidates = mergeUnique([ftsResults, recentMemories, importantMemories]);
45
+
46
+ // 5. Score and rank
47
+ const scored = candidates.map(memory => ({
48
+ memory,
49
+ score: calculateRetrievalScore(memory, query, {
50
+ recencyWeight,
51
+ importanceWeight,
52
+ relevanceWeight,
53
+ }),
54
+ }));
55
+
56
+ // 6. Sort by score and return top K
57
+ const topMemories = scored
58
+ .sort((a, b) => b.score - a.score)
59
+ .slice(0, limit)
60
+ .map(s => s.memory);
61
+
62
+ // 7. Update access counts asynchronously
63
+ setImmediate(() => {
64
+ for (const memory of topMemories) {
65
+ try {
66
+ store.incrementAccessCount(memory.id);
67
+ } catch (err) {
68
+ logger.warn({ err, memoryId: memory.id }, 'Failed to increment access count');
69
+ }
70
+ }
71
+ });
72
+
73
+ return topMemories;
74
+ } catch (err) {
75
+ logger.error({ err, query }, 'Memory retrieval failed');
76
+ return [];
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Calculate retrieval score for a memory
82
+ */
83
+ function calculateRetrievalScore(memory, query, weights) {
84
+ // Recency score: exponential decay based on last access
85
+ const ageMs = Date.now() - (memory.lastAccessedAt || memory.createdAt);
86
+ const halfLifeMs = 7 * 24 * 60 * 60 * 1000; // 7 days
87
+ const recencyScore = Math.exp(-ageMs / halfLifeMs);
88
+
89
+ // Importance score: direct from stored value
90
+ const importanceScore = memory.importance ?? 0.5;
91
+
92
+ // Relevance score: keyword overlap with query
93
+ const relevanceScore = calculateKeywordOverlap(memory.content, query);
94
+
95
+ // Weighted combination
96
+ return (
97
+ weights.recencyWeight * recencyScore +
98
+ weights.importanceWeight * importanceScore +
99
+ weights.relevanceWeight * relevanceScore
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Calculate keyword overlap between content and query
105
+ */
106
+ function calculateKeywordOverlap(content, query) {
107
+ const contentKeywords = new Set(search.extractKeywords(content));
108
+ const queryKeywords = search.extractKeywords(query);
109
+
110
+ if (queryKeywords.length === 0 || contentKeywords.size === 0) {
111
+ return 0.0;
112
+ }
113
+
114
+ let overlapCount = 0;
115
+ for (const keyword of queryKeywords) {
116
+ if (contentKeywords.has(keyword)) {
117
+ overlapCount++;
118
+ }
119
+ }
120
+
121
+ return overlapCount / queryKeywords.length;
122
+ }
123
+
124
+ /**
125
+ * Merge arrays and remove duplicates by memory ID
126
+ */
127
+ function mergeUnique(arrays) {
128
+ const seen = new Set();
129
+ const merged = [];
130
+
131
+ for (const arr of arrays) {
132
+ for (const item of arr) {
133
+ if (!seen.has(item.id)) {
134
+ seen.add(item.id);
135
+ merged.push(item);
136
+ }
137
+ }
138
+ }
139
+
140
+ return merged;
141
+ }
142
+
143
+ /**
144
+ * Extract query from message (handle different formats)
145
+ */
146
+ function extractQueryFromMessage(message) {
147
+ if (!message) return '';
148
+
149
+ if (typeof message === 'string') return message;
150
+
151
+ if (message.content) {
152
+ if (typeof message.content === 'string') return message.content;
153
+
154
+ if (Array.isArray(message.content)) {
155
+ return message.content
156
+ .filter(block => block?.type === 'text' || typeof block === 'string')
157
+ .map(block => typeof block === 'string' ? block : block.text)
158
+ .filter(Boolean)
159
+ .join(' ');
160
+ }
161
+ }
162
+
163
+ return '';
164
+ }
165
+
166
+ /**
167
+ * Format memories for injection into context
168
+ */
169
+ function formatMemoriesForContext(memories) {
170
+ if (!memories || memories.length === 0) return '';
171
+
172
+ return memories
173
+ .map((memory, index) => {
174
+ const age = formatAge(Date.now() - memory.createdAt);
175
+ const typeLabel = memory.type || 'memory';
176
+ return `${index + 1}. [${typeLabel}] ${memory.content} (${age})`;
177
+ })
178
+ .join('\n');
179
+ }
180
+
181
+ /**
182
+ * Format age in human-readable form
183
+ */
184
+ function formatAge(ageMs) {
185
+ const seconds = Math.floor(ageMs / 1000);
186
+ const minutes = Math.floor(seconds / 60);
187
+ const hours = Math.floor(minutes / 60);
188
+ const days = Math.floor(hours / 24);
189
+ const weeks = Math.floor(days / 7);
190
+
191
+ if (weeks > 0) return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
192
+ if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
193
+ if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
194
+ if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
195
+ return 'just now';
196
+ }
197
+
198
+ /**
199
+ * Inject memories into system prompt
200
+ */
201
+ function injectMemoriesIntoSystem(existingSystem, memories, format = 'system') {
202
+ if (!memories || memories.length === 0) return existingSystem;
203
+
204
+ const formattedMemories = formatMemoriesForContext(memories);
205
+
206
+ if (format === 'system') {
207
+ const memoryBlock = `
208
+ <long_term_memory>
209
+ The following are relevant facts and context from previous conversations:
210
+ ${formattedMemories}
211
+ </long_term_memory>`;
212
+
213
+ return existingSystem
214
+ ? `${existingSystem}\n${memoryBlock}`
215
+ : memoryBlock;
216
+ }
217
+
218
+ if (format === 'assistant_preamble') {
219
+ return {
220
+ system: existingSystem,
221
+ memoryPreamble: formattedMemories,
222
+ };
223
+ }
224
+
225
+ return existingSystem;
226
+ }
227
+
228
+ /**
229
+ * Get memory statistics
230
+ */
231
+ function getMemoryStats(sessionId = null) {
232
+ try {
233
+ const total = store.countMemories();
234
+ const byType = {};
235
+ const types = ['preference', 'decision', 'fact', 'entity', 'relationship'];
236
+
237
+ for (const type of types) {
238
+ byType[type] = store.getMemoriesByType(type, 1000).length;
239
+ }
240
+
241
+ const recent = store.getRecentMemories({ limit: 10, sessionId });
242
+ const important = store.getMemoriesByImportance({ limit: 10, sessionId });
243
+
244
+ return {
245
+ total,
246
+ byType,
247
+ recentCount: recent.length,
248
+ importantCount: important.length,
249
+ sessionId,
250
+ };
251
+ } catch (err) {
252
+ logger.error({ err }, 'Failed to get memory stats');
253
+ return null;
254
+ }
255
+ }
256
+
257
+ module.exports = {
258
+ retrieveRelevantMemories,
259
+ calculateRetrievalScore,
260
+ calculateKeywordOverlap,
261
+ extractQueryFromMessage,
262
+ formatMemoriesForContext,
263
+ injectMemoriesIntoSystem,
264
+ formatAge,
265
+ getMemoryStats,
266
+ };