server-memory-enhanced 3.1.0 → 3.2.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
|
@@ -299,14 +299,15 @@ server.registerTool("delete_relations", {
|
|
|
299
299
|
// Register read_graph tool
|
|
300
300
|
server.registerTool("read_graph", {
|
|
301
301
|
title: "Read Graph",
|
|
302
|
-
description: "Read the knowledge graph for a specific thread (thread isolation enforced)",
|
|
302
|
+
description: "Read the knowledge graph for a specific thread (thread isolation enforced). Supports filtering by minimum importance threshold.",
|
|
303
303
|
inputSchema: ReadGraphInputSchema,
|
|
304
304
|
outputSchema: {
|
|
305
305
|
entities: z.array(EntitySchemaCompat),
|
|
306
306
|
relations: z.array(RelationSchemaCompat)
|
|
307
307
|
}
|
|
308
308
|
}, async (input) => {
|
|
309
|
-
const
|
|
309
|
+
const { threadId, minImportance } = input;
|
|
310
|
+
const graph = await knowledgeGraphManager.readGraph(threadId, minImportance);
|
|
310
311
|
return {
|
|
311
312
|
content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
|
|
312
313
|
structuredContent: { ...graph }
|
|
@@ -74,8 +74,12 @@ export class KnowledgeGraphManager {
|
|
|
74
74
|
return ObservationOps.updateObservation(this.storage, params);
|
|
75
75
|
}
|
|
76
76
|
// Graph Reading Operations
|
|
77
|
-
async readGraph(threadId) {
|
|
77
|
+
async readGraph(threadId, minImportance) {
|
|
78
78
|
await this.ensureInitialized();
|
|
79
|
+
// Pass minImportance if provided, otherwise let readGraph use its default
|
|
80
|
+
if (minImportance !== undefined) {
|
|
81
|
+
return GraphReader.readGraph(this.storage, threadId, minImportance);
|
|
82
|
+
}
|
|
79
83
|
return GraphReader.readGraph(this.storage, threadId);
|
|
80
84
|
}
|
|
81
85
|
// Search Operations
|
|
@@ -1,18 +1,152 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Graph reading operations
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Default threshold for marking items as ARCHIVED
|
|
6
|
+
* Items with importance below this but >= minImportance get ARCHIVED status
|
|
7
|
+
*/
|
|
8
|
+
export const ARCHIVED_THRESHOLD = 0.1;
|
|
9
|
+
/**
|
|
10
|
+
* Check if an observation has a status field
|
|
11
|
+
*/
|
|
12
|
+
function hasStatus(obs) {
|
|
13
|
+
return obs.status !== undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if an entity or any of its observations has a status field
|
|
17
|
+
*/
|
|
18
|
+
function entityHasStatus(entity) {
|
|
19
|
+
return entity.status !== undefined || entity.observations.some(hasStatus);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if a relation has a status field
|
|
23
|
+
*/
|
|
24
|
+
function relationHasStatus(relation) {
|
|
25
|
+
return relation.status !== undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Strip status from an observation (used to clean persisted status values)
|
|
29
|
+
*/
|
|
30
|
+
function stripObservationStatus(obs) {
|
|
31
|
+
if (!hasStatus(obs))
|
|
32
|
+
return obs;
|
|
33
|
+
const { status: _oldStatus, ...obsWithoutStatus } = obs;
|
|
34
|
+
return obsWithoutStatus;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Strip status from an entity and its observations (used to clean persisted status values)
|
|
38
|
+
* Optimized to avoid unnecessary copies when only entity status needs stripping
|
|
39
|
+
*/
|
|
40
|
+
function stripEntityStatus(entity) {
|
|
41
|
+
if (!entityHasStatus(entity))
|
|
42
|
+
return entity;
|
|
43
|
+
const entityHasOwnStatus = entity.status !== undefined;
|
|
44
|
+
const observationsHaveStatus = entity.observations.some(hasStatus);
|
|
45
|
+
// If only observations have status, keep entity as-is and just map observations
|
|
46
|
+
if (!entityHasOwnStatus && observationsHaveStatus) {
|
|
47
|
+
return {
|
|
48
|
+
...entity,
|
|
49
|
+
observations: entity.observations.map(stripObservationStatus),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// If only entity has status, strip it but keep observations as-is
|
|
53
|
+
if (entityHasOwnStatus && !observationsHaveStatus) {
|
|
54
|
+
const { status: _oldStatus, ...entityWithoutStatus } = entity;
|
|
55
|
+
return entityWithoutStatus;
|
|
56
|
+
}
|
|
57
|
+
// Both entity and observations have status
|
|
58
|
+
const { status: _oldStatus, ...entityWithoutStatus } = entity;
|
|
59
|
+
return {
|
|
60
|
+
...entityWithoutStatus,
|
|
61
|
+
observations: entity.observations.map(stripObservationStatus),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Strip status from a relation (used to clean persisted status values)
|
|
66
|
+
*/
|
|
67
|
+
function stripRelationStatus(relation) {
|
|
68
|
+
if (!relationHasStatus(relation))
|
|
69
|
+
return relation;
|
|
70
|
+
const { status: _oldStatus, ...relationWithoutStatus } = relation;
|
|
71
|
+
return relationWithoutStatus;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Strip status from all entities and relations in a graph (used to clean persisted status values)
|
|
75
|
+
* Optimized to avoid unnecessary deep copies when no status fields exist
|
|
76
|
+
*/
|
|
77
|
+
export function stripGraphStatus(graph) {
|
|
78
|
+
// Check if any items have status fields - if not, return as-is
|
|
79
|
+
const hasAnyStatus = graph.entities.some(entityHasStatus) || graph.relations.some(relationHasStatus);
|
|
80
|
+
if (!hasAnyStatus) {
|
|
81
|
+
return graph;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
entities: graph.entities.map(stripEntityStatus),
|
|
85
|
+
relations: graph.relations.map(stripRelationStatus),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
4
88
|
/**
|
|
5
89
|
* Read the knowledge graph filtered by threadId for thread isolation
|
|
90
|
+
* and optionally filtered by minimum importance threshold
|
|
6
91
|
*/
|
|
7
|
-
export async function readGraph(storage, threadId) {
|
|
92
|
+
export async function readGraph(storage, threadId, minImportance = ARCHIVED_THRESHOLD) {
|
|
8
93
|
const graph = await storage.loadGraph();
|
|
9
|
-
// Filter entities by threadId
|
|
10
|
-
const filteredEntities = graph.entities
|
|
94
|
+
// Filter entities by threadId and importance
|
|
95
|
+
const filteredEntities = graph.entities
|
|
96
|
+
.filter(e => e.agentThreadId === threadId)
|
|
97
|
+
.filter(e => e.importance >= minImportance)
|
|
98
|
+
.map(entity => {
|
|
99
|
+
// Add ARCHIVED status if importance is less than ARCHIVED_THRESHOLD but >= minImportance.
|
|
100
|
+
// Otherwise, explicitly clear status so pre-existing values don't leak through.
|
|
101
|
+
const isArchived = entity.importance < ARCHIVED_THRESHOLD && entity.importance >= minImportance;
|
|
102
|
+
const { status: _oldStatus, ...entityWithoutStatus } = entity;
|
|
103
|
+
const entityWithStatus = {
|
|
104
|
+
...entityWithoutStatus,
|
|
105
|
+
...(isArchived ? { status: 'ARCHIVED' } : {}),
|
|
106
|
+
};
|
|
107
|
+
// Process observations: filter by importance and add ARCHIVED status
|
|
108
|
+
// Defensive: handle potential legacy string observations
|
|
109
|
+
entityWithStatus.observations = entity.observations
|
|
110
|
+
.filter(obs => {
|
|
111
|
+
// Keep legacy string observations (defensive against legacy data)
|
|
112
|
+
if (typeof obs !== 'object' || obs === null)
|
|
113
|
+
return true;
|
|
114
|
+
// Use observation importance if set, otherwise inherit from entity
|
|
115
|
+
const obsImportance = obs.importance !== undefined ? obs.importance : entity.importance;
|
|
116
|
+
return obsImportance >= minImportance;
|
|
117
|
+
})
|
|
118
|
+
.map(obs => {
|
|
119
|
+
// Pass through legacy string observations unchanged
|
|
120
|
+
if (typeof obs !== 'object' || obs === null)
|
|
121
|
+
return obs;
|
|
122
|
+
const obsImportance = obs.importance !== undefined ? obs.importance : entity.importance;
|
|
123
|
+
const isObsArchived = obsImportance < ARCHIVED_THRESHOLD && obsImportance >= minImportance;
|
|
124
|
+
const { status: _oldObsStatus, ...obsWithoutStatus } = obs;
|
|
125
|
+
return {
|
|
126
|
+
...obsWithoutStatus,
|
|
127
|
+
...(isObsArchived ? { status: 'ARCHIVED' } : {}),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
return entityWithStatus;
|
|
131
|
+
});
|
|
11
132
|
// Create a Set of filtered entity names for quick lookup
|
|
12
133
|
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
|
|
13
|
-
// Filter relations to only include those between filtered entities
|
|
14
|
-
const filteredRelations = graph.relations
|
|
15
|
-
|
|
134
|
+
// Filter relations to only include those between filtered entities, from the same thread, and by importance
|
|
135
|
+
const filteredRelations = graph.relations
|
|
136
|
+
.filter(r => r.agentThreadId === threadId &&
|
|
137
|
+
filteredEntityNames.has(r.from) &&
|
|
138
|
+
filteredEntityNames.has(r.to) &&
|
|
139
|
+
r.importance >= minImportance)
|
|
140
|
+
.map(relation => {
|
|
141
|
+
// Add ARCHIVED status if importance is less than ARCHIVED_THRESHOLD but >= minImportance.
|
|
142
|
+
// Otherwise, explicitly clear status so pre-existing values don't leak through.
|
|
143
|
+
const isArchived = relation.importance < ARCHIVED_THRESHOLD && relation.importance >= minImportance;
|
|
144
|
+
const { status: _oldStatus, ...relationWithoutStatus } = relation;
|
|
145
|
+
return {
|
|
146
|
+
...relationWithoutStatus,
|
|
147
|
+
...(isArchived ? { status: 'ARCHIVED' } : {}),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
16
150
|
return {
|
|
17
151
|
entities: filteredEntities,
|
|
18
152
|
relations: filteredRelations
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search and filter operations for the knowledge graph
|
|
3
3
|
*/
|
|
4
|
+
import { stripGraphStatus } from './graph-reader.js';
|
|
4
5
|
/**
|
|
5
6
|
* Search for nodes in the knowledge graph by query string
|
|
6
7
|
* Searches entity names, types, and observation content
|
|
@@ -22,7 +23,8 @@ export async function searchNodes(storage, threadId, query) {
|
|
|
22
23
|
entities: filteredEntities,
|
|
23
24
|
relations: filteredRelations,
|
|
24
25
|
};
|
|
25
|
-
|
|
26
|
+
// Strip any persisted status values to prevent leaking stale data
|
|
27
|
+
return stripGraphStatus(filteredGraph);
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Open specific nodes by name
|
|
@@ -42,7 +44,8 @@ export async function openNodes(storage, threadId, names) {
|
|
|
42
44
|
entities: filteredEntities,
|
|
43
45
|
relations: filteredRelations,
|
|
44
46
|
};
|
|
45
|
-
|
|
47
|
+
// Strip any persisted status values to prevent leaking stale data
|
|
48
|
+
return stripGraphStatus(filteredGraph);
|
|
46
49
|
}
|
|
47
50
|
/**
|
|
48
51
|
* Query nodes with advanced filters
|
|
@@ -106,8 +109,10 @@ export async function queryNodes(storage, threadId, filters) {
|
|
|
106
109
|
return false;
|
|
107
110
|
return true;
|
|
108
111
|
});
|
|
109
|
-
|
|
112
|
+
const filteredGraph = {
|
|
110
113
|
entities: filteredEntities,
|
|
111
114
|
relations: filteredRelations,
|
|
112
115
|
};
|
|
116
|
+
// Strip any persisted status values to prevent leaking stale data
|
|
117
|
+
return stripGraphStatus(filteredGraph);
|
|
113
118
|
}
|
package/dist/lib/schemas.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Zod schema definitions for MCP tools
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { ARCHIVED_THRESHOLD } from "./queries/graph-reader.js";
|
|
5
6
|
// Schema for Observation with versioning support
|
|
6
7
|
export const ObservationSchema = z.object({
|
|
7
8
|
id: z.string().describe("Unique observation ID"),
|
|
@@ -12,8 +13,11 @@ export const ObservationSchema = z.object({
|
|
|
12
13
|
superseded_by: z.string().optional().describe("ID of observation that supersedes this one"),
|
|
13
14
|
agentThreadId: z.string().describe("Thread that created this observation"),
|
|
14
15
|
confidence: z.number().min(0).max(1).optional().describe("Confidence in accuracy (0-1, optional, inherits from entity if not set)"),
|
|
15
|
-
importance: z.number().min(0).max(1).optional().describe("Importance for memory integrity (0-1, optional, inherits from entity if not set)")
|
|
16
|
+
importance: z.number().min(0).max(1).optional().describe("Importance for memory integrity (0-1, optional, inherits from entity if not set)"),
|
|
17
|
+
status: z.literal('ARCHIVED').optional().describe("Status indicator - set to 'ARCHIVED' for low-importance items")
|
|
16
18
|
});
|
|
19
|
+
// Input schema for observations (excludes status field which is computed)
|
|
20
|
+
export const ObservationInputSchema = ObservationSchema.omit({ status: true }).strict();
|
|
17
21
|
// Schema for existing tools
|
|
18
22
|
export const EntitySchema = z.object({
|
|
19
23
|
name: z.string().describe("Unique identifier for the entity"),
|
|
@@ -22,8 +26,13 @@ export const EntitySchema = z.object({
|
|
|
22
26
|
agentThreadId: z.string().describe("Agent thread that created/modified this entity"),
|
|
23
27
|
timestamp: z.string().describe("ISO 8601 timestamp of creation/modification"),
|
|
24
28
|
confidence: z.number().min(0).max(1).describe("Confidence in the accuracy of this entity (0-1)"),
|
|
25
|
-
importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
|
|
29
|
+
importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)"),
|
|
30
|
+
status: z.literal('ARCHIVED').optional().describe("Status indicator - set to 'ARCHIVED' for low-importance items")
|
|
26
31
|
});
|
|
32
|
+
// Input schema for entities (excludes status field which is computed, uses input observations)
|
|
33
|
+
export const EntityInputSchema = EntitySchema.omit({ status: true, observations: true }).extend({
|
|
34
|
+
observations: z.array(ObservationInputSchema).describe("Versioned observations about this entity")
|
|
35
|
+
}).strict();
|
|
27
36
|
export const RelationSchema = z.object({
|
|
28
37
|
from: z.string().describe("Source entity name"),
|
|
29
38
|
to: z.string().describe("Target entity name"),
|
|
@@ -31,8 +40,11 @@ export const RelationSchema = z.object({
|
|
|
31
40
|
agentThreadId: z.string().describe("Agent thread that created/modified this relation"),
|
|
32
41
|
timestamp: z.string().describe("ISO 8601 timestamp of creation/modification"),
|
|
33
42
|
confidence: z.number().min(0).max(1).describe("Confidence in the accuracy of this relation (0-1)"),
|
|
34
|
-
importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)")
|
|
43
|
+
importance: z.number().min(0).max(1).describe("Importance for memory integrity if lost: 0 (not important) to 1 (critical)"),
|
|
44
|
+
status: z.literal('ARCHIVED').optional().describe("Status indicator - set to 'ARCHIVED' for low-importance items")
|
|
35
45
|
});
|
|
46
|
+
// Input schema for relations (excludes status field which is computed)
|
|
47
|
+
export const RelationInputSchema = RelationSchema.omit({ status: true }).strict();
|
|
36
48
|
// Schema for save_memory tool (Section 1 of spec)
|
|
37
49
|
export const SaveMemoryRelationSchema = z.object({
|
|
38
50
|
targetEntity: z.string().describe("Name of entity to connect to (must exist in this request)"),
|
|
@@ -147,7 +159,8 @@ export const UpdateObservationOutputSchema = z.object({
|
|
|
147
159
|
});
|
|
148
160
|
// Schema for read_graph tool
|
|
149
161
|
export const ReadGraphInputSchema = z.object({
|
|
150
|
-
threadId: z.string().min(1).describe("Thread ID for this conversation/project")
|
|
162
|
+
threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
|
|
163
|
+
minImportance: z.number().min(0).max(1).optional().default(ARCHIVED_THRESHOLD).describe(`Minimum importance threshold (0-1). Items with importance below this value are excluded. Items with importance between minImportance and ${ARCHIVED_THRESHOLD} are marked as ARCHIVED. Default: ${ARCHIVED_THRESHOLD}`)
|
|
151
164
|
});
|
|
152
165
|
// Schema for search_nodes tool
|
|
153
166
|
export const SearchNodesInputSchema = z.object({
|
|
@@ -202,7 +215,7 @@ export const GetContextInputSchema = z.object({
|
|
|
202
215
|
// Schema for create_entities tool
|
|
203
216
|
export const CreateEntitiesInputSchema = z.object({
|
|
204
217
|
threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
|
|
205
|
-
entities: z.array(
|
|
218
|
+
entities: z.array(EntityInputSchema).describe("Array of entities to create")
|
|
206
219
|
}).superRefine((data, ctx) => {
|
|
207
220
|
const { threadId, entities } = data;
|
|
208
221
|
entities.forEach((entity, index) => {
|
|
@@ -218,7 +231,7 @@ export const CreateEntitiesInputSchema = z.object({
|
|
|
218
231
|
// Schema for create_relations tool
|
|
219
232
|
export const CreateRelationsInputSchema = z.object({
|
|
220
233
|
threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
|
|
221
|
-
relations: z.array(
|
|
234
|
+
relations: z.array(RelationInputSchema).describe("Array of relations to create")
|
|
222
235
|
}).superRefine((data, ctx) => {
|
|
223
236
|
const { threadId, relations } = data;
|
|
224
237
|
relations.forEach((relation, index) => {
|
|
@@ -269,7 +282,7 @@ export const DeleteObservationsInputSchema = z.object({
|
|
|
269
282
|
// Schema for delete_relations tool
|
|
270
283
|
export const DeleteRelationsInputSchema = z.object({
|
|
271
284
|
threadId: z.string().min(1).describe("Thread ID for this conversation/project"),
|
|
272
|
-
relations: z.array(
|
|
285
|
+
relations: z.array(RelationInputSchema).describe("An array of relations to delete")
|
|
273
286
|
});
|
|
274
287
|
// Schema for prune_memory tool
|
|
275
288
|
export const PruneMemoryInputSchema = z.object({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "server-memory-enhanced",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Persistent memory for long conversations - MCP server for knowledge graph with atomic facts, cross-chat memory sharing, and multi-agent delegation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"mcpName": "io.github.modelcontextprotocol/server-memory-enhanced",
|