@memstack/core 0.2.0 → 0.5.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/README.md +321 -69
- package/dist/index.cjs +2480 -269
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +537 -27
- package/dist/index.d.ts +537 -27
- package/dist/index.js +2470 -267
- package/dist/index.js.map +1 -1
- package/package.json +27 -24
- package/src/adapters/embedding/cohere.ts +0 -77
- package/src/adapters/embedding/openai.ts +0 -65
- package/src/adapters/llm/anthropic.ts +0 -69
- package/src/adapters/llm/groq.ts +0 -185
- package/src/adapters/llm/ollama.ts +0 -85
- package/src/adapters/llm/openai.ts +0 -178
- package/src/adapters/storage/disk.ts +0 -334
- package/src/adapters/storage/memory.ts +0 -147
- package/src/adapters/storage/postgres.ts +0 -310
- package/src/adapters/storage/redis.ts +0 -274
- package/src/client.ts +0 -236
- package/src/errors.ts +0 -47
- package/src/index.ts +0 -52
- package/src/interfaces.ts +0 -154
- package/src/memory/ContextCompiler.ts +0 -158
- package/src/memory/MemoryStore.ts +0 -266
- package/src/memory/Pruner.ts +0 -66
- package/src/memory/Summarizer.ts +0 -46
- package/src/types.ts +0 -50
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import type { Memory, MemoryType } from "../../types.js";
|
|
2
|
-
import type { StorageProvider, MemoryStoreInput, MemoryRetrieveQuery, MemoryCountFilter } from "../../interfaces.js";
|
|
3
|
-
import { storageError, notFound } from "../../errors.js";
|
|
4
|
-
|
|
5
|
-
// User provides their own pg Pool instance
|
|
6
|
-
type PgPool = {
|
|
7
|
-
query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
interface MemoryRecord extends Memory {
|
|
11
|
-
_touchedAt: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface PgMemoryRow {
|
|
15
|
-
id: string;
|
|
16
|
-
actor_id: string;
|
|
17
|
-
memory_type: string;
|
|
18
|
-
content: string;
|
|
19
|
-
importance: number;
|
|
20
|
-
emotional_valence: number;
|
|
21
|
-
tags: string[];
|
|
22
|
-
embedding: number[] | null;
|
|
23
|
-
source_id: string | null;
|
|
24
|
-
metadata: Record<string, unknown>;
|
|
25
|
-
expires_at: string | null;
|
|
26
|
-
created_at: string;
|
|
27
|
-
touched_at: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface PostgresStorageConfig {
|
|
31
|
-
/** An active pg Pool instance */
|
|
32
|
-
pool: PgPool;
|
|
33
|
-
/** Table name. Default: "memstack_memories" */
|
|
34
|
-
tableName?: string;
|
|
35
|
-
/** Automatically run CREATE TABLE IF NOT EXISTS on init. Default: true */
|
|
36
|
-
autoMigrate?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class PostgresStorage implements StorageProvider {
|
|
40
|
-
private pool: PgPool;
|
|
41
|
-
private table: string;
|
|
42
|
-
private autoMigrate: boolean;
|
|
43
|
-
|
|
44
|
-
constructor(config: PostgresStorageConfig) {
|
|
45
|
-
this.pool = config.pool;
|
|
46
|
-
this.table = config.tableName ?? "memstack_memories";
|
|
47
|
-
if (!/^[a-zA-Z0-9_]+$/.test(this.table)) {
|
|
48
|
-
throw new Error(`Invalid table name: "${this.table}". Use only alphanumeric characters and underscores.`);
|
|
49
|
-
}
|
|
50
|
-
this.autoMigrate = config.autoMigrate ?? true;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async initialize(): Promise<void> {
|
|
54
|
-
if (!this.autoMigrate) return;
|
|
55
|
-
await this.pool.query(`
|
|
56
|
-
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
actor_id TEXT NOT NULL,
|
|
59
|
-
memory_type TEXT NOT NULL DEFAULT 'interaction',
|
|
60
|
-
content TEXT NOT NULL,
|
|
61
|
-
importance REAL NOT NULL DEFAULT 0.5,
|
|
62
|
-
emotional_valence REAL NOT NULL DEFAULT 0,
|
|
63
|
-
tags JSONB NOT NULL DEFAULT '[]',
|
|
64
|
-
embedding vector(3072),
|
|
65
|
-
source_id TEXT,
|
|
66
|
-
metadata JSONB NOT NULL DEFAULT '{}',
|
|
67
|
-
expires_at TIMESTAMPTZ,
|
|
68
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
69
|
-
touched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
70
|
-
)
|
|
71
|
-
`);
|
|
72
|
-
// Indexes
|
|
73
|
-
try {
|
|
74
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_actor ON ${this.table} (actor_id)`);
|
|
75
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_type ON ${this.table} (memory_type)`);
|
|
76
|
-
} catch {
|
|
77
|
-
// Index creation may fail; non-critical
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
generateId(): string {
|
|
82
|
-
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async store(input: MemoryStoreInput): Promise<Memory> {
|
|
86
|
-
const now = new Date().toISOString();
|
|
87
|
-
const id = input.id ?? this.generateId();
|
|
88
|
-
|
|
89
|
-
await this.pool.query(
|
|
90
|
-
`INSERT INTO ${this.table} (id, actor_id, memory_type, content, importance, emotional_valence, tags, embedding, source_id, metadata, expires_at, created_at, touched_at)
|
|
91
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::float8[]::vector, $9, $10, $11, $12, $13)
|
|
92
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
93
|
-
content = EXCLUDED.content,
|
|
94
|
-
importance = EXCLUDED.importance,
|
|
95
|
-
emotional_valence = EXCLUDED.emotional_valence,
|
|
96
|
-
tags = EXCLUDED.tags,
|
|
97
|
-
embedding = EXCLUDED.embedding,
|
|
98
|
-
source_id = EXCLUDED.source_id,
|
|
99
|
-
metadata = EXCLUDED.metadata,
|
|
100
|
-
expires_at = EXCLUDED.expires_at,
|
|
101
|
-
touched_at = EXCLUDED.touched_at`,
|
|
102
|
-
[
|
|
103
|
-
id,
|
|
104
|
-
input.actorId,
|
|
105
|
-
input.memoryType ?? "interaction",
|
|
106
|
-
input.content,
|
|
107
|
-
input.importance ?? 0.5,
|
|
108
|
-
input.emotionalValence ?? 0,
|
|
109
|
-
JSON.stringify(input.tags ?? []),
|
|
110
|
-
input.embedding ? `{${input.embedding.join(",")}}` : null,
|
|
111
|
-
input.sourceId ?? null,
|
|
112
|
-
JSON.stringify(input.metadata ?? {}),
|
|
113
|
-
input.expiresAt?.toISOString() ?? null,
|
|
114
|
-
now,
|
|
115
|
-
now,
|
|
116
|
-
]
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
id,
|
|
121
|
-
actorId: input.actorId,
|
|
122
|
-
memoryType: (input.memoryType ?? "interaction") as MemoryType,
|
|
123
|
-
content: input.content,
|
|
124
|
-
importance: input.importance ?? 0.5,
|
|
125
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
126
|
-
tags: input.tags ?? [],
|
|
127
|
-
embedding: input.embedding,
|
|
128
|
-
sourceId: input.sourceId,
|
|
129
|
-
metadata: input.metadata ?? {},
|
|
130
|
-
expiresAt: input.expiresAt,
|
|
131
|
-
createdAt: new Date(now),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> {
|
|
136
|
-
const results: Memory[] = [];
|
|
137
|
-
for (const input of inputs) {
|
|
138
|
-
results.push(await this.store(input));
|
|
139
|
-
}
|
|
140
|
-
return results;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async get(id: string): Promise<Memory | null> {
|
|
144
|
-
const { rows } = await this.pool.query(
|
|
145
|
-
`SELECT * FROM ${this.table} WHERE id = $1 AND (expires_at IS NULL OR expires_at > NOW())`,
|
|
146
|
-
[id]
|
|
147
|
-
);
|
|
148
|
-
if (rows.length === 0) return null;
|
|
149
|
-
return this._rowToMemory(rows[0] as PgMemoryRow);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async delete(id: string): Promise<void> {
|
|
153
|
-
const { rows } = await this.pool.query(`SELECT id FROM ${this.table} WHERE id = $1`, [id]);
|
|
154
|
-
if (rows.length === 0) throw notFound("Memory", id);
|
|
155
|
-
await this.pool.query(`DELETE FROM ${this.table} WHERE id = $1`, [id]);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async deleteMany(ids: string[]): Promise<number> {
|
|
159
|
-
const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
|
|
160
|
-
const { rows } = await this.pool.query(
|
|
161
|
-
`DELETE FROM ${this.table} WHERE id IN (${placeholders}) RETURNING id`,
|
|
162
|
-
ids
|
|
163
|
-
);
|
|
164
|
-
return rows.length;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async retrieve(query: MemoryRetrieveQuery, embedding?: number[]): Promise<Memory[]> {
|
|
168
|
-
const conditions: string[] = ["(expires_at IS NULL OR expires_at > NOW())"];
|
|
169
|
-
const params: unknown[] = [];
|
|
170
|
-
let paramIdx = 1;
|
|
171
|
-
|
|
172
|
-
if (query.actorId) {
|
|
173
|
-
conditions.push(`actor_id = $${paramIdx++}`);
|
|
174
|
-
params.push(query.actorId);
|
|
175
|
-
}
|
|
176
|
-
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
177
|
-
conditions.push(`memory_type = ANY($${paramIdx++})`);
|
|
178
|
-
params.push(query.memoryTypes);
|
|
179
|
-
}
|
|
180
|
-
if (query.tags && query.tags.length > 0) {
|
|
181
|
-
conditions.push(`tags ?| $${paramIdx++}`);
|
|
182
|
-
params.push(query.tags);
|
|
183
|
-
}
|
|
184
|
-
if (query.query) {
|
|
185
|
-
conditions.push(`content ILIKE $${paramIdx++}`);
|
|
186
|
-
params.push(`%${query.query}%`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
190
|
-
|
|
191
|
-
let orderBy: string;
|
|
192
|
-
switch (query.strategy) {
|
|
193
|
-
case "recent":
|
|
194
|
-
orderBy = "touched_at DESC";
|
|
195
|
-
break;
|
|
196
|
-
case "important":
|
|
197
|
-
orderBy = "importance DESC";
|
|
198
|
-
break;
|
|
199
|
-
case "hybrid":
|
|
200
|
-
case "semantic":
|
|
201
|
-
default:
|
|
202
|
-
if (embedding && query.query) {
|
|
203
|
-
// Semantic: order by vector distance
|
|
204
|
-
return this._semanticRetrieve(embedding, query, params, where);
|
|
205
|
-
}
|
|
206
|
-
orderBy = "importance DESC, touched_at DESC";
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const limit = query.limit ?? 10;
|
|
211
|
-
const sql = `SELECT * FROM ${this.table} ${where} ORDER BY ${orderBy} LIMIT $${paramIdx}`;
|
|
212
|
-
params.push(limit);
|
|
213
|
-
|
|
214
|
-
const { rows } = await this.pool.query(sql, params);
|
|
215
|
-
|
|
216
|
-
// Touch records
|
|
217
|
-
for (const row of rows.slice(0, limit)) {
|
|
218
|
-
const r = row as PgMemoryRow;
|
|
219
|
-
await this.pool.query(
|
|
220
|
-
`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`,
|
|
221
|
-
[r.id]
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return (rows as PgMemoryRow[]).map((r) => this._rowToMemory(r));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async count(filter?: MemoryCountFilter): Promise<number> {
|
|
229
|
-
const conditions: string[] = ["(expires_at IS NULL OR expires_at > NOW())"];
|
|
230
|
-
const params: unknown[] = [];
|
|
231
|
-
let paramIdx = 1;
|
|
232
|
-
|
|
233
|
-
if (filter?.actorId) {
|
|
234
|
-
conditions.push(`actor_id = $${paramIdx++}`);
|
|
235
|
-
params.push(filter.actorId);
|
|
236
|
-
}
|
|
237
|
-
if (filter?.memoryType) {
|
|
238
|
-
conditions.push(`memory_type = $${paramIdx++}`);
|
|
239
|
-
params.push(filter.memoryType);
|
|
240
|
-
}
|
|
241
|
-
if (filter?.minImportance !== undefined) {
|
|
242
|
-
conditions.push(`importance >= $${paramIdx++}`);
|
|
243
|
-
params.push(filter.minImportance);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const where = conditions.join(" AND ");
|
|
247
|
-
const { rows } = await this.pool.query(
|
|
248
|
-
`SELECT COUNT(*) as count FROM ${this.table} WHERE ${where}`,
|
|
249
|
-
params
|
|
250
|
-
);
|
|
251
|
-
return Number((rows[0] as { count: string }).count);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async close(): Promise<void> {
|
|
255
|
-
// User manages pool lifecycle
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── Internal ──
|
|
259
|
-
|
|
260
|
-
private async _semanticRetrieve(
|
|
261
|
-
embedding: number[],
|
|
262
|
-
query: MemoryRetrieveQuery,
|
|
263
|
-
params: unknown[],
|
|
264
|
-
baseWhere: string
|
|
265
|
-
): Promise<Memory[]> {
|
|
266
|
-
const vecParam = `{${embedding.join(",")}}`;
|
|
267
|
-
const limit = query.limit ?? 10;
|
|
268
|
-
// baseWhere already includes "WHERE" keyword; strip the prefix
|
|
269
|
-
const conditions = baseWhere.replace(/^WHERE\s+/i, "");
|
|
270
|
-
const whereClause = conditions
|
|
271
|
-
? `WHERE ${conditions} AND embedding IS NOT NULL`
|
|
272
|
-
: `WHERE embedding IS NOT NULL`;
|
|
273
|
-
const sql = `
|
|
274
|
-
SELECT *,
|
|
275
|
-
(embedding <-> $${params.length + 1}::float8[]::vector) AS distance
|
|
276
|
-
FROM ${this.table}
|
|
277
|
-
${whereClause}
|
|
278
|
-
ORDER BY distance ASC
|
|
279
|
-
LIMIT $${params.length + 2}
|
|
280
|
-
`;
|
|
281
|
-
params.push(vecParam);
|
|
282
|
-
params.push(limit);
|
|
283
|
-
|
|
284
|
-
const { rows } = await this.pool.query(sql, params);
|
|
285
|
-
|
|
286
|
-
for (const row of rows.slice(0, limit)) {
|
|
287
|
-
const r = row as PgMemoryRow;
|
|
288
|
-
await this.pool.query(`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`, [r.id]);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return (rows as PgMemoryRow[]).map((r) => this._rowToMemory(r));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private _rowToMemory(row: PgMemoryRow): Memory {
|
|
295
|
-
return {
|
|
296
|
-
id: row.id,
|
|
297
|
-
actorId: row.actor_id,
|
|
298
|
-
memoryType: row.memory_type as MemoryType,
|
|
299
|
-
content: row.content,
|
|
300
|
-
importance: row.importance,
|
|
301
|
-
emotionalValence: row.emotional_valence,
|
|
302
|
-
tags: Array.isArray(row.tags) ? row.tags : [],
|
|
303
|
-
embedding: row.embedding ?? undefined,
|
|
304
|
-
sourceId: row.source_id ?? undefined,
|
|
305
|
-
metadata: row.metadata ?? {},
|
|
306
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
307
|
-
createdAt: new Date(row.created_at),
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
}
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import type { Memory, MemoryType } from "../../types.js";
|
|
2
|
-
import type { StorageProvider, MemoryStoreInput, MemoryRetrieveQuery, MemoryCountFilter } from "../../interfaces.js";
|
|
3
|
-
import { storageError, notFound } from "../../errors.js";
|
|
4
|
-
|
|
5
|
-
// Type-only import — user provides their own ioredis instance
|
|
6
|
-
type RedisClient = {
|
|
7
|
-
set(key: string, value: string): Promise<unknown>;
|
|
8
|
-
get(key: string): Promise<string | null>;
|
|
9
|
-
mget(...keys: string[]): Promise<(string | null)[]>;
|
|
10
|
-
del(key: string | string[]): Promise<number>;
|
|
11
|
-
sadd(key: string, ...members: string[]): Promise<number>;
|
|
12
|
-
srem(key: string, ...members: string[]): Promise<number>;
|
|
13
|
-
smembers(key: string): Promise<string[]>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
interface MemoryRecord extends Memory {
|
|
17
|
-
_touchedAt: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface RedisStorageConfig {
|
|
21
|
-
/** An active ioredis client instance (or compatible) */
|
|
22
|
-
redis: RedisClient;
|
|
23
|
-
/** Key prefix. Default: "memstack" */
|
|
24
|
-
keyPrefix?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class RedisStorage implements StorageProvider {
|
|
28
|
-
private redis: RedisClient;
|
|
29
|
-
private prefix: string;
|
|
30
|
-
|
|
31
|
-
constructor(config: RedisStorageConfig) {
|
|
32
|
-
this.redis = config.redis;
|
|
33
|
-
this.prefix = config.keyPrefix ?? "memstack";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async initialize(): Promise<void> {
|
|
37
|
-
// Connection managed by the user's Redis client
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
generateId(): string {
|
|
41
|
-
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async store(input: MemoryStoreInput): Promise<Memory> {
|
|
45
|
-
const now = new Date();
|
|
46
|
-
const memory: MemoryRecord = {
|
|
47
|
-
id: input.id ?? this.generateId(),
|
|
48
|
-
actorId: input.actorId,
|
|
49
|
-
memoryType: (input.memoryType ?? "interaction") as MemoryType,
|
|
50
|
-
content: input.content,
|
|
51
|
-
importance: input.importance ?? 0.5,
|
|
52
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
53
|
-
tags: input.tags ?? [],
|
|
54
|
-
embedding: input.embedding,
|
|
55
|
-
sourceId: input.sourceId,
|
|
56
|
-
metadata: input.metadata ?? {},
|
|
57
|
-
expiresAt: input.expiresAt,
|
|
58
|
-
createdAt: now,
|
|
59
|
-
_touchedAt: now.toISOString(),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
63
|
-
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
64
|
-
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
65
|
-
|
|
66
|
-
return this._toExternal(memory);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> {
|
|
70
|
-
let results: Memory[] = [];
|
|
71
|
-
const now = new Date();
|
|
72
|
-
|
|
73
|
-
for (const input of inputs) {
|
|
74
|
-
const memory: MemoryRecord = {
|
|
75
|
-
id: input.id ?? this.generateId(),
|
|
76
|
-
actorId: input.actorId,
|
|
77
|
-
memoryType: (input.memoryType ?? "interaction") as MemoryType,
|
|
78
|
-
content: input.content,
|
|
79
|
-
importance: input.importance ?? 0.5,
|
|
80
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
81
|
-
tags: input.tags ?? [],
|
|
82
|
-
embedding: input.embedding,
|
|
83
|
-
sourceId: input.sourceId,
|
|
84
|
-
metadata: input.metadata ?? {},
|
|
85
|
-
expiresAt: input.expiresAt,
|
|
86
|
-
createdAt: now,
|
|
87
|
-
_touchedAt: now.toISOString(),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
91
|
-
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
92
|
-
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
93
|
-
results.push(this._toExternal(memory));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return results;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async get(id: string): Promise<Memory | null> {
|
|
100
|
-
const raw = await this.redis.get(this._memKey(id));
|
|
101
|
-
if (!raw) return null;
|
|
102
|
-
|
|
103
|
-
const record = JSON.parse(raw) as MemoryRecord;
|
|
104
|
-
if (record.expiresAt && new Date(record.expiresAt) <= new Date()) return null;
|
|
105
|
-
return this._toExternal(record);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async delete(id: string): Promise<void> {
|
|
109
|
-
const raw = await this.redis.get(this._memKey(id));
|
|
110
|
-
if (!raw) throw notFound("Memory", id);
|
|
111
|
-
|
|
112
|
-
const record = JSON.parse(raw) as MemoryRecord;
|
|
113
|
-
await this.redis.del(this._memKey(id));
|
|
114
|
-
await this.redis.srem(this._actorKey(record.actorId), id);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async deleteMany(ids: string[]): Promise<number> {
|
|
118
|
-
let count = 0;
|
|
119
|
-
const byActor = new Map<string, string[]>();
|
|
120
|
-
|
|
121
|
-
for (const id of ids) {
|
|
122
|
-
const raw = await this.redis.get(this._memKey(id));
|
|
123
|
-
if (raw) {
|
|
124
|
-
const record = JSON.parse(raw) as MemoryRecord;
|
|
125
|
-
const list = byActor.get(record.actorId) ?? [];
|
|
126
|
-
list.push(id);
|
|
127
|
-
byActor.set(record.actorId, list);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
for (const [actorId, memIds] of byActor) {
|
|
132
|
-
await this.redis.del(memIds.map((id) => this._memKey(id)));
|
|
133
|
-
await this.redis.srem(this._actorKey(actorId), ...memIds);
|
|
134
|
-
count += memIds.length;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return count;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async retrieve(query: MemoryRetrieveQuery, _embedding?: number[]): Promise<Memory[]> {
|
|
141
|
-
let allRecords: MemoryRecord[] = [];
|
|
142
|
-
|
|
143
|
-
if (query.actorId) {
|
|
144
|
-
allRecords = await this._getActorRecords(query.actorId);
|
|
145
|
-
} else {
|
|
146
|
-
const actorIds = await this._listActors();
|
|
147
|
-
for (const actorId of actorIds) {
|
|
148
|
-
const records = await this._getActorRecords(actorId);
|
|
149
|
-
allRecords.push(...records);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const now = new Date();
|
|
154
|
-
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
155
|
-
|
|
156
|
-
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
157
|
-
results = results.filter((r) => query.memoryTypes!.includes(r.memoryType));
|
|
158
|
-
}
|
|
159
|
-
if (query.tags && query.tags.length > 0) {
|
|
160
|
-
results = results.filter((r) => r.tags?.some((t) => query.tags!.includes(t)));
|
|
161
|
-
}
|
|
162
|
-
if (query.query) {
|
|
163
|
-
const q = query.query.toLowerCase();
|
|
164
|
-
results = results.filter((r) => r.content.toLowerCase().includes(q));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
switch (query.strategy) {
|
|
168
|
-
case "recent":
|
|
169
|
-
results.sort((a, b) => new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime());
|
|
170
|
-
break;
|
|
171
|
-
case "important":
|
|
172
|
-
results.sort((a, b) => b.importance - a.importance);
|
|
173
|
-
break;
|
|
174
|
-
case "hybrid":
|
|
175
|
-
case "semantic":
|
|
176
|
-
default:
|
|
177
|
-
results.sort(
|
|
178
|
-
(a, b) =>
|
|
179
|
-
b.importance - a.importance || new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime()
|
|
180
|
-
);
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Touch records on retrieval
|
|
185
|
-
const nowStr = new Date().toISOString();
|
|
186
|
-
const toTouch = results.slice(0, query.limit ?? 10);
|
|
187
|
-
for (const r of toTouch) {
|
|
188
|
-
r._touchedAt = nowStr;
|
|
189
|
-
await this.redis.set(this._memKey(r.id), JSON.stringify(r));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const limit = query.limit ?? 10;
|
|
193
|
-
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async count(filter?: MemoryCountFilter): Promise<number> {
|
|
197
|
-
let allRecords: MemoryRecord[] = [];
|
|
198
|
-
|
|
199
|
-
if (filter?.actorId) {
|
|
200
|
-
allRecords = await this._getActorRecords(filter.actorId);
|
|
201
|
-
} else {
|
|
202
|
-
const actorIds = await this._listActors();
|
|
203
|
-
for (const actorId of actorIds) {
|
|
204
|
-
const records = await this._getActorRecords(actorId);
|
|
205
|
-
allRecords.push(...records);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const now = new Date();
|
|
210
|
-
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
211
|
-
|
|
212
|
-
if (filter?.memoryType) {
|
|
213
|
-
results = results.filter((r) => r.memoryType === filter.memoryType);
|
|
214
|
-
}
|
|
215
|
-
if (filter?.minImportance !== undefined) {
|
|
216
|
-
results = results.filter((r) => r.importance >= filter.minImportance!);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return results.length;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async close(): Promise<void> {
|
|
223
|
-
// User manages connection lifecycle
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── Internal ──
|
|
227
|
-
|
|
228
|
-
private _actorsSetKey(): string {
|
|
229
|
-
return `${this.prefix}:actors`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private _actorKey(actorId: string): string {
|
|
233
|
-
const safe = actorId.replace(/[^a-zA-Z0-9_\-.,@]/g, "_");
|
|
234
|
-
return `${this.prefix}:a:${safe}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private _memKey(id: string): string {
|
|
238
|
-
return `${this.prefix}:m:${id}`;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private async _getActorRecords(actorId: string): Promise<MemoryRecord[]> {
|
|
242
|
-
const ids = await this.redis.smembers(this._actorKey(actorId));
|
|
243
|
-
if (ids.length === 0) return [];
|
|
244
|
-
|
|
245
|
-
const keys = ids.map((id) => this._memKey(id));
|
|
246
|
-
const raws = await this.redis.mget(...keys);
|
|
247
|
-
|
|
248
|
-
const records: MemoryRecord[] = [];
|
|
249
|
-
for (const raw of raws) {
|
|
250
|
-
if (raw) {
|
|
251
|
-
try {
|
|
252
|
-
records.push(JSON.parse(raw) as MemoryRecord);
|
|
253
|
-
} catch {
|
|
254
|
-
// Skip corrupted records
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return records;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private async _listActors(): Promise<string[]> {
|
|
263
|
-
return this.redis.smembers(this._actorsSetKey());
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private _toExternal(record: MemoryRecord): Memory {
|
|
267
|
-
const { _touchedAt: _, ...rest } = record;
|
|
268
|
-
return {
|
|
269
|
-
...rest,
|
|
270
|
-
createdAt: new Date(record.createdAt),
|
|
271
|
-
expiresAt: record.expiresAt ? new Date(record.expiresAt) : undefined,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
}
|