server-memory-enhanced 0.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import { z } from "zod";
5
5
  import { promises as fs } from 'fs';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
+ import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
9
+ import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema } from './lib/schemas.js';
10
+ import { handleSaveMemory } from './lib/save-memory-handler.js';
8
11
  // Define memory directory path using environment variable with fallback
9
12
  export const defaultMemoryDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory-data');
10
13
  export async function ensureMemoryDirectory() {
@@ -24,661 +27,56 @@ export async function ensureMemoryDirectory() {
24
27
  }
25
28
  // Initialize memory directory path (will be set during startup)
26
29
  let MEMORY_DIR_PATH;
27
- // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
28
- export class KnowledgeGraphManager {
29
- memoryDirPath;
30
- static NEGATION_WORDS = new Set(['not', 'no', 'never', 'neither', 'none', 'doesn\'t', 'don\'t', 'isn\'t', 'aren\'t']);
31
- constructor(memoryDirPath) {
32
- this.memoryDirPath = memoryDirPath;
33
- }
34
- getThreadFilePath(agentThreadId) {
35
- return path.join(this.memoryDirPath, `thread-${agentThreadId}.jsonl`);
36
- }
37
- async loadGraphFromFile(filePath) {
38
- try {
39
- const data = await fs.readFile(filePath, "utf-8");
40
- const lines = data.split("\n").filter(line => line.trim() !== "");
41
- return lines.reduce((graph, line) => {
42
- let item;
43
- try {
44
- item = JSON.parse(line);
45
- }
46
- catch (parseError) {
47
- console.warn(`Skipping malformed JSON line in ${filePath} (line length: ${line.length} chars)`);
48
- return graph;
49
- }
50
- if (item.type === "entity") {
51
- // Validate required fields
52
- if (!item.name || !item.entityType || !Array.isArray(item.observations) ||
53
- !item.agentThreadId || !item.timestamp ||
54
- typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
55
- console.warn(`Skipping entity with missing required fields in ${filePath}`);
56
- return graph;
57
- }
58
- graph.entities.push({
59
- name: item.name,
60
- entityType: item.entityType,
61
- observations: item.observations,
62
- agentThreadId: item.agentThreadId,
63
- timestamp: item.timestamp,
64
- confidence: item.confidence,
65
- importance: item.importance
66
- });
67
- }
68
- if (item.type === "relation") {
69
- // Validate required fields
70
- if (!item.from || !item.to || !item.relationType ||
71
- !item.agentThreadId || !item.timestamp ||
72
- typeof item.confidence !== 'number' || typeof item.importance !== 'number') {
73
- console.warn(`Skipping relation with missing required fields in ${filePath}`);
74
- return graph;
75
- }
76
- graph.relations.push({
77
- from: item.from,
78
- to: item.to,
79
- relationType: item.relationType,
80
- agentThreadId: item.agentThreadId,
81
- timestamp: item.timestamp,
82
- confidence: item.confidence,
83
- importance: item.importance
84
- });
85
- }
86
- return graph;
87
- }, { entities: [], relations: [] });
88
- }
89
- catch (error) {
90
- if (error instanceof Error && 'code' in error && error.code === "ENOENT") {
91
- return { entities: [], relations: [] };
92
- }
93
- throw error;
94
- }
95
- }
96
- async loadGraph() {
97
- const files = await fs.readdir(this.memoryDirPath).catch(() => []);
98
- const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
99
- const graphs = await Promise.all(threadFiles.map(f => this.loadGraphFromFile(path.join(this.memoryDirPath, f))));
100
- return graphs.reduce((acc, graph) => ({
101
- entities: [...acc.entities, ...graph.entities],
102
- relations: [...acc.relations, ...graph.relations]
103
- }), { entities: [], relations: [] });
104
- }
105
- async saveGraphForThread(agentThreadId, entities, relations) {
106
- const threadFilePath = this.getThreadFilePath(agentThreadId);
107
- const lines = [
108
- ...entities.map(e => JSON.stringify({
109
- type: "entity",
110
- name: e.name,
111
- entityType: e.entityType,
112
- observations: e.observations,
113
- agentThreadId: e.agentThreadId,
114
- timestamp: e.timestamp,
115
- confidence: e.confidence,
116
- importance: e.importance
117
- })),
118
- ...relations.map(r => JSON.stringify({
119
- type: "relation",
120
- from: r.from,
121
- to: r.to,
122
- relationType: r.relationType,
123
- agentThreadId: r.agentThreadId,
124
- timestamp: r.timestamp,
125
- confidence: r.confidence,
126
- importance: r.importance
127
- })),
128
- ];
129
- // Avoid creating or keeping empty files when there is no data for this thread
130
- if (lines.length === 0) {
131
- try {
132
- await fs.unlink(threadFilePath);
133
- }
134
- catch (error) {
135
- // Only ignore ENOENT errors (file doesn't exist)
136
- if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
137
- console.warn(`Failed to delete empty thread file ${threadFilePath}:`, error);
138
- }
139
- }
140
- return;
141
- }
142
- await fs.writeFile(threadFilePath, lines.join("\n"));
143
- }
144
- async saveGraph(graph) {
145
- // Group entities and relations by agentThreadId
146
- const threadMap = new Map();
147
- for (const entity of graph.entities) {
148
- if (!threadMap.has(entity.agentThreadId)) {
149
- threadMap.set(entity.agentThreadId, { entities: [], relations: [] });
150
- }
151
- threadMap.get(entity.agentThreadId).entities.push(entity);
152
- }
153
- for (const relation of graph.relations) {
154
- if (!threadMap.has(relation.agentThreadId)) {
155
- threadMap.set(relation.agentThreadId, { entities: [], relations: [] });
156
- }
157
- threadMap.get(relation.agentThreadId).relations.push(relation);
158
- }
159
- // Save each thread's data to its own file
160
- await Promise.all(Array.from(threadMap.entries()).map(([threadId, data]) => this.saveGraphForThread(threadId, data.entities, data.relations)));
161
- // Clean up stale thread files that no longer have data
162
- try {
163
- const files = await fs.readdir(this.memoryDirPath).catch(() => []);
164
- const threadFiles = files.filter(f => f.startsWith('thread-') && f.endsWith('.jsonl'));
165
- await Promise.all(threadFiles.map(async (fileName) => {
166
- // Extract threadId from filename: thread-{agentThreadId}.jsonl
167
- const match = fileName.match(/^thread-(.+)\.jsonl$/);
168
- if (match) {
169
- const threadId = match[1];
170
- if (!threadMap.has(threadId)) {
171
- const filePath = path.join(this.memoryDirPath, fileName);
172
- try {
173
- await fs.unlink(filePath);
174
- }
175
- catch (error) {
176
- // Only log non-ENOENT errors
177
- if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
178
- console.warn(`Failed to delete stale thread file ${filePath}:`, error);
179
- }
180
- }
181
- }
182
- }
183
- }));
184
- }
185
- catch (error) {
186
- // Best-effort cleanup: log but don't fail the save operation
187
- console.warn('Failed to clean up stale thread files:', error);
188
- }
189
- }
190
- async createEntities(entities) {
191
- const graph = await this.loadGraph();
192
- // Entity names are globally unique across all threads in the collaborative knowledge graph
193
- // This prevents duplicate entities while allowing multiple threads to contribute to the same entity
194
- const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
195
- graph.entities.push(...newEntities);
196
- await this.saveGraph(graph);
197
- return newEntities;
198
- }
199
- async createRelations(relations) {
200
- const graph = await this.loadGraph();
201
- // Validate that referenced entities exist
202
- const entityNames = new Set(graph.entities.map(e => e.name));
203
- const validRelations = relations.filter(r => {
204
- if (!entityNames.has(r.from) || !entityNames.has(r.to)) {
205
- console.warn(`Skipping relation ${r.from} -> ${r.to}: one or both entities do not exist`);
206
- return false;
207
- }
208
- return true;
209
- });
210
- // Relations are globally unique by (from, to, relationType) across all threads
211
- // This enables multiple threads to collaboratively build the knowledge graph
212
- const newRelations = validRelations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
213
- existingRelation.to === r.to &&
214
- existingRelation.relationType === r.relationType));
215
- graph.relations.push(...newRelations);
216
- await this.saveGraph(graph);
217
- return newRelations;
218
- }
219
- async addObservations(observations) {
220
- const graph = await this.loadGraph();
221
- const results = observations.map(o => {
222
- const entity = graph.entities.find(e => e.name === o.entityName);
223
- if (!entity) {
224
- throw new Error(`Entity with name ${o.entityName} not found`);
225
- }
226
- const newObservations = o.contents.filter(content => !entity.observations.includes(content));
227
- entity.observations.push(...newObservations);
228
- // Update metadata based on this operation, but keep original agentThreadId
229
- // to maintain thread file consistency and avoid orphaned data
230
- entity.timestamp = o.timestamp;
231
- entity.confidence = o.confidence;
232
- entity.importance = o.importance;
233
- return { entityName: o.entityName, addedObservations: newObservations };
234
- });
235
- await this.saveGraph(graph);
236
- return results;
237
- }
238
- async deleteEntities(entityNames) {
239
- const graph = await this.loadGraph();
240
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
241
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
242
- await this.saveGraph(graph);
243
- }
244
- async deleteObservations(deletions) {
245
- const graph = await this.loadGraph();
246
- deletions.forEach(d => {
247
- const entity = graph.entities.find(e => e.name === d.entityName);
248
- if (entity) {
249
- entity.observations = entity.observations.filter(o => !d.observations.includes(o));
250
- }
251
- });
252
- await this.saveGraph(graph);
253
- }
254
- async deleteRelations(relations) {
255
- const graph = await this.loadGraph();
256
- // Delete relations globally across all threads by matching (from, to, relationType)
257
- // In a collaborative knowledge graph, deletions affect all threads
258
- graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
259
- r.to === delRelation.to &&
260
- r.relationType === delRelation.relationType));
261
- await this.saveGraph(graph);
262
- }
263
- async readGraph() {
264
- return this.loadGraph();
265
- }
266
- async searchNodes(query) {
267
- const graph = await this.loadGraph();
268
- // Filter entities
269
- const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) ||
270
- e.entityType.toLowerCase().includes(query.toLowerCase()) ||
271
- e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())));
272
- // Create a Set of filtered entity names for quick lookup
273
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
274
- // Filter relations to only include those between filtered entities
275
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
276
- const filteredGraph = {
277
- entities: filteredEntities,
278
- relations: filteredRelations,
279
- };
280
- return filteredGraph;
281
- }
282
- async openNodes(names) {
283
- const graph = await this.loadGraph();
284
- // Filter entities
285
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
286
- // Create a Set of filtered entity names for quick lookup
287
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
288
- // Filter relations to only include those between filtered entities
289
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
290
- const filteredGraph = {
291
- entities: filteredEntities,
292
- relations: filteredRelations,
293
- };
294
- return filteredGraph;
295
- }
296
- async queryNodes(filters) {
297
- const graph = await this.loadGraph();
298
- // If no filters provided, return entire graph
299
- if (!filters) {
300
- return graph;
301
- }
302
- // Apply filters to entities
303
- const filteredEntities = graph.entities.filter(e => {
304
- // Timestamp range filter
305
- if (filters.timestampStart && e.timestamp < filters.timestampStart)
306
- return false;
307
- if (filters.timestampEnd && e.timestamp > filters.timestampEnd)
308
- return false;
309
- // Confidence range filter
310
- if (filters.confidenceMin !== undefined && e.confidence < filters.confidenceMin)
311
- return false;
312
- if (filters.confidenceMax !== undefined && e.confidence > filters.confidenceMax)
313
- return false;
314
- // Importance range filter
315
- if (filters.importanceMin !== undefined && e.importance < filters.importanceMin)
316
- return false;
317
- if (filters.importanceMax !== undefined && e.importance > filters.importanceMax)
318
- return false;
319
- return true;
320
- });
321
- // Create a Set of filtered entity names for quick lookup
322
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
323
- // Apply filters to relations (and ensure they connect filtered entities)
324
- const filteredRelations = graph.relations.filter(r => {
325
- // Must connect filtered entities
326
- if (!filteredEntityNames.has(r.from) || !filteredEntityNames.has(r.to))
327
- return false;
328
- // Timestamp range filter
329
- if (filters.timestampStart && r.timestamp < filters.timestampStart)
330
- return false;
331
- if (filters.timestampEnd && r.timestamp > filters.timestampEnd)
332
- return false;
333
- // Confidence range filter
334
- if (filters.confidenceMin !== undefined && r.confidence < filters.confidenceMin)
335
- return false;
336
- if (filters.confidenceMax !== undefined && r.confidence > filters.confidenceMax)
337
- return false;
338
- // Importance range filter
339
- if (filters.importanceMin !== undefined && r.importance < filters.importanceMin)
340
- return false;
341
- if (filters.importanceMax !== undefined && r.importance > filters.importanceMax)
342
- return false;
343
- return true;
344
- });
345
- return {
346
- entities: filteredEntities,
347
- relations: filteredRelations,
348
- };
349
- }
350
- // Enhancement 1: Memory Statistics & Insights
351
- async getMemoryStats() {
352
- const graph = await this.loadGraph();
353
- // Count entity types
354
- const entityTypes = {};
355
- graph.entities.forEach(e => {
356
- entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
357
- });
358
- // Calculate averages
359
- const avgConfidence = graph.entities.length > 0
360
- ? graph.entities.reduce((sum, e) => sum + e.confidence, 0) / graph.entities.length
361
- : 0;
362
- const avgImportance = graph.entities.length > 0
363
- ? graph.entities.reduce((sum, e) => sum + e.importance, 0) / graph.entities.length
364
- : 0;
365
- // Count unique threads
366
- const threads = new Set([
367
- ...graph.entities.map(e => e.agentThreadId),
368
- ...graph.relations.map(r => r.agentThreadId)
369
- ]);
370
- // Recent activity (last 7 days, grouped by day)
371
- const now = new Date();
372
- const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
373
- const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
374
- // Group by day
375
- const activityByDay = {};
376
- recentEntities.forEach(e => {
377
- const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
378
- activityByDay[day] = (activityByDay[day] || 0) + 1;
379
- });
380
- const recentActivity = Object.entries(activityByDay)
381
- .map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
382
- .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
383
- return {
384
- entityCount: graph.entities.length,
385
- relationCount: graph.relations.length,
386
- threadCount: threads.size,
387
- entityTypes,
388
- avgConfidence,
389
- avgImportance,
390
- recentActivity
391
- };
392
- }
393
- // Enhancement 2: Get recent changes
394
- async getRecentChanges(since) {
395
- const graph = await this.loadGraph();
396
- const sinceDate = new Date(since);
397
- // Only return entities and relations that were actually modified since the specified time
398
- const recentEntities = graph.entities.filter(e => new Date(e.timestamp) >= sinceDate);
399
- const recentEntityNames = new Set(recentEntities.map(e => e.name));
400
- // Only include relations that are recent themselves
401
- const recentRelations = graph.relations.filter(r => new Date(r.timestamp) >= sinceDate);
402
- return {
403
- entities: recentEntities,
404
- relations: recentRelations
405
- };
406
- }
407
- // Enhancement 3: Relationship path finding
408
- async findRelationPath(from, to, maxDepth = 5) {
409
- const graph = await this.loadGraph();
410
- if (from === to) {
411
- return { found: true, path: [from], relations: [] };
412
- }
413
- // BFS to find shortest path
414
- const queue = [
415
- { entity: from, path: [from], relations: [] }
416
- ];
417
- const visited = new Set([from]);
418
- while (queue.length > 0) {
419
- const current = queue.shift();
420
- if (current.path.length > maxDepth) {
421
- continue;
422
- }
423
- // Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
424
- const outgoing = graph.relations.filter(r => r.from === current.entity);
425
- const incoming = graph.relations.filter(r => r.to === current.entity);
426
- // Check outgoing relations
427
- for (const rel of outgoing) {
428
- if (rel.to === to) {
429
- return {
430
- found: true,
431
- path: [...current.path, rel.to],
432
- relations: [...current.relations, rel]
433
- };
434
- }
435
- if (!visited.has(rel.to)) {
436
- visited.add(rel.to);
437
- queue.push({
438
- entity: rel.to,
439
- path: [...current.path, rel.to],
440
- relations: [...current.relations, rel]
441
- });
442
- }
443
- }
444
- // Check incoming relations (traverse backwards)
445
- for (const rel of incoming) {
446
- if (rel.from === to) {
447
- return {
448
- found: true,
449
- path: [...current.path, rel.from],
450
- relations: [...current.relations, rel]
451
- };
452
- }
453
- if (!visited.has(rel.from)) {
454
- visited.add(rel.from);
455
- queue.push({
456
- entity: rel.from,
457
- path: [...current.path, rel.from],
458
- relations: [...current.relations, rel]
459
- });
460
- }
461
- }
462
- }
463
- return { found: false, path: [], relations: [] };
464
- }
465
- // Enhancement 4: Detect conflicting observations
466
- async detectConflicts() {
467
- const graph = await this.loadGraph();
468
- const conflicts = [];
469
- for (const entity of graph.entities) {
470
- const entityConflicts = [];
471
- for (let i = 0; i < entity.observations.length; i++) {
472
- for (let j = i + 1; j < entity.observations.length; j++) {
473
- const obs1 = entity.observations[i].toLowerCase();
474
- const obs2 = entity.observations[j].toLowerCase();
475
- // Check for negation patterns
476
- const obs1HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs1.includes(word));
477
- const obs2HasNegation = Array.from(KnowledgeGraphManager.NEGATION_WORDS).some(word => obs2.includes(word));
478
- // If one has negation and they share key words, might be a conflict
479
- if (obs1HasNegation !== obs2HasNegation) {
480
- const words1 = obs1.split(/\s+/).filter(w => w.length > 3);
481
- const words2Set = new Set(obs2.split(/\s+/).filter(w => w.length > 3));
482
- const commonWords = words1.filter(w => words2Set.has(w) && !KnowledgeGraphManager.NEGATION_WORDS.has(w));
483
- if (commonWords.length >= 2) {
484
- entityConflicts.push({
485
- obs1: entity.observations[i],
486
- obs2: entity.observations[j],
487
- reason: 'Potential contradiction with negation'
488
- });
489
- }
490
- }
491
- }
492
- }
493
- if (entityConflicts.length > 0) {
494
- conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
495
- }
496
- }
497
- return conflicts;
498
- }
499
- // Enhancement 5: Memory pruning
500
- async pruneMemory(options) {
501
- const graph = await this.loadGraph();
502
- const initialEntityCount = graph.entities.length;
503
- const initialRelationCount = graph.relations.length;
504
- // Filter entities to remove
505
- let entitiesToKeep = graph.entities;
506
- if (options.olderThan) {
507
- const cutoffDate = new Date(options.olderThan);
508
- entitiesToKeep = entitiesToKeep.filter(e => new Date(e.timestamp) >= cutoffDate);
509
- }
510
- if (options.importanceLessThan !== undefined) {
511
- entitiesToKeep = entitiesToKeep.filter(e => e.importance >= options.importanceLessThan);
512
- }
513
- // Ensure we keep minimum entities
514
- // If keepMinEntities is set and we need more entities, take from the already-filtered set
515
- // sorted by importance and recency
516
- if (options.keepMinEntities && entitiesToKeep.length < options.keepMinEntities) {
517
- // Sort the filtered entities by importance and timestamp, keep the most important and recent
518
- const sorted = [...entitiesToKeep].sort((a, b) => {
519
- if (a.importance !== b.importance)
520
- return b.importance - a.importance;
521
- return b.timestamp.localeCompare(a.timestamp);
522
- });
523
- // If we still don't have enough, we keep what we have
524
- entitiesToKeep = sorted.slice(0, Math.min(options.keepMinEntities, sorted.length));
525
- }
526
- const keptEntityNames = new Set(entitiesToKeep.map(e => e.name));
527
- // Remove relations that reference removed entities
528
- const relationsToKeep = graph.relations.filter(r => keptEntityNames.has(r.from) && keptEntityNames.has(r.to));
529
- graph.entities = entitiesToKeep;
530
- graph.relations = relationsToKeep;
531
- await this.saveGraph(graph);
532
- return {
533
- removedEntities: initialEntityCount - entitiesToKeep.length,
534
- removedRelations: initialRelationCount - relationsToKeep.length
535
- };
536
- }
537
- // Enhancement 6: Batch operations
538
- async bulkUpdate(updates) {
539
- const graph = await this.loadGraph();
540
- let updated = 0;
541
- const notFound = [];
542
- for (const update of updates) {
543
- const entity = graph.entities.find(e => e.name === update.entityName);
544
- if (!entity) {
545
- notFound.push(update.entityName);
546
- continue;
547
- }
548
- if (update.confidence !== undefined) {
549
- entity.confidence = update.confidence;
550
- }
551
- if (update.importance !== undefined) {
552
- entity.importance = update.importance;
553
- }
554
- if (update.addObservations) {
555
- const newObs = update.addObservations.filter(obs => !entity.observations.includes(obs));
556
- entity.observations.push(...newObs);
557
- }
558
- entity.timestamp = new Date().toISOString();
559
- updated++;
560
- }
561
- await this.saveGraph(graph);
562
- return { updated, notFound };
563
- }
564
- // Enhancement 7: Flag for review (Human-in-the-Loop)
565
- async flagForReview(entityName, reason, reviewer) {
566
- const graph = await this.loadGraph();
567
- const entity = graph.entities.find(e => e.name === entityName);
568
- if (!entity) {
569
- throw new Error(`Entity with name ${entityName} not found`);
570
- }
571
- // Add a special observation to mark for review
572
- const flagObservation = `[FLAGGED FOR REVIEW: ${reason}${reviewer ? ` - Reviewer: ${reviewer}` : ''}]`;
573
- if (!entity.observations.includes(flagObservation)) {
574
- entity.observations.push(flagObservation);
575
- entity.timestamp = new Date().toISOString();
576
- await this.saveGraph(graph);
577
- }
578
- }
579
- // Enhancement 8: Get entities flagged for review
580
- async getFlaggedEntities() {
581
- const graph = await this.loadGraph();
582
- return graph.entities.filter(e => e.observations.some(obs => obs.includes('[FLAGGED FOR REVIEW:')));
583
- }
584
- // Enhancement 9: Get context (entities related to a topic/entity)
585
- async getContext(entityNames, depth = 1) {
586
- const graph = await this.loadGraph();
587
- const contextEntityNames = new Set(entityNames);
588
- // Expand to include related entities up to specified depth
589
- for (let d = 0; d < depth; d++) {
590
- const currentEntities = Array.from(contextEntityNames);
591
- for (const entityName of currentEntities) {
592
- // Find all relations involving this entity
593
- const relatedRelations = graph.relations.filter(r => r.from === entityName || r.to === entityName);
594
- // Add related entities
595
- relatedRelations.forEach(r => {
596
- contextEntityNames.add(r.from);
597
- contextEntityNames.add(r.to);
598
- });
599
- }
600
- }
601
- // Get all entities and relations in context
602
- const contextEntities = graph.entities.filter(e => contextEntityNames.has(e.name));
603
- const contextRelations = graph.relations.filter(r => contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
604
- return {
605
- entities: contextEntities,
606
- relations: contextRelations
607
- };
608
- }
609
- // Enhancement 10: List conversations (agent threads)
610
- async listConversations() {
611
- const graph = await this.loadGraph();
612
- // Group data by agent thread
613
- const threadMap = new Map();
614
- // Collect entities by thread
615
- for (const entity of graph.entities) {
616
- if (!threadMap.has(entity.agentThreadId)) {
617
- threadMap.set(entity.agentThreadId, { entities: [], relations: [], timestamps: [] });
618
- }
619
- const threadData = threadMap.get(entity.agentThreadId);
620
- threadData.entities.push(entity);
621
- threadData.timestamps.push(entity.timestamp);
622
- }
623
- // Collect relations by thread
624
- for (const relation of graph.relations) {
625
- if (!threadMap.has(relation.agentThreadId)) {
626
- threadMap.set(relation.agentThreadId, { entities: [], relations: [], timestamps: [] });
627
- }
628
- const threadData = threadMap.get(relation.agentThreadId);
629
- threadData.relations.push(relation);
630
- threadData.timestamps.push(relation.timestamp);
631
- }
632
- // Build conversation summaries
633
- const conversations = Array.from(threadMap.entries()).map(([agentThreadId, data]) => {
634
- const timestamps = data.timestamps.sort((a, b) => a.localeCompare(b));
635
- return {
636
- agentThreadId,
637
- entityCount: data.entities.length,
638
- relationCount: data.relations.length,
639
- firstCreated: timestamps[0] || '',
640
- lastUpdated: timestamps[timestamps.length - 1] || ''
641
- };
642
- });
643
- // Sort by last updated (most recent first)
644
- conversations.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
645
- return { conversations };
646
- }
647
- }
30
+ export { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
31
+ export { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
32
+ export { Neo4jStorageAdapter } from './lib/neo4j-storage-adapter.js';
648
33
  let knowledgeGraphManager;
649
34
  // Zod schemas for enhanced entities and relations
650
- const EntitySchema = z.object({
651
- name: z.string().describe("The name of the entity"),
652
- entityType: z.string().describe("The type of the entity"),
653
- observations: z.array(z.string()).describe("An array of observation contents associated with the entity"),
654
- agentThreadId: z.string().describe("The agent thread ID that created this entity"),
655
- timestamp: z.string().describe("ISO 8601 timestamp of when the entity was created"),
656
- confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
657
- importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
658
- });
659
- const RelationSchema = z.object({
660
- from: z.string().describe("The name of the entity where the relation starts"),
661
- to: z.string().describe("The name of the entity where the relation ends"),
662
- relationType: z.string().describe("The type of the relation"),
663
- agentThreadId: z.string().describe("The agent thread ID that created this relation"),
664
- timestamp: z.string().describe("ISO 8601 timestamp of when the relation was created"),
665
- confidence: z.number().min(0).max(1).describe("Confidence coefficient from 0 to 1"),
666
- importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
667
- });
35
+ const EntitySchemaCompat = EntitySchema;
36
+ const RelationSchemaCompat = RelationSchema;
668
37
  // The server instance and tools exposed to Claude
669
38
  const server = new McpServer({
670
39
  name: "memory-enhanced-server",
671
40
  version: "0.2.0",
672
41
  });
673
- // Register create_entities tool
42
+ // Register NEW save_memory tool (Section 1 of spec - Unified Tool)
43
+ server.registerTool("save_memory", {
44
+ title: "Save Memory",
45
+ description: "Save entities and their relations to memory graph atomically. RULES: 1) Each observation max 150 chars (atomic facts only). 2) Each entity MUST have at least 1 relation. This is the recommended way to create entities and relations.",
46
+ inputSchema: SaveMemoryInputSchema,
47
+ outputSchema: SaveMemoryOutputSchema
48
+ }, async (input) => {
49
+ const result = await handleSaveMemory(input, (entities) => knowledgeGraphManager.createEntities(entities), (relations) => knowledgeGraphManager.createRelations(relations));
50
+ if (result.success) {
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: `✓ Successfully saved ${result.created.entities} entities and ${result.created.relations} relations.\n` +
55
+ `Quality score: ${(result.quality_score * 100).toFixed(1)}%\n` +
56
+ (result.warnings.length > 0 ? `\nWarnings:\n${result.warnings.join('\n')}` : '')
57
+ }],
58
+ structuredContent: result
59
+ };
60
+ }
61
+ else {
62
+ return {
63
+ content: [{
64
+ type: "text",
65
+ text: `✗ Validation failed:\n${result.validation_errors?.join('\n')}`
66
+ }],
67
+ structuredContent: result,
68
+ isError: true
69
+ };
70
+ }
71
+ });
674
72
  server.registerTool("create_entities", {
675
73
  title: "Create Entities",
676
74
  description: "Create multiple new entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance)",
677
75
  inputSchema: {
678
- entities: z.array(EntitySchema)
76
+ entities: z.array(EntitySchemaCompat)
679
77
  },
680
78
  outputSchema: {
681
- entities: z.array(EntitySchema)
79
+ entities: z.array(EntitySchemaCompat)
682
80
  }
683
81
  }, async ({ entities }) => {
684
82
  const result = await knowledgeGraphManager.createEntities(entities);
@@ -692,10 +90,10 @@ server.registerTool("create_relations", {
692
90
  title: "Create Relations",
693
91
  description: "Create multiple new relations between entities in the knowledge graph with metadata (agent thread ID, timestamp, confidence, importance). Relations should be in active voice",
694
92
  inputSchema: {
695
- relations: z.array(RelationSchema)
93
+ relations: z.array(RelationSchemaCompat)
696
94
  },
697
95
  outputSchema: {
698
- relations: z.array(RelationSchema)
96
+ relations: z.array(RelationSchemaCompat)
699
97
  }
700
98
  }, async ({ relations }) => {
701
99
  const result = await knowledgeGraphManager.createRelations(relations);
@@ -775,7 +173,7 @@ server.registerTool("delete_relations", {
775
173
  title: "Delete Relations",
776
174
  description: "Delete multiple relations from the knowledge graph",
777
175
  inputSchema: {
778
- relations: z.array(RelationSchema).describe("An array of relations to delete")
176
+ relations: z.array(RelationSchemaCompat).describe("An array of relations to delete")
779
177
  },
780
178
  outputSchema: {
781
179
  success: z.boolean(),
@@ -794,8 +192,8 @@ server.registerTool("read_graph", {
794
192
  description: "Read the entire knowledge graph",
795
193
  inputSchema: {},
796
194
  outputSchema: {
797
- entities: z.array(EntitySchema),
798
- relations: z.array(RelationSchema)
195
+ entities: z.array(EntitySchemaCompat),
196
+ relations: z.array(RelationSchemaCompat)
799
197
  }
800
198
  }, async () => {
801
199
  const graph = await knowledgeGraphManager.readGraph();
@@ -812,8 +210,8 @@ server.registerTool("search_nodes", {
812
210
  query: z.string().describe("The search query to match against entity names, types, and observation content")
813
211
  },
814
212
  outputSchema: {
815
- entities: z.array(EntitySchema),
816
- relations: z.array(RelationSchema)
213
+ entities: z.array(EntitySchemaCompat),
214
+ relations: z.array(RelationSchemaCompat)
817
215
  }
818
216
  }, async ({ query }) => {
819
217
  const graph = await knowledgeGraphManager.searchNodes(query);
@@ -830,8 +228,8 @@ server.registerTool("open_nodes", {
830
228
  names: z.array(z.string()).describe("An array of entity names to retrieve")
831
229
  },
832
230
  outputSchema: {
833
- entities: z.array(EntitySchema),
834
- relations: z.array(RelationSchema)
231
+ entities: z.array(EntitySchemaCompat),
232
+ relations: z.array(RelationSchemaCompat)
835
233
  }
836
234
  }, async ({ names }) => {
837
235
  const graph = await knowledgeGraphManager.openNodes(names);
@@ -853,8 +251,8 @@ server.registerTool("query_nodes", {
853
251
  importanceMax: z.number().min(0).max(1).optional().describe("Maximum importance value (0-1)")
854
252
  },
855
253
  outputSchema: {
856
- entities: z.array(EntitySchema),
857
- relations: z.array(RelationSchema)
254
+ entities: z.array(EntitySchemaCompat),
255
+ relations: z.array(RelationSchemaCompat)
858
256
  }
859
257
  }, async (filters) => {
860
258
  const graph = await knowledgeGraphManager.queryNodes(filters);
@@ -895,8 +293,8 @@ server.registerTool("get_recent_changes", {
895
293
  since: z.string().describe("ISO 8601 timestamp - return changes since this time")
896
294
  },
897
295
  outputSchema: {
898
- entities: z.array(EntitySchema),
899
- relations: z.array(RelationSchema)
296
+ entities: z.array(EntitySchemaCompat),
297
+ relations: z.array(RelationSchemaCompat)
900
298
  }
901
299
  }, async ({ since }) => {
902
300
  const changes = await knowledgeGraphManager.getRecentChanges(since);
@@ -917,7 +315,7 @@ server.registerTool("find_relation_path", {
917
315
  outputSchema: {
918
316
  found: z.boolean(),
919
317
  path: z.array(z.string()),
920
- relations: z.array(RelationSchema)
318
+ relations: z.array(RelationSchemaCompat)
921
319
  }
922
320
  }, async ({ from, to, maxDepth }) => {
923
321
  const result = await knowledgeGraphManager.findRelationPath(from, to, maxDepth || 5);
@@ -1017,7 +415,7 @@ server.registerTool("get_flagged_entities", {
1017
415
  description: "Retrieve all entities that have been flagged for human review",
1018
416
  inputSchema: {},
1019
417
  outputSchema: {
1020
- entities: z.array(EntitySchema)
418
+ entities: z.array(EntitySchemaCompat)
1021
419
  }
1022
420
  }, async () => {
1023
421
  const entities = await knowledgeGraphManager.getFlaggedEntities();
@@ -1035,8 +433,8 @@ server.registerTool("get_context", {
1035
433
  depth: z.number().optional().default(1).describe("How many relationship hops to include (default: 1)")
1036
434
  },
1037
435
  outputSchema: {
1038
- entities: z.array(EntitySchema),
1039
- relations: z.array(RelationSchema)
436
+ entities: z.array(EntitySchemaCompat),
437
+ relations: z.array(RelationSchemaCompat)
1040
438
  }
1041
439
  }, async ({ entityNames, depth }) => {
1042
440
  const context = await knowledgeGraphManager.getContext(entityNames, depth || 1);
@@ -1066,6 +464,32 @@ server.registerTool("list_conversations", {
1066
464
  structuredContent: result
1067
465
  };
1068
466
  });
467
+ // Register get_analytics tool
468
+ server.registerTool("get_analytics", {
469
+ title: "Get Analytics",
470
+ description: "Get analytics for a specific thread with 4 core metrics: recent changes, top important entities, most connected entities, and orphaned entities",
471
+ inputSchema: GetAnalyticsInputSchema,
472
+ outputSchema: GetAnalyticsOutputSchema
473
+ }, async (input) => {
474
+ const result = await knowledgeGraphManager.getAnalytics(input.threadId);
475
+ return {
476
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
477
+ structuredContent: result
478
+ };
479
+ });
480
+ // Register get_observation_history tool
481
+ server.registerTool("get_observation_history", {
482
+ title: "Get Observation History",
483
+ description: "Retrieve the full version chain for a specific observation, showing how it evolved over time",
484
+ inputSchema: GetObservationHistoryInputSchema,
485
+ outputSchema: GetObservationHistoryOutputSchema
486
+ }, async (input) => {
487
+ const result = await knowledgeGraphManager.getObservationHistory(input.entityName, input.observationId);
488
+ return {
489
+ content: [{ type: "text", text: JSON.stringify({ history: result }, null, 2) }],
490
+ structuredContent: { history: result }
491
+ };
492
+ });
1069
493
  async function main() {
1070
494
  // Initialize memory directory path
1071
495
  MEMORY_DIR_PATH = await ensureMemoryDirectory();