server-memory-enhanced 2.3.1 → 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.
Files changed (30) hide show
  1. package/dist/index.js +45 -64
  2. package/dist/lib/analysis/analytics-service.js +111 -0
  3. package/dist/lib/analysis/conflict-detector.js +50 -0
  4. package/dist/lib/analysis/context-builder.js +34 -0
  5. package/dist/lib/analysis/memory-stats.js +67 -0
  6. package/dist/lib/analysis/path-finder.js +78 -0
  7. package/dist/lib/collaboration/conversation-service.js +43 -0
  8. package/dist/lib/collaboration/flag-manager.js +40 -0
  9. package/dist/lib/knowledge-graph-manager.js +102 -832
  10. package/dist/lib/maintenance/bulk-updater.js +44 -0
  11. package/dist/lib/maintenance/memory-pruner.js +48 -0
  12. package/dist/lib/operations/entity-operations.js +27 -0
  13. package/dist/lib/operations/observation-operations.js +105 -0
  14. package/dist/lib/operations/relation-operations.js +45 -0
  15. package/dist/lib/queries/entity-queries.js +70 -0
  16. package/dist/lib/queries/graph-reader.js +20 -0
  17. package/dist/lib/queries/search-service.js +113 -0
  18. package/dist/lib/schemas.js +61 -6
  19. package/dist/lib/utils/entity-finder.js +31 -0
  20. package/dist/lib/utils/negation-detector.js +23 -0
  21. package/dist/lib/utils/observation-validator.js +43 -0
  22. package/dist/lib/utils/relation-key.js +16 -0
  23. package/dist/lib/validation/entity-type-validator.js +32 -0
  24. package/dist/lib/validation/observation-validator.js +61 -0
  25. package/dist/lib/validation/quality-scorer.js +27 -0
  26. package/dist/lib/validation/relation-validator.js +36 -0
  27. package/dist/lib/validation/request-validator.js +91 -0
  28. package/dist/lib/validation.js +8 -200
  29. package/dist/lib/versioning/observation-history.js +60 -0
  30. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { promises as fs } from 'fs';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { KnowledgeGraphManager } from './lib/knowledge-graph-manager.js';
9
- import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema } from './lib/schemas.js';
9
+ import { EntitySchema, RelationSchema, SaveMemoryInputSchema, SaveMemoryOutputSchema, GetAnalyticsInputSchema, GetAnalyticsOutputSchema, GetObservationHistoryInputSchema, GetObservationHistoryOutputSchema, ListEntitiesInputSchema, ListEntitiesOutputSchema, ValidateMemoryInputSchema, ValidateMemoryOutputSchema, UpdateObservationInputSchema, UpdateObservationOutputSchema, ReadGraphInputSchema, SearchNodesInputSchema, OpenNodesInputSchema, QueryNodesInputSchema, GetMemoryStatsInputSchema, GetRecentChangesInputSchema, FindRelationPathInputSchema, DetectConflictsInputSchema, GetFlaggedEntitiesInputSchema, GetContextInputSchema } from './lib/schemas.js';
10
10
  import { handleSaveMemory } from './lib/save-memory-handler.js';
11
11
  import { validateSaveMemoryRequest } from './lib/validation.js';
12
12
  import { JsonlStorageAdapter } from './lib/jsonl-storage-adapter.js';
