Skip to content

Commit df40bf7

Browse files
feat(memory): add lightweight read modes for scalability
Add optional parameters to read_graph, search_nodes, and open_nodes tools to address context explosion in long-running deployments: - read_graph: metadataOnly, includeObservations, observationLimit, entityTypes - search_nodes: limit, includeObservations, observationLimit - open_nodes: includeObservations, observationLimit, observationOffset (pagination) Fixes #3953
1 parent d5bfe34 commit df40bf7

1 file changed

Lines changed: 86 additions & 65 deletions

File tree

src/memory/index.ts

Lines changed: 86 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ export class KnowledgeGraphManager {
9898
}
9999
}
100100

101+
private static sliceObservations(obs: string[], options?: { limit?: number; offset?: number }): string[] {
102+
if (!options || (options.offset === undefined && options.limit === undefined)) return obs;
103+
const offset = options.offset ?? 0;
104+
return options.limit !== undefined ? obs.slice(offset, offset + options.limit) : obs.slice(offset);
105+
}
106+
101107
private async saveGraph(graph: KnowledgeGraph): Promise<void> {
102108
const lines = [
103109
...graph.entities.map(e => JSON.stringify({
@@ -179,61 +185,74 @@ export class KnowledgeGraphManager {
179185
await this.saveGraph(graph);
180186
}
181187

182-
async readGraph(): Promise<KnowledgeGraph> {
183-
return this.loadGraph();
188+
async readGraph(options?: {
189+
includeObservations?: boolean;
190+
observationLimit?: number;
191+
entityTypes?: string[];
192+
metadataOnly?: boolean;
193+
}): Promise<KnowledgeGraph> {
194+
const graph = await this.loadGraph();
195+
let entities = graph.entities;
196+
if (options?.entityTypes?.length) {
197+
entities = entities.filter(e => options.entityTypes!.includes(e.entityType));
198+
}
199+
const withObservations = options?.metadataOnly || options?.includeObservations === false ? [] : undefined;
200+
const processedEntities = entities.map(e => ({
201+
name: e.name,
202+
entityType: e.entityType,
203+
observations: withObservations ?? KnowledgeGraphManager.sliceObservations(e.observations, { limit: options?.observationLimit })
204+
}));
205+
const names = new Set(processedEntities.map(e => e.name));
206+
return {
207+
entities: processedEntities,
208+
relations: graph.relations.filter(r => names.has(r.from) && names.has(r.to))
209+
};
184210
}
185211

186212
// Very basic search function
187-
async searchNodes(query: string): Promise<KnowledgeGraph> {
213+
async searchNodes(query: string, options?: {
214+
includeObservations?: boolean;
215+
limit?: number;
216+
observationLimit?: number;
217+
}): Promise<KnowledgeGraph> {
188218
const graph = await this.loadGraph();
189-
190-
// Filter entities
191-
const filteredEntities = graph.entities.filter(e =>
219+
let entities = graph.entities.filter(e =>
192220
e.name.toLowerCase().includes(query.toLowerCase()) ||
193221
e.entityType.toLowerCase().includes(query.toLowerCase()) ||
194222
e.observations.some(o => o.toLowerCase().includes(query.toLowerCase()))
195223
);
196-
197-
// Create a Set of filtered entity names for quick lookup
198-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
199-
200-
// Include relations where at least one endpoint matches the search results.
201-
// This lets callers discover connections to nodes outside the result set.
202-
const filteredRelations = graph.relations.filter(r =>
203-
filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to)
204-
);
205-
206-
const filteredGraph: KnowledgeGraph = {
207-
entities: filteredEntities,
208-
relations: filteredRelations,
224+
if (options?.limit !== undefined) entities = entities.slice(0, options.limit);
225+
const withObservations = options?.includeObservations === false ? [] : undefined;
226+
const processedEntities = entities.map(e => ({
227+
name: e.name,
228+
entityType: e.entityType,
229+
observations: withObservations ?? KnowledgeGraphManager.sliceObservations(e.observations, { limit: options?.observationLimit })
230+
}));
231+
const names = new Set(processedEntities.map(e => e.name));
232+
return {
233+
entities: processedEntities,
234+
relations: graph.relations.filter(r => names.has(r.from) || names.has(r.to))
209235
};
210-
211-
return filteredGraph;
212236
}
213237

214-
async openNodes(names: string[]): Promise<KnowledgeGraph> {
238+
async openNodes(names: string[], options?: {
239+
includeObservations?: boolean;
240+
observationLimit?: number;
241+
observationOffset?: number;
242+
}): Promise<KnowledgeGraph> {
215243
const graph = await this.loadGraph();
216-
217-
// Filter entities
218244
const filteredEntities = graph.entities.filter(e => names.includes(e.name));
219-
220-
// Create a Set of filtered entity names for quick lookup
221-
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
222-
223-
// Include relations where at least one endpoint is in the requested set.
224-
// Previously this required BOTH endpoints, which meant relations from a
225-
// requested node to an unrequested node were silently dropped — making it
226-
// impossible to discover a node's connections without reading the full graph.
227-
const filteredRelations = graph.relations.filter(r =>
228-
filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to)
229-
);
230-
231-
const filteredGraph: KnowledgeGraph = {
232-
entities: filteredEntities,
233-
relations: filteredRelations,
245+
const withObservations = options?.includeObservations === false ? [] : undefined;
246+
const processedEntities = filteredEntities.map(e => ({
247+
name: e.name,
248+
entityType: e.entityType,
249+
observations: withObservations ?? KnowledgeGraphManager.sliceObservations(e.observations, { offset: options?.observationOffset, limit: options?.observationLimit })
250+
}));
251+
const entityNames = new Set(processedEntities.map(e => e.name));
252+
return {
253+
entities: processedEntities,
254+
relations: graph.relations.filter(r => entityNames.has(r.from) || entityNames.has(r.to))
234255
};
235-
236-
return filteredGraph;
237256
}
238257
}
239258

@@ -407,19 +426,21 @@ server.registerTool(
407426
"read_graph",
408427
{
409428
title: "Read Graph",
410-
description: "Read the entire knowledge graph",
411-
inputSchema: {},
429+
description: "Read the entire knowledge graph. Optional params: metadataOnly (names/types only), includeObservations (default true), observationLimit (max per entity), entityTypes (filter by types).",
430+
inputSchema: {
431+
includeObservations: z.boolean().optional().describe("Whether to include observation content (default: true)"),
432+
observationLimit: z.number().int().positive().optional().describe("Max observations per entity"),
433+
entityTypes: z.array(z.string()).optional().describe("Filter entities by types"),
434+
metadataOnly: z.boolean().optional().describe("Return only entity names and types, no observations")
435+
},
412436
outputSchema: {
413437
entities: z.array(EntitySchema),
414438
relations: z.array(RelationSchema)
415439
}
416440
},
417-
async () => {
418-
const graph = await knowledgeGraphManager.readGraph();
419-
return {
420-
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
421-
structuredContent: { ...graph }
422-
};
441+
async (args) => {
442+
const graph = await knowledgeGraphManager.readGraph(args);
443+
return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }], structuredContent: { ...graph } };
423444
}
424445
);
425446

@@ -428,21 +449,21 @@ server.registerTool(
428449
"search_nodes",
429450
{
430451
title: "Search Nodes",
431-
description: "Search for nodes in the knowledge graph based on a query",
452+
description: "Search for nodes by query. Optional: includeObservations (default true), limit (max entities), observationLimit (max per entity).",
432453
inputSchema: {
433-
query: z.string().describe("The search query to match against entity names, types, and observation content")
454+
query: z.string().describe("Search query"),
455+
includeObservations: z.boolean().optional().describe("Include observations (default: true)"),
456+
limit: z.number().int().positive().optional().describe("Max entities to return"),
457+
observationLimit: z.number().int().positive().optional().describe("Max observations per entity")
434458
},
435459
outputSchema: {
436460
entities: z.array(EntitySchema),
437461
relations: z.array(RelationSchema)
438462
}
439463
},
440-
async ({ query }) => {
441-
const graph = await knowledgeGraphManager.searchNodes(query);
442-
return {
443-
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
444-
structuredContent: { ...graph }
445-
};
464+
async ({ query, ...options }) => {
465+
const graph = await knowledgeGraphManager.searchNodes(query, options);
466+
return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }], structuredContent: { ...graph } };
446467
}
447468
);
448469

@@ -451,21 +472,21 @@ server.registerTool(
451472
"open_nodes",
452473
{
453474
title: "Open Nodes",
454-
description: "Open specific nodes in the knowledge graph by their names",
475+
description: "Open specific nodes by name. Optional: includeObservations (default true), observationLimit, observationOffset (for pagination).",
455476
inputSchema: {
456-
names: z.array(z.string()).describe("An array of entity names to retrieve")
477+
names: z.array(z.string()).describe("Entity names to retrieve"),
478+
includeObservations: z.boolean().optional().describe("Include observations (default: true)"),
479+
observationLimit: z.number().int().positive().optional().describe("Max observations per entity"),
480+
observationOffset: z.number().int().nonnegative().optional().describe("Observations to skip (for pagination)")
457481
},
458482
outputSchema: {
459483
entities: z.array(EntitySchema),
460484
relations: z.array(RelationSchema)
461485
}
462486
},
463-
async ({ names }) => {
464-
const graph = await knowledgeGraphManager.openNodes(names);
465-
return {
466-
content: [{ type: "text" as const, text: JSON.stringify(graph, null, 2) }],
467-
structuredContent: { ...graph }
468-
};
487+
async ({ names, ...options }) => {
488+
const graph = await knowledgeGraphManager.openNodes(names, options);
489+
return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }], structuredContent: { ...graph } };
469490
}
470491
);
471492

0 commit comments

Comments
 (0)