@memstack/core 0.2.0 → 0.4.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/src/client.ts DELETED
@@ -1,236 +0,0 @@
1
- import type { Memory, ProcessResult, MemStackSnapshot, HealthStatus } from "./types.js";
2
- import type { MemStackConfig, ProcessInput, MemoryStoreInput, StorageProvider } from "./interfaces.js";
3
- import { MemoryStore } from "./memory/MemoryStore.js";
4
- import { InMemoryStorage } from "./adapters/storage/memory.js";
5
- import { configError, validationError } from "./errors.js";
6
-
7
- export class MemStack {
8
- readonly memory: MemoryStore;
9
-
10
- private config: MemStackConfig;
11
- private storage: StorageProvider;
12
- private processCount = 0;
13
-
14
- constructor(config: MemStackConfig) {
15
- if (!config.llm) {
16
- throw configError("LLM provider is required");
17
- }
18
-
19
- this.config = config;
20
- this.storage = config.storage ?? new InMemoryStorage();
21
-
22
- this.memory = new MemoryStore({
23
- storage: this.storage,
24
- embedding: config.embedding,
25
- llm: config.llm,
26
- embedOnStore: config.defaults?.embedOnStore ?? true,
27
- summarizationPrompt: config.defaults?.summarizationPrompt,
28
- });
29
- }
30
-
31
- async process(input: ProcessInput): Promise<ProcessResult> {
32
- // Auto-enrichment: importance + tags via LLM when not provided
33
- let importance = input.importance;
34
- let tags = input.tags;
35
-
36
- const needImportance = importance === undefined && this.config.defaults?.autoImportance;
37
- const needTags = (!tags || tags.length === 0) && this.config.defaults?.autoTags;
38
-
39
- if (needImportance || needTags) {
40
- try {
41
- if (needImportance && needTags) {
42
- // Batch both into one LLM call
43
- const result = await this.config.llm.complete({
44
- system:
45
- "Analyze the memory and respond with a JSON object: {\"importance\": <0.0-1.0>, \"tags\": [<array of 1-5 lowercase single-word tags>]}. Return ONLY the JSON, nothing else.",
46
- user: input.content,
47
- maxTokens: 100,
48
- temperature: 0,
49
- });
50
- const parsed = this._parseEnrichmentJson(result.text);
51
- if (parsed.importance !== undefined) importance = parsed.importance;
52
- if (parsed.tags) tags = parsed.tags;
53
- } else if (needImportance) {
54
- const result = await this.config.llm.complete({
55
- system:
56
- "Rate the importance of the following memory for an AI agent on a scale of 0.0 to 1.0, where 0.0 is trivial and 1.0 is critical. Return ONLY a single float number, nothing else.",
57
- user: input.content,
58
- maxTokens: 10,
59
- temperature: 0,
60
- });
61
- const parsed = parseFloat(result.text.trim());
62
- importance = Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : 0.5;
63
- } else if (needTags) {
64
- const result = await this.config.llm.complete({
65
- system:
66
- "Extract 1-5 concise, lowercase, single-word tags from the following memory. Return ONLY a JSON array of strings, nothing else. Example: [\"combat\", \"goblin\", \"forest\"]",
67
- user: input.content,
68
- maxTokens: 80,
69
- temperature: 0,
70
- });
71
- tags = this._parseTagsJson(result.text);
72
- }
73
- } catch (err) {
74
- this.config.hooks?.onError?.(
75
- err instanceof Error ? err : new Error(String(err)),
76
- "auto-enrich"
77
- );
78
- if (importance === undefined) importance = 0.5;
79
- if (!tags) tags = [];
80
- }
81
- }
82
-
83
- const memoryInput: MemoryStoreInput = {
84
- actorId: input.actorId,
85
- content: input.content,
86
- memoryType: input.memoryType ?? "interaction",
87
- importance: importance ?? 0.5,
88
- emotionalValence: input.emotionalValence ?? 0,
89
- tags: tags ?? [],
90
- metadata: input.metadata ?? {},
91
- expiresAt: input.expiresAt,
92
- onConflict: input.onConflict,
93
- };
94
-
95
- const memory = await this.memory.store(memoryInput);
96
- this.config.hooks?.onMemoryStored?.(memory);
97
- this.processCount++;
98
-
99
- // Auto-summarize if threshold is hit
100
- let summaryCreated: Memory | undefined;
101
- const threshold = this.config.defaults?.summarizationThreshold ?? 100;
102
- if (threshold > 0) {
103
- const count = await this.memory.count({ actorId: input.actorId, memoryType: "interaction" });
104
- if (count % threshold === 0 && count > 0) {
105
- try {
106
- const result = await this.memory.summarize(
107
- {
108
- actorId: input.actorId,
109
- memoryTypes: ["interaction"],
110
- skipMostRecent: 5,
111
- targetCount: Math.min(15, count),
112
- },
113
- this.config.hooks?.onError
114
- ? (err) => this.config.hooks!.onError!(err, "summarize")
115
- : undefined
116
- );
117
- summaryCreated = result.summary;
118
- this.config.hooks?.onSummaryCreated?.(result.summary, result.deletedCount);
119
- } catch (err) {
120
- this.config.hooks?.onError?.(
121
- err instanceof Error ? err : new Error(String(err)),
122
- "summarize"
123
- );
124
- }
125
- }
126
- }
127
-
128
- // Auto-prune if configured (throttled)
129
- const pruneStrategy = this.config.defaults?.pruneStrategy;
130
- const pruneInterval = this.config.defaults?.pruneInterval ?? 100;
131
- if (pruneStrategy && this.processCount % pruneInterval === 0) {
132
- try {
133
- const pruneResult = await this.memory.prune(pruneStrategy);
134
- if (pruneResult.count > 0) {
135
- this.config.hooks?.onMemoryPruned?.(pruneResult.pruned);
136
- }
137
- } catch (err) {
138
- this.config.hooks?.onError?.(
139
- err instanceof Error ? err : new Error(String(err)),
140
- "prune"
141
- );
142
- }
143
- }
144
-
145
- return { memory, summaryCreated };
146
- }
147
-
148
- async export(): Promise<MemStackSnapshot> {
149
- const memories = await this.memory.export();
150
- return {
151
- version: 1,
152
- memories,
153
- exportedAt: new Date().toISOString(),
154
- };
155
- }
156
-
157
- async import(snapshot: MemStackSnapshot): Promise<void> {
158
- if (snapshot.version !== 1) {
159
- throw validationError(`Unsupported snapshot version: ${snapshot.version}`);
160
- }
161
- await this.memory.storeBatch(snapshot.memories);
162
- }
163
-
164
- async health(): Promise<HealthStatus> {
165
- const status: HealthStatus = { storage: false, llm: false, embedding: false };
166
-
167
- try {
168
- await this.memory.count();
169
- status.storage = true;
170
- } catch { /* storage check failed */ }
171
-
172
- try {
173
- await this.config.llm.complete({ system: "ok", user: "health", maxTokens: 1 });
174
- status.llm = true;
175
- } catch { /* llm check failed */ }
176
-
177
- if (this.config.embedding) {
178
- try {
179
- await this.config.embedding.embed(["health check"]);
180
- status.embedding = true;
181
- } catch { /* embedding check failed */ }
182
- }
183
-
184
- return status;
185
- }
186
-
187
- async close(): Promise<void> {
188
- await this.storage.close();
189
- }
190
-
191
- private _parseEnrichmentJson(
192
- text: string
193
- ): { importance?: number; tags?: string[] } {
194
- try {
195
- const json = JSON.parse(this._extractJson(text)) as Record<string, unknown>;
196
- const result: { importance?: number; tags?: string[] } = {};
197
- if (typeof json.importance === "number" && Number.isFinite(json.importance)) {
198
- result.importance = Math.max(0, Math.min(1, json.importance));
199
- }
200
- if (
201
- Array.isArray(json.tags) &&
202
- json.tags.every((t: unknown) => typeof t === "string")
203
- ) {
204
- result.tags = (json.tags as string[]).map((t) => t.toLowerCase().trim()).slice(0, 5);
205
- }
206
- return result;
207
- } catch {
208
- return {};
209
- }
210
- }
211
-
212
- private _parseTagsJson(text: string): string[] {
213
- try {
214
- const parsed = JSON.parse(this._extractJson(text)) as unknown;
215
- if (Array.isArray(parsed) && parsed.every((t: unknown) => typeof t === "string")) {
216
- return (parsed as string[]).map((t) => t.toLowerCase().trim()).slice(0, 5);
217
- }
218
- } catch {
219
- // Fallback: try comma-separated
220
- }
221
- return text
222
- .split(",")
223
- .map((t) => t.replace(/[\[\]"]/g, "").trim().toLowerCase())
224
- .filter((t) => t.length > 0)
225
- .slice(0, 5);
226
- }
227
-
228
- /** Strip markdown code fences and extract the JSON payload from LLM output. */
229
- private _extractJson(text: string): string {
230
- let cleaned = text.trim();
231
- // Remove ```json ... ``` or ``` ... ``` fences
232
- const fenceMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
233
- if (fenceMatch) cleaned = fenceMatch[1].trim();
234
- return cleaned;
235
- }
236
- }
package/src/errors.ts DELETED
@@ -1,47 +0,0 @@
1
- export type MemStackErrorCode =
2
- | "STORAGE_ERROR"
3
- | "EMBEDDING_ERROR"
4
- | "LLM_ERROR"
5
- | "VALIDATION_ERROR"
6
- | "NOT_FOUND"
7
- | "CONFIG_ERROR";
8
-
9
- export class MemStackError extends Error {
10
- code: MemStackErrorCode;
11
- retryable: boolean;
12
- details?: Record<string, unknown>;
13
-
14
- constructor(
15
- code: MemStackErrorCode,
16
- message: string,
17
- options: { retryable?: boolean; details?: Record<string, unknown> } = {}
18
- ) {
19
- super(message);
20
- this.name = "MemStackError";
21
- this.code = code;
22
- this.retryable = options.retryable ?? false;
23
- this.details = options.details;
24
- }
25
- }
26
-
27
- export function notFound(entity: string, id: string): MemStackError {
28
- return new MemStackError("NOT_FOUND", `${entity} not found: ${id}`, {
29
- retryable: false,
30
- details: { entity, id },
31
- });
32
- }
33
-
34
- export function validationError(message: string, details?: Record<string, unknown>): MemStackError {
35
- return new MemStackError("VALIDATION_ERROR", message, { details });
36
- }
37
-
38
- export function storageError(message: string, cause?: unknown): MemStackError {
39
- return new MemStackError("STORAGE_ERROR", message, {
40
- retryable: true,
41
- details: { cause: cause instanceof Error ? cause.message : String(cause) },
42
- });
43
- }
44
-
45
- export function configError(message: string): MemStackError {
46
- return new MemStackError("CONFIG_ERROR", message, { retryable: false });
47
- }
package/src/index.ts DELETED
@@ -1,52 +0,0 @@
1
- // Main client
2
- export { MemStack } from "./client.js";
3
-
4
- // Types
5
- export type {
6
- Memory,
7
- MemoryType,
8
- CompiledContext,
9
- ProcessResult,
10
- MemStackSnapshot,
11
- HealthStatus,
12
- } from "./types.js";
13
-
14
- // Interfaces
15
- export type {
16
- MemoryStoreInput,
17
- MemoryRetrieveQuery,
18
- MemoryCountFilter,
19
- ContextOptions,
20
- SummarizeOptions,
21
- PruneStrategy,
22
- LLMProvider,
23
- EmbeddingProvider,
24
- StorageProvider,
25
- MemStackConfig,
26
- ProcessInput,
27
- } from "./interfaces.js";
28
-
29
- // Errors
30
- export { MemStackError, notFound, validationError, storageError, configError } from "./errors.js";
31
- export type { MemStackErrorCode } from "./errors.js";
32
-
33
- // Adapters
34
- export { InMemoryStorage } from "./adapters/storage/memory.js";
35
- export { DiskStorage } from "./adapters/storage/disk.js";
36
- export type { DiskStorageConfig } from "./adapters/storage/disk.js";
37
- export { RedisStorage } from "./adapters/storage/redis.js";
38
- export type { RedisStorageConfig } from "./adapters/storage/redis.js";
39
- export { PostgresStorage } from "./adapters/storage/postgres.js";
40
- export type { PostgresStorageConfig } from "./adapters/storage/postgres.js";
41
- export { OpenAILLMAdapter } from "./adapters/llm/openai.js";
42
- export type { OpenAILLMConfig } from "./adapters/llm/openai.js";
43
- export { AnthropicLLMAdapter } from "./adapters/llm/anthropic.js";
44
- export type { AnthropicLLMConfig } from "./adapters/llm/anthropic.js";
45
- export { OpenAIEmbeddingAdapter } from "./adapters/embedding/openai.js";
46
- export type { OpenAIEmbeddingConfig } from "./adapters/embedding/openai.js";
47
- export { CohereEmbeddingAdapter } from "./adapters/embedding/cohere.js";
48
- export type { CohereEmbeddingConfig } from "./adapters/embedding/cohere.js";
49
- export { OllamaLLMAdapter } from "./adapters/llm/ollama.js";
50
- export type { OllamaLLMConfig } from "./adapters/llm/ollama.js";
51
- export { GroqLLMAdapter } from "./adapters/llm/groq.js";
52
- export type { GroqLLMConfig } from "./adapters/llm/groq.js";
package/src/interfaces.ts DELETED
@@ -1,154 +0,0 @@
1
- import type { Memory, ProcessResult, MemoryType } from "./types.js";
2
-
3
- // ── Memory ──
4
-
5
- export interface MemoryStoreInput {
6
- actorId: string;
7
- content: string;
8
- memoryType?: MemoryType;
9
- importance?: number;
10
- emotionalValence?: number;
11
- tags?: string[];
12
- embedding?: number[];
13
- sourceId?: string;
14
- metadata?: Record<string, unknown>;
15
- expiresAt?: Date;
16
- onConflict?: "append";
17
- /** Pre-assigned ID. When set, the storage adapter uses it instead of generating one. */
18
- id?: string;
19
- }
20
-
21
- export interface MemoryStore {
22
- store(input: MemoryStoreInput): Promise<Memory>;
23
- storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]>;
24
- get(id: string): Promise<Memory | null>;
25
- delete(id: string): Promise<void>;
26
- deleteMany(ids: string[]): Promise<number>;
27
- touch(id: string): Promise<void>;
28
- count(filter?: MemoryCountFilter): Promise<number>;
29
- retrieve(query: MemoryRetrieveQuery): Promise<Memory[]>;
30
- compileContext(options: ContextOptions): Promise<import("./types.js").CompiledContext>;
31
- summarize(options: SummarizeOptions, onError?: (err: Error) => void): Promise<{ summary: Memory; deletedCount: number }>;
32
- prune(strategy: PruneStrategy): Promise<{ pruned: string[]; count: number }>;
33
- dryRunPrune(strategy: PruneStrategy): Promise<{ wouldPrune: string[]; count: number }>;
34
- export(): Promise<Memory[]>;
35
- }
36
-
37
- export interface MemoryCountFilter {
38
- actorId?: string;
39
- memoryType?: MemoryType;
40
- minImportance?: number;
41
- }
42
-
43
- export interface MemoryRetrieveQuery {
44
- actorId?: string;
45
- query?: string;
46
- memoryTypes?: MemoryType[];
47
- tags?: string[];
48
- limit?: number;
49
- strategy?: "semantic" | "hybrid" | "recent" | "important";
50
- }
51
-
52
- export interface ContextOptions {
53
- actorId: string;
54
- maxTokens?: number;
55
- memoryTypes?: MemoryType[];
56
- }
57
-
58
- export interface SummarizeOptions {
59
- actorId?: string;
60
- olderThan?: Date;
61
- skipMostRecent?: number;
62
- targetCount?: number;
63
- memoryTypes?: MemoryType[];
64
- keepOriginals?: boolean;
65
- }
66
-
67
- export interface PruneStrategy {
68
- type: "byAge" | "byImportance" | "byCount" | "byType" | "custom";
69
- maxAge?: number;
70
- minImportance?: number;
71
- maxPerActor?: number;
72
- memoryTypes?: MemoryType[];
73
- /** Custom predicate: return true for memories that should be REMOVED. */
74
- shouldRemove?: (memory: Memory) => boolean;
75
- }
76
-
77
- // ── Adapters ──
78
-
79
- export interface LLMProvider {
80
- complete(request: {
81
- system: string;
82
- user: string;
83
- model?: string;
84
- temperature?: number;
85
- maxTokens?: number;
86
- }): Promise<{
87
- text: string;
88
- tokens: { prompt: number; completion: number; total: number };
89
- }>;
90
- /** Optional streaming variant. Returns an async iterable of text chunks. */
91
- completeStream?(request: {
92
- system: string;
93
- user: string;
94
- model?: string;
95
- temperature?: number;
96
- maxTokens?: number;
97
- }): AsyncIterable<{ text: string; tokens: { prompt: number; completion: number; total: number } }>;
98
- }
99
-
100
- export interface EmbeddingProvider {
101
- embed(texts: string[]): Promise<number[][]>;
102
- dimensions: number;
103
- }
104
-
105
- export interface StorageProvider {
106
- initialize(): Promise<void>;
107
- store(memory: MemoryStoreInput): Promise<Memory>;
108
- storeBatch(memories: MemoryStoreInput[]): Promise<Memory[]>;
109
- get(id: string): Promise<Memory | null>;
110
- delete(id: string): Promise<void>;
111
- deleteMany(ids: string[]): Promise<number>;
112
- /** Update the last-accessed timestamp for a memory without changing its content, id, or createdAt. */
113
- touch?(id: string): Promise<void>;
114
- retrieve(query: MemoryRetrieveQuery, embedding?: number[]): Promise<Memory[]>;
115
- count(filter?: MemoryCountFilter): Promise<number>;
116
- close(): Promise<void>;
117
- }
118
-
119
- // ── MemStack Client ──
120
-
121
- export interface MemStackConfig {
122
- llm: LLMProvider;
123
- embedding?: EmbeddingProvider;
124
- storage?: StorageProvider;
125
- defaults?: {
126
- summarizationThreshold?: number;
127
- embedOnStore?: boolean;
128
- pruneStrategy?: PruneStrategy;
129
- pruneInterval?: number; // Prune every N process() calls. Default: 100.
130
- autoImportance?: boolean; // LLM-based importance scoring when importance not provided
131
- autoTags?: boolean; // LLM-based tag extraction when tags not provided
132
- summarizationPrompt?: string; // Custom prompt for the summarizer
133
- };
134
- hooks?: {
135
- onMemoryStored?: (memory: Memory) => void;
136
- onMemoryPruned?: (ids: string[]) => void;
137
- onSummaryCreated?: (summary: Memory, deletedCount: number) => void;
138
- onError?: (error: Error, context: string) => void;
139
- };
140
- }
141
-
142
- export interface ProcessInput {
143
- actorId: string;
144
- content: string;
145
- memoryType?: MemoryType;
146
- importance?: number;
147
- emotionalValence?: number;
148
- tags?: string[];
149
- metadata?: Record<string, unknown>;
150
- expiresAt?: Date;
151
- onConflict?: "append";
152
- }
153
-
154
- export type { ProcessResult };
@@ -1,158 +0,0 @@
1
- import type { Memory, CompiledContext } from "../types.js";
2
- import type { ContextOptions } from "../interfaces.js";
3
-
4
- export class ContextCompiler {
5
- /**
6
- * Estimate token count from text using a blended heuristic.
7
- * Combines word-based (prose) and character-based (code/json) estimates,
8
- * taking the max to handle arbitrary content types.
9
- */
10
- private estimateTokens(text: string): number {
11
- const charEst = Math.ceil(text.length / 4);
12
- const words = text.split(/\s+/).filter((w) => w.length > 0);
13
- const wordEst = Math.ceil(words.length * 1.3);
14
- return Math.max(charEst, Math.max(1, wordEst));
15
- }
16
-
17
- /**
18
- * Truncate text to fit within a token budget, preserving whole words.
19
- * Appends "..." when content is cut. Falls back to character-based
20
- * truncation for text with few/no word boundaries.
21
- */
22
- private truncateToTokens(text: string, maxTokens: number): string {
23
- if (this.estimateTokens(text) <= maxTokens) return text;
24
-
25
- const words = text.split(/\s+/);
26
- const targetWords = Math.max(1, Math.floor(maxTokens / 1.3));
27
- let currentTokens = 0;
28
- let i = 0;
29
- for (; i < words.length && i < targetWords; i++) {
30
- currentTokens += this.estimateTokens(words[i]);
31
- if (currentTokens > maxTokens) break;
32
- }
33
- if (i >= words.length) return text;
34
- if (i > 0) return words.slice(0, i).join(" ") + "...";
35
-
36
- // Single long word (e.g., JSON): fall back to character-based cut
37
- const charLimit = maxTokens * 4;
38
- return text.slice(0, charLimit) + "...";
39
- }
40
-
41
- compile(memories: Memory[], options: ContextOptions): CompiledContext {
42
- const maxTokens = options.maxTokens ?? 2000;
43
-
44
- const relevant = memories.filter((m) => {
45
- if (options.memoryTypes && options.memoryTypes.length > 0) {
46
- return options.memoryTypes.includes(m.memoryType);
47
- }
48
- return true;
49
- });
50
-
51
- const sortedRecent = [...relevant].sort(
52
- (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
53
- );
54
-
55
- const sortedImportant = this.deduplicate(
56
- [...relevant]
57
- .filter((m) => m.importance >= 0.5)
58
- .sort((a, b) => b.importance - a.importance)
59
- );
60
-
61
- const { recentMemories, importantMemories, systemPrompt } =
62
- this.assembleTokenBudgeted(sortedRecent, sortedImportant, maxTokens);
63
-
64
- const tokenEstimate = this.estimateTokens(systemPrompt);
65
-
66
- return { systemPrompt, recentMemories, importantMemories, tokenEstimate };
67
- }
68
-
69
- private assembleTokenBudgeted(
70
- recent: Memory[],
71
- important: Memory[],
72
- maxTokens: number
73
- ): { recentMemories: Memory[]; importantMemories: Memory[]; systemPrompt: string } {
74
- const includedImportant: Memory[] = [];
75
- const includedRecent: Memory[] = [];
76
- const seen = new Set<string>();
77
- let usedTokens = 0;
78
-
79
- // Add important memories first (signal over recency)
80
- for (const m of important) {
81
- if (seen.has(m.id)) continue;
82
- const prefix = `- `;
83
- const suffix = ` (${m.memoryType}, importance: ${m.importance.toFixed(2)})\n`;
84
- const overhead = this.estimateTokens(prefix + suffix);
85
- let tokens = this.estimateTokens(m.content) + overhead;
86
- const remaining = Math.floor(maxTokens * 0.6) - usedTokens;
87
- if (remaining <= 0) break;
88
- if (tokens > remaining) {
89
- const contentBudget = Math.max(0, remaining - overhead);
90
- if (contentBudget > 0) {
91
- const truncatedContent = this.truncateToTokens(m.content, contentBudget);
92
- includedImportant.push({ ...m, content: truncatedContent });
93
- tokens = this.estimateTokens(truncatedContent) + overhead;
94
- usedTokens += tokens;
95
- seen.add(m.id);
96
- }
97
- } else {
98
- includedImportant.push(m);
99
- usedTokens += tokens;
100
- seen.add(m.id);
101
- }
102
- }
103
-
104
- // Add recent memories until budget is full
105
- for (const m of recent) {
106
- if (seen.has(m.id)) continue;
107
- const prefix = `- `;
108
- const suffix = `\n`;
109
- const overhead = this.estimateTokens(prefix + suffix);
110
- let tokens = this.estimateTokens(m.content) + overhead;
111
- const remaining = maxTokens - usedTokens;
112
- if (remaining <= 0) break;
113
- if (tokens > remaining) {
114
- const contentBudget = Math.max(0, remaining - overhead);
115
- if (contentBudget > 0) {
116
- const truncatedContent = this.truncateToTokens(m.content, contentBudget);
117
- includedRecent.push({ ...m, content: truncatedContent });
118
- tokens = this.estimateTokens(truncatedContent) + overhead;
119
- usedTokens += tokens;
120
- seen.add(m.id);
121
- }
122
- } else {
123
- includedRecent.push(m);
124
- usedTokens += tokens;
125
- seen.add(m.id);
126
- }
127
- }
128
-
129
- const parts: string[] = [];
130
- if (includedImportant.length > 0) {
131
- parts.push("## Important Memories");
132
- for (const m of includedImportant) {
133
- parts.push(`- ${m.content} (${m.memoryType}, importance: ${m.importance.toFixed(2)})`);
134
- }
135
- }
136
- if (includedRecent.length > 0) {
137
- parts.push("\n## Recent Interactions");
138
- for (const m of includedRecent) {
139
- parts.push(`- ${m.content}`);
140
- }
141
- }
142
-
143
- return { recentMemories: includedRecent, importantMemories: includedImportant, systemPrompt: parts.join("\n") };
144
- }
145
-
146
- private deduplicate(memories: Memory[]): Memory[] {
147
- const seen = new Set<string>();
148
- const result: Memory[] = [];
149
- for (const m of memories) {
150
- const key = m.content.toLowerCase().trim();
151
- if (!seen.has(key)) {
152
- seen.add(key);
153
- result.push(m);
154
- }
155
- }
156
- return result;
157
- }
158
- }