@@ -314,14 +314,14 @@ server.registerTool("delete_relations", {
314
314
  // Register read_graph tool
315
315
  server.registerTool("read_graph", {
316
316
  title: "Read Graph",
317
- description: "Read the entire knowledge graph",
318
- inputSchema: {},
317
+ description: "Read the knowledge graph for a specific thread (thread isolation enforced)",
318
+ inputSchema: ReadGraphInputSchema,
319
319
  outputSchema: {
320
320
  entities: z.array(EntitySchemaCompat),
321
321
  relations: z.array(RelationSchemaCompat)
322
322
  }
323
- }, async () => {
324
- const graph = await knowledgeGraphManager.readGraph();
323
+ }, async (input) => {
324
+ const graph = await knowledgeGraphManager.readGraph(input.threadId);
325
325
  return {
326
326
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
327
327
  structuredContent: { ...graph }
@@ -330,16 +330,14 @@ server.registerTool("read_graph", {
330
330
  // Register search_nodes tool
331
331
  server.registerTool("search_nodes", {
332
332
  title: "Search Nodes",
333
- description: "Search for nodes in the knowledge graph based on a query",
334
- inputSchema: {
335
- query: z.string().describe("The search query to match against entity names, types, and observation content")
336
- },
333
+ description: "Search for nodes in the knowledge graph based on a query (thread isolation enforced)",
334
+ inputSchema: SearchNodesInputSchema,
337
335
  outputSchema: {
338
336
  entities: z.array(EntitySchemaCompat),
339
337
  relations: z.array(RelationSchemaCompat)
340
338
  }
341
- }, async ({ query }) => {
342
- const graph = await knowledgeGraphManager.searchNodes(query);
339
+ }, async (input) => {
340
+ const graph = await knowledgeGraphManager.searchNodes(input.threadId, input.query);
343
341
  return {
344
342
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
345
343
  structuredContent: { ...graph }
@@ -348,16 +346,14 @@ server.registerTool("search_nodes", {
348
346
  // Register open_nodes tool
349
347
  server.registerTool("open_nodes", {
350
348
  title: "Open Nodes",
351
- description: "Open specific nodes in the knowledge graph by their names",
352
- inputSchema: {
353
- names: z.array(z.string()).describe("An array of entity names to retrieve")
354
- },
349
+ description: "Open specific nodes in the knowledge graph by their names (thread isolation enforced)",
350
+ inputSchema: OpenNodesInputSchema,
355
351
  outputSchema: {
356
352
  entities: z.array(EntitySchemaCompat),
357
353
  relations: z.array(RelationSchemaCompat)
358
354
  }
359
- }, async ({ names }) => {
360
- const graph = await knowledgeGraphManager.openNodes(names);
355
+ }, async (input) => {
356
+ const graph = await knowledgeGraphManager.openNodes(input.threadId, input.names);
361
357
  return {
362
358
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
363
359
  structuredContent: { ...graph }
@@ -366,21 +362,15 @@ server.registerTool("open_nodes", {
366
362
  // Register query_nodes tool for advanced filtering
367
363
  server.registerTool("query_nodes", {
368
364
  title: "Query Nodes",
369
- description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges",
370
- inputSchema: {
371
- timestampStart: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or after this time"),
372
- timestampEnd: z.string().optional().describe("ISO 8601 timestamp - filter for items created on or before this time"),
373
- confidenceMin: z.number().min(0).max(1).optional().describe("Minimum confidence value (0-1)"),
374
- confidenceMax: z.number().min(0).max(1).optional().describe("Maximum confidence value (0-1)"),
375
- importanceMin: z.number().min(0).max(1).optional().describe("Minimum importance value (0-1)"),
376
- importanceMax: z.number().min(0).max(1).optional().describe("Maximum importance value (0-1)")
377
- },
365
+ description: "Query nodes and relations in the knowledge graph with advanced filtering by timestamp, confidence, and importance ranges (thread isolation enforced)",
366
+ inputSchema: QueryNodesInputSchema,
378
367
  outputSchema: {
379
368
  entities: z.array(EntitySchemaCompat),
380
369
  relations: z.array(RelationSchemaCompat)
381
370
  }
382
- }, async (filters) => {
383
- const graph = await knowledgeGraphManager.queryNodes(filters);
371
+ }, async (input) => {
372
+ const { threadId, ...filters } = input;
373
+ const graph = await knowledgeGraphManager.queryNodes(threadId, Object.keys(filters).length > 0 ? filters : undefined);
384
374
  return {
385
375
  content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
386
376
  structuredContent: { ...graph }
@@ -389,7 +379,7 @@ server.registerTool("query_nodes", {
389
379
  // Register list_entities tool for simple entity lookup
390
380
  server.registerTool("list_entities", {
391
381
  title: "List Entities",
392
- description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery.",
382
+ description: "List entities with optional filtering by entity type and name pattern. Returns a simple list of entity names and types for quick discovery. Thread isolation enforced - only shows entities from the specified thread.",
393
383
  inputSchema: ListEntitiesInputSchema,
394
384
  outputSchema: ListEntitiesOutputSchema
395
385
  }, async (input) => {
@@ -522,8 +512,8 @@ server.registerTool("validate_memory", {
522
512
  // Register get_memory_stats tool
523
513
  server.registerTool("get_memory_stats", {
524
514
  title: "Get Memory Statistics",
525
- description: "Get comprehensive statistics about the knowledge graph including entity counts, thread activity, and confidence/importance metrics",
526
- inputSchema: {},
515
+ description: "Get comprehensive statistics about the knowledge graph for a specific thread including entity counts, activity, and confidence/importance metrics (thread isolation enforced)",
516
+ inputSchema: GetMemoryStatsInputSchema,
527
517
  outputSchema: {
528
518
  entityCount: z.number(),
529
519
  relationCount: z.number(),
@@ -536,8 +526,8 @@ server.registerTool("get_memory_stats", {
536
526
  entityCount: z.number()
537
527
  }))
538
528
  }
539
- }, async () => {
540
- const stats = await knowledgeGraphManager.getMemoryStats();
529
+ }, async (input) => {
530
+ const stats = await knowledgeGraphManager.getMemoryStats(input.threadId);
541
531
  return {
542
532
  content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
543
533
  structuredContent: stats
@@ -546,16 +536,14 @@ server.registerTool("get_memory_stats", {
546
536
  // Register get_recent_changes tool
547
537
  server.registerTool("get_recent_changes", {
548
538
  title: "Get Recent Changes",
549
- description: "Retrieve entities and relations that were created or modified since a specific timestamp",
550
- inputSchema: {
551
- since: z.string().describe("ISO 8601 timestamp - return changes since this time")
552
- },
539
+ description: "Retrieve entities and relations that were created or modified since a specific timestamp (thread isolation enforced)",
540
+ inputSchema: GetRecentChangesInputSchema,
553
541
  outputSchema: {
554
542
  entities: z.array(EntitySchemaCompat),
555
543
  relations: z.array(RelationSchemaCompat)
556
544
  }
557
- }, async ({ since }) => {
558
- const changes = await knowledgeGraphManager.getRecentChanges(since);
545
+ }, async (input) => {
546
+ const changes = await knowledgeGraphManager.getRecentChanges(input.threadId, input.since);
559
547
  return {
560
548
  content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
561
549
  structuredContent: { ...changes }
@@ -564,19 +552,15 @@ server.registerTool("get_recent_changes", {
564
552
  // Register find_relation_path tool
565
553
  server.registerTool("find_relation_path", {
566
554
  title: "Find Relationship Path",
567
- description: "Find a path of relationships connecting two entities in the knowledge graph",
568
- inputSchema: {
569
- from: z.string().describe("Starting entity name"),
570
- to: z.string().describe("Target entity name"),
571
- maxDepth: z.number().optional().default(5).describe("Maximum path depth to search (default: 5)")
572
- },
555
+ description: "Find a path of relationships connecting two entities in the knowledge graph (thread isolation enforced)",
556
+ inputSchema: FindRelationPathInputSchema,
573
557
  outputSchema: {
574
558
  found: z.boolean(),
575
559
  path: z.array(z.string()),
576
560
  relations: z.array(RelationSchemaCompat)
577
561
  }
578
- }, async ({ from, to, maxDepth }) => {
579
- const result = await knowledgeGraphManager.findRelationPath(from, to, maxDepth || 5);
562
+ }, async (input) => {
563
+ const result = await knowledgeGraphManager.findRelationPath(input.threadId, input.from, input.to, input.maxDepth || 5);
580
564
  return {
581
565
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
582
566
  structuredContent: result
@@ -585,8 +569,8 @@ server.registerTool("find_relation_path", {
585
569
  // Register detect_conflicts tool
586
570
  server.registerTool("detect_conflicts", {
587
571
  title: "Detect Conflicts",
588
- description: "Detect potentially conflicting observations within entities using pattern matching and negation detection",
589
- inputSchema: {},
572
+ description: "Detect potentially conflicting observations within entities using pattern matching and negation detection (thread isolation enforced)",
573
+ inputSchema: DetectConflictsInputSchema,
590
574
  outputSchema: {
591
575
  conflicts: z.array(z.object({
592
576
  entityName: z.string(),
@@ -597,8 +581,8 @@ server.registerTool("detect_conflicts", {
597
581
  }))
598
582
  }))
599
583
  }
600
- }, async () => {
601
- const conflicts = await knowledgeGraphManager.detectConflicts();
584
+ }, async (input) => {
585
+ const conflicts = await knowledgeGraphManager.detectConflicts(input.threadId);
602
586
  return {
603
587
  content: [{ type: "text", text: JSON.stringify({ conflicts }, null, 2) }],
604
588
  structuredContent: { conflicts }
@@ -670,13 +654,13 @@ server.registerTool("flag_for_review", {
670
654
  // Register get_flagged_entities tool
671
655
  server.registerTool("get_flagged_entities", {
672
656
  title: "Get Flagged Entities",
673
- description: "Retrieve all entities that have been flagged for human review",
674
- inputSchema: {},
657
+ description: "Retrieve all entities that have been flagged for human review (thread isolation enforced)",
658
+ inputSchema: GetFlaggedEntitiesInputSchema,
675
659
  outputSchema: {
676
660
  entities: z.array(EntitySchemaCompat)
677
661
  }
678
- }, async () => {
679
- const entities = await knowledgeGraphManager.getFlaggedEntities();
662
+ }, async (input) => {
663
+ const entities = await knowledgeGraphManager.getFlaggedEntities(input.threadId);
680
664
  return {
681
665
  content: [{ type: "text", text: JSON.stringify({ entities }, null, 2) }],
682
666
  structuredContent: { entities }
@@ -685,17 +669,14 @@ server.registerTool("get_flagged_entities", {
685
669
  // Register get_context tool
686
670
  server.registerTool("get_context", {
687
671
  title: "Get Context",
688
- description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics",
689
- inputSchema: {
690
- entityNames: z.array(z.string()).describe("Names of entities to get context for"),
691
- depth: z.number().optional().default(1).describe("How many relationship hops to include (default: 1)")
692
- },
672
+ description: "Retrieve entities and relations related to specified entities up to a certain depth, useful for understanding context around specific topics (thread isolation enforced)",
673
+ inputSchema: GetContextInputSchema,
693
674
  outputSchema: {
694
675
  entities: z.array(EntitySchemaCompat),
695
676
  relations: z.array(RelationSchemaCompat)
696
677
  }
697
- }, async ({ entityNames, depth }) => {
698
- const context = await knowledgeGraphManager.getContext(entityNames, depth || 1);
678
+ }, async (input) => {
679
+ const context = await knowledgeGraphManager.getContext(input.threadId, input.entityNames, input.depth || 1);
699
680
  return {
700
681
  content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
701
682
  structuredContent: { ...context }
@@ -738,11 +719,11 @@ server.registerTool("get_analytics", {
738
719
  // Register get_observation_history tool
739
720
  server.registerTool("get_observation_history", {
740
721
  title: "Get Observation History",
741
- description: "Retrieve the full version chain for a specific observation, showing how it evolved over time",
722
+ description: "Retrieve the full version chain for a specific observation, showing how it evolved over time (thread isolation enforced)",
742
723
  inputSchema: GetObservationHistoryInputSchema,
743
724
  outputSchema: GetObservationHistoryOutputSchema
744
725
  }, async (input) => {
745
- const result = await knowledgeGraphManager.getObservationHistory(input.entityName, input.observationId);
726
+ const result = await knowledgeGraphManager.getObservationHistory(input.threadId, input.entityName, input.observationId);
746
727
  return {
747
728
  content: [{ type: "text", text: JSON.stringify({ history: result }, null, 2) }],
748
729
  structuredContent: { history: result }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Analytics service for thread-specific metrics
3
+ */
4
+ /**
5
+ * Calculate recent changes for a thread
6
+ */
7
+ function calculateRecentChanges(threadEntities) {
8
+ return threadEntities
9
+ .map(e => ({
10
+ entityName: e.name,
11
+ entityType: e.entityType,
12
+ lastModified: e.timestamp,
13
+ changeType: 'created' // Simplified: all are 'created' for now
14
+ }))
15
+ .sort((a, b) => b.lastModified.localeCompare(a.lastModified))
16
+ .slice(0, 10);
17
+ }
18
+ /**
19
+ * Calculate top important entities for a thread
20
+ */
21
+ function calculateTopImportant(threadEntities) {
22
+ return threadEntities
23
+ .map(e => ({
24
+ entityName: e.name,
25
+ entityType: e.entityType,
26
+ importance: e.importance,
27
+ observationCount: e.observations.length
28
+ }))
29
+ .sort((a, b) => b.importance - a.importance)
30
+ .slice(0, 10);
31
+ }
32
+ /**
33
+ * Calculate most connected entities for a thread
34
+ */
35
+ function calculateMostConnected(threadEntities, entityRelationCounts) {
36
+ return Array.from(entityRelationCounts.entries())
37
+ .map(([entityName, connectedSet]) => {
38
+ const entity = threadEntities.find(e => e.name === entityName);
39
+ return {
40
+ entityName,
41
+ entityType: entity.entityType,
42
+ relationCount: connectedSet.size,
43
+ connectedTo: Array.from(connectedSet)
44
+ };
45
+ })
46
+ .sort((a, b) => b.relationCount - a.relationCount)
47
+ .slice(0, 10);
48
+ }
49
+ /**
50
+ * Calculate orphaned entities for a thread
51
+ */
52
+ function calculateOrphanedEntities(threadEntities, threadRelations, entityRelationCounts) {
53
+ const orphaned_entities = [];
54
+ const allEntityNames = new Set(threadEntities.map(e => e.name));
55
+ for (const entity of threadEntities) {
56
+ const relationCount = entityRelationCounts.get(entity.name)?.size || 0;
57
+ if (relationCount === 0) {
58
+ orphaned_entities.push({
59
+ entityName: entity.name,
60
+ entityType: entity.entityType,
61
+ reason: 'no_relations'
62
+ });
63
+ }
64
+ else {
65
+ // Check for broken relations (pointing to non-existent entities)
66
+ const entityRelations = threadRelations.filter(r => r.from === entity.name || r.to === entity.name);
67
+ const hasBrokenRelation = entityRelations.some(r => !allEntityNames.has(r.from) || !allEntityNames.has(r.to));
68
+ if (hasBrokenRelation) {
69
+ orphaned_entities.push({
70
+ entityName: entity.name,
71
+ entityType: entity.entityType,
72
+ reason: 'broken_relation'
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return orphaned_entities;
78
+ }
79
+ /**
80
+ * Get analytics for a specific thread (limited to 4 core metrics)
81
+ */
82
+ export async function getAnalytics(storage, threadId) {
83
+ const graph = await storage.loadGraph();
84
+ // Filter to thread-specific data
85
+ const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
86
+ const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
87
+ // Calculate all metrics
88
+ const recent_changes = calculateRecentChanges(threadEntities);
89
+ const top_important = calculateTopImportant(threadEntities);
90
+ // Build the relation counts map once for both most_connected and orphaned_entities
91
+ const entityRelationCounts = new Map();
92
+ for (const entity of threadEntities) {
93
+ entityRelationCounts.set(entity.name, new Set());
94
+ }
95
+ for (const relation of threadRelations) {
96
+ if (entityRelationCounts.has(relation.from)) {
97
+ entityRelationCounts.get(relation.from).add(relation.to);
98
+ }
99
+ if (entityRelationCounts.has(relation.to)) {
100
+ entityRelationCounts.get(relation.to).add(relation.from);
101
+ }
102
+ }
103
+ const most_connected = calculateMostConnected(threadEntities, entityRelationCounts);
104
+ const orphaned_entities = calculateOrphanedEntities(threadEntities, threadRelations, entityRelationCounts);
105
+ return {
106
+ recent_changes,
107
+ top_important,
108
+ most_connected,
109
+ orphaned_entities
110
+ };
111
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Conflict detection service
3
+ */
4
+ import { hasNegation, NEGATION_WORDS } from '../utils/negation-detector.js';
5
+ /**
6
+ * Detect conflicting observations within entities
7
+ * Identifies potential contradictions by checking for negation patterns
8
+ * Filtered by threadId for thread isolation
9
+ */
10
+ export async function detectConflicts(storage, threadId) {
11
+ const graph = await storage.loadGraph();
12
+ const conflicts = [];
13
+ // Only analyze entities in the specified thread
14
+ for (const entity of graph.entities.filter(e => e.agentThreadId === threadId)) {
15
+ const entityConflicts = [];
16
+ for (let i = 0; i < entity.observations.length; i++) {
17
+ for (let j = i + 1; j < entity.observations.length; j++) {
18
+ const obs1Content = entity.observations[i].content.toLowerCase();
19
+ const obs2Content = entity.observations[j].content.toLowerCase();
20
+ // Skip if observations are in the same version chain
21
+ if (entity.observations[i].supersedes === entity.observations[j].id ||
22
+ entity.observations[j].supersedes === entity.observations[i].id ||
23
+ entity.observations[i].superseded_by === entity.observations[j].id ||
24
+ entity.observations[j].superseded_by === entity.observations[i].id) {
25
+ continue;
26
+ }
27
+ // Check for negation patterns
28
+ const obs1HasNegation = hasNegation(obs1Content);
29
+ const obs2HasNegation = hasNegation(obs2Content);
30
+ // If one has negation and they share key words, might be a conflict
31
+ if (obs1HasNegation !== obs2HasNegation) {
32
+ const words1 = obs1Content.split(/\s+/).filter(w => w.length > 3);
33
+ const words2Set = new Set(obs2Content.split(/\s+/).filter(w => w.length > 3));
34
+ const commonWords = words1.filter(w => words2Set.has(w) && !NEGATION_WORDS.has(w));
35
+ if (commonWords.length >= 2) {
36
+ entityConflicts.push({
37
+ obs1: entity.observations[i].content,
38
+ obs2: entity.observations[j].content,
39
+ reason: 'Potential contradiction with negation'
40
+ });
41
+ }
42
+ }
43
+ }
44
+ }
45
+ if (entityConflicts.length > 0) {
46
+ conflicts.push({ entityName: entity.name, conflicts: entityConflicts });
47
+ }
48
+ }
49
+ return conflicts;
50
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Context builder service
3
+ */
4
+ /**
5
+ * Get context (entities related to specified entities up to a certain depth)
6
+ * Expands to include related entities up to specified depth
7
+ * Filtered by threadId for thread isolation
8
+ */
9
+ export async function getContext(storage, threadId, entityNames, depth = 1) {
10
+ const graph = await storage.loadGraph();
11
+ const contextEntityNames = new Set(entityNames);
12
+ // Expand to include related entities up to specified depth - only within this thread
13
+ for (let d = 0; d < depth; d++) {
14
+ const currentEntities = Array.from(contextEntityNames);
15
+ for (const entityName of currentEntities) {
16
+ // Find all relations involving this entity - only from this thread
17
+ const relatedRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
18
+ (r.from === entityName || r.to === entityName));
19
+ // Add related entities
20
+ relatedRelations.forEach(r => {
21
+ contextEntityNames.add(r.from);
22
+ contextEntityNames.add(r.to);
23
+ });
24
+ }
25
+ }
26
+ // Get all entities and relations in context - only from this thread
27
+ const contextEntities = graph.entities.filter(e => e.agentThreadId === threadId && contextEntityNames.has(e.name));
28
+ const contextRelations = graph.relations.filter(r => r.agentThreadId === threadId &&
29
+ contextEntityNames.has(r.from) && contextEntityNames.has(r.to));
30
+ return {
31
+ entities: contextEntities,
32
+ relations: contextRelations
33
+ };
34
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Memory statistics service
3
+ */
4
+ /**
5
+ * Get comprehensive memory statistics for a specific thread
6
+ * Filtered by threadId for thread isolation
7
+ */
8
+ export async function getMemoryStats(storage, threadId) {
9
+ const graph = await storage.loadGraph();
10
+ // Filter entities and relations by threadId
11
+ const threadEntities = graph.entities.filter(e => e.agentThreadId === threadId);
12
+ const threadRelations = graph.relations.filter(r => r.agentThreadId === threadId);
13
+ // Count entity types
14
+ const entityTypes = {};
15
+ threadEntities.forEach(e => {
16
+ entityTypes[e.entityType] = (entityTypes[e.entityType] || 0) + 1;
17
+ });
18
+ // Calculate averages
19
+ const avgConfidence = threadEntities.length > 0
20
+ ? threadEntities.reduce((sum, e) => sum + e.confidence, 0) / threadEntities.length
21
+ : 0;
22
+ const avgImportance = threadEntities.length > 0
23
+ ? threadEntities.reduce((sum, e) => sum + e.importance, 0) / threadEntities.length
24
+ : 0;
25
+ // Count unique threads in the system (across all entities in the graph)
26
+ const threads = new Set(graph.entities
27
+ .map(e => e.agentThreadId)
28
+ .filter((id) => !!id));
29
+ // Recent activity (last 7 days, grouped by day) - only for this thread
30
+ const now = new Date();
31
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
32
+ const recentEntities = threadEntities.filter(e => new Date(e.timestamp) >= sevenDaysAgo);
33
+ // Group by day
34
+ const activityByDay = {};
35
+ recentEntities.forEach(e => {
36
+ const day = e.timestamp.substring(0, 10); // YYYY-MM-DD
37
+ activityByDay[day] = (activityByDay[day] || 0) + 1;
38
+ });
39
+ const recentActivity = Object.entries(activityByDay)
40
+ .map(([timestamp, entityCount]) => ({ timestamp, entityCount }))
41
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
42
+ return {
43
+ entityCount: threadEntities.length,
44
+ relationCount: threadRelations.length,
45
+ threadCount: threads.size,
46
+ entityTypes,
47
+ avgConfidence,
48
+ avgImportance,
49
+ recentActivity
50
+ };
51
+ }
52
+ /**
53
+ * Get recent changes since a specific timestamp for a specific thread
54
+ * Filtered by threadId for thread isolation
55
+ */
56
+ export async function getRecentChanges(storage, threadId, since) {
57
+ const graph = await storage.loadGraph();
58
+ const sinceDate = new Date(since);
59
+ // Only return entities and relations from this thread that were modified since the specified time
60
+ const recentEntities = graph.entities.filter(e => e.agentThreadId === threadId && new Date(e.timestamp) >= sinceDate);
61
+ // Only include relations from this thread that are recent themselves
62
+ const recentRelations = graph.relations.filter(r => r.agentThreadId === threadId && new Date(r.timestamp) >= sinceDate);
63
+ return {
64
+ entities: recentEntities,
65
+ relations: recentRelations
66
+ };
67
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Path finding service for the knowledge graph
3
+ */
4
+ /**
5
+ * Find the shortest path between two entities in the knowledge graph
6
+ * Uses BFS algorithm with bidirectional search
7
+ * Filtered by threadId for thread isolation
8
+ */
9
+ export async function findRelationPath(storage, threadId, from, to, maxDepth = 5) {
10
+ const graph = await storage.loadGraph();
11
+ if (from === to) {
12
+ return { found: true, path: [from], relations: [] };
13
+ }
14
+ // Build indexes for efficient relation lookup - only for relations in this thread
15
+ const relationsFrom = new Map();
16
+ const relationsTo = new Map();
17
+ for (const rel of graph.relations.filter(r => r.agentThreadId === threadId)) {
18
+ if (!relationsFrom.has(rel.from)) {
19
+ relationsFrom.set(rel.from, []);
20
+ }
21
+ relationsFrom.get(rel.from).push(rel);
22
+ if (!relationsTo.has(rel.to)) {
23
+ relationsTo.set(rel.to, []);
24
+ }
25
+ relationsTo.get(rel.to).push(rel);
26
+ }
27
+ // BFS to find shortest path
28
+ const queue = [
29
+ { entity: from, path: [from], relations: [] }
30
+ ];
31
+ const visited = new Set([from]);
32
+ while (queue.length > 0) {
33
+ const current = queue.shift();
34
+ if (current.path.length > maxDepth) {
35
+ continue;
36
+ }
37
+ // Find all relations connected to current entity (both outgoing and incoming for bidirectional search)
38
+ const outgoing = relationsFrom.get(current.entity) || [];
39
+ const incoming = relationsTo.get(current.entity) || [];
40
+ // Check outgoing relations
41
+ for (const rel of outgoing) {
42
+ if (rel.to === to) {
43
+ return {
44
+ found: true,
45
+ path: [...current.path, rel.to],
46
+ relations: [...current.relations, rel]
47
+ };
48
+ }
49
+ if (!visited.has(rel.to)) {
50
+ visited.add(rel.to);
51
+ queue.push({
52
+ entity: rel.to,
53
+ path: [...current.path, rel.to],
54
+ relations: [...current.relations, rel]
55
+ });
56
+ }
57
+ }
58
+ // Check incoming relations (traverse backwards)
59
+ for (const rel of incoming) {
60
+ if (rel.from === to) {
61
+ return {
62
+ found: true,
63
+ path: [...current.path, rel.from],
64
+ relations: [...current.relations, rel]
65
+ };
66
+ }
67
+ if (!visited.has(rel.from)) {
68
+ visited.add(rel.from);
69
+ queue.push({
70
+ entity: rel.from,
71
+ path: [...current.path, rel.from],
72
+ relations: [...current.relations, rel]
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return { found: false, path: [], relations: [] };
78
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Conversation service (agent threads)
3
+ */
4
+ /**
5
+ * List all conversations (agent threads) with summary information
6
+ */
7
+ export async function listConversations(storage) {
8
+ const graph = await storage.loadGraph();
9
+ // Group data by agent thread
10
+ const threadMap = new Map();
11
+ // Collect entities by thread
12
+ for (const entity of graph.entities) {
13
+ if (!threadMap.has(entity.agentThreadId)) {
14
+ threadMap.set(entity.agentThreadId, { entities: [], relations: [], timestamps: [] });
15
+ }
16
+ const threadData = threadMap.get(entity.agentThreadId);
17
+ threadData.entities.push(entity);
18
+ threadData.timestamps.push(entity.timestamp);
19
+ }
20
+ // Collect relations by thread
21
+ for (const relation of graph.relations) {
22
+ if (!threadMap.has(relation.agentThreadId)) {
23
+ threadMap.set(relation.agentThreadId, { entities: [], relations: [], timestamps: [] });
24
+ }
25
+ const threadData = threadMap.get(relation.agentThreadId);
26
+ threadData.relations.push(relation);
27
+ threadData.timestamps.push(relation.timestamp);
28
+ }
29
+ // Build conversation summaries
30
+ const conversations = Array.from(threadMap.entries()).map(([agentThreadId, data]) => {
31
+ const timestamps = data.timestamps.sort((a, b) => a.localeCompare(b));
32
+ return {
33
+ agentThreadId,
34
+ entityCount: data.entities.length,
35
+ relationCount: data.relations.length,
36
+ firstCreated: timestamps[0] || '',
37
+ lastUpdated: timestamps[timestamps.length - 1] || ''
38
+ };
39
+ });
40
+ // Sort by last updated (most recent first)
41
+ conversations.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
42
+ return { conversations };
43
+ }