feat: add persistent memory system (Phase 2)
Implement file-based persistent memory with read/write/search tools: - MemoryStore with namespace-scoped JSON storage - memory-read, memory-write, memory-search builtin tools - Auto-extraction of facts during context compaction - Configurable via memory.enabled, memory.dir, memory.max_context_tokens
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { MemoryStore } from '../../memory/store.js';
|
||||
|
||||
interface MemoryReadArgs {
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory.read tool bound to the given MemoryStore instance.
|
||||
* Reads the full contents of a persistent memory namespace.
|
||||
*/
|
||||
export function createMemoryReadTool(store: MemoryStore): Tool {
|
||||
return {
|
||||
name: 'memory.read',
|
||||
description:
|
||||
'Read a persistent memory file by namespace. Available namespaces include "user" (user preferences and facts), "global" (cross-session knowledge), and session-specific namespaces. Returns the full contents of the memory file.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
namespace: {
|
||||
type: 'string',
|
||||
description: 'Memory namespace to read (e.g. "user", "global", "sessions/abc123")',
|
||||
},
|
||||
},
|
||||
required: ['namespace'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as MemoryReadArgs;
|
||||
|
||||
try {
|
||||
const content = store.read(args.namespace);
|
||||
|
||||
if (!content) {
|
||||
const namespaces = store.listNamespaces();
|
||||
const available = namespaces.length > 0
|
||||
? `Available namespaces: ${namespaces.join(', ')}`
|
||||
: 'No namespaces exist yet.';
|
||||
return {
|
||||
success: true,
|
||||
output: `Namespace "${args.namespace}" is empty or does not exist.\n${available}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, output: content };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { MemoryStore } from '../../memory/store.js';
|
||||
|
||||
interface MemorySearchArgs {
|
||||
query: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory.search tool bound to the given MemoryStore instance.
|
||||
* Searches across all memory namespaces for matching content.
|
||||
*/
|
||||
export function createMemorySearchTool(store: MemoryStore): Tool {
|
||||
return {
|
||||
name: 'memory.search',
|
||||
description:
|
||||
'Search across all memory files for a keyword or phrase. Returns matching lines with surrounding context from every namespace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The keyword or phrase to search for across all memory namespaces',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as MemorySearchArgs;
|
||||
|
||||
try {
|
||||
const results = store.search(args.query);
|
||||
|
||||
if (results.length === 0) {
|
||||
return { success: true, output: `No matches found for "${args.query}".` };
|
||||
}
|
||||
|
||||
// Format each result as a readable block with namespace, line number, and context
|
||||
const formatted = results.map((result) =>
|
||||
`[${result.namespace}:${result.line}] ${result.content}\n context: ${result.context}`
|
||||
).join('\n\n');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `Found ${results.length} match${results.length === 1 ? '' : 'es'} for "${args.query}":\n\n${formatted}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { MemoryStore } from '../../memory/store.js';
|
||||
|
||||
interface MemoryWriteArgs {
|
||||
namespace: string;
|
||||
content: string;
|
||||
mode: 'append' | 'replace';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory.write tool bound to the given MemoryStore instance.
|
||||
* Writes or appends content to a persistent memory namespace.
|
||||
*/
|
||||
export function createMemoryWriteTool(store: MemoryStore): Tool {
|
||||
return {
|
||||
name: 'memory.write',
|
||||
description:
|
||||
'Write to a persistent memory file. Use mode="append" to add new information without overwriting existing content, or mode="replace" to overwrite the entire namespace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
namespace: {
|
||||
type: 'string',
|
||||
description: 'Memory namespace to write to (e.g. "user", "global", "sessions/abc123")',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The content to write',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['append', 'replace'],
|
||||
description: 'Write mode: "append" to add to existing content, "replace" to overwrite',
|
||||
},
|
||||
},
|
||||
required: ['namespace', 'content', 'mode'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as MemoryWriteArgs;
|
||||
|
||||
try {
|
||||
store.write(args.namespace, args.content, args.mode);
|
||||
|
||||
const verb = args.mode === 'append' ? 'Appended to' : 'Replaced';
|
||||
return {
|
||||
success: true,
|
||||
output: `${verb} namespace "${args.namespace}" successfully.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user