@smithers-orchestrator/memory 0.24.2 → 0.25.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/package.json +6 -6
- package/src/Summarizer.js +77 -6
- package/src/TokenLimiter.js +17 -6
- package/src/index.d.ts +4 -0
- package/src/store/MemoryStore.ts +7 -0
- package/src/store/MemoryStoreLive.js +67 -14
- package/src/react-types.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Persistent and semantic memory services for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"drizzle-orm": "^0.45.2",
|
|
39
39
|
"effect": "^3.21.1",
|
|
40
40
|
"zod": "^4.3.6",
|
|
41
|
-
"@smithers-orchestrator/db": "0.
|
|
42
|
-
"@smithers-orchestrator/errors": "0.
|
|
43
|
-
"@smithers-orchestrator/
|
|
44
|
-
"@smithers-orchestrator/scheduler": "0.
|
|
45
|
-
"@smithers-orchestrator/
|
|
41
|
+
"@smithers-orchestrator/db": "0.25.0",
|
|
42
|
+
"@smithers-orchestrator/errors": "0.25.0",
|
|
43
|
+
"@smithers-orchestrator/observability": "0.25.0",
|
|
44
|
+
"@smithers-orchestrator/scheduler": "0.25.0",
|
|
45
|
+
"@smithers-orchestrator/graph": "0.25.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/bun": "latest",
|
package/src/Summarizer.js
CHANGED
|
@@ -1,21 +1,92 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
/** @typedef {import("./MemoryProcessor.ts").MemoryProcessor} MemoryProcessor */
|
|
3
3
|
|
|
4
|
+
const RECENT_MESSAGE_COUNT = 2;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("./MemoryMessage.ts").MemoryMessage} message
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function renderMessage(message) {
|
|
11
|
+
let content = message.contentJson;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(message.contentJson);
|
|
14
|
+
content = typeof parsed === "string" ? parsed : JSON.stringify(parsed);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Preserve raw content when it is not JSON.
|
|
18
|
+
}
|
|
19
|
+
return `${message.role}: ${content}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {unknown} result
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function extractSummary(result) {
|
|
27
|
+
if (typeof result === "string") {
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
if (result && typeof result === "object") {
|
|
31
|
+
if ("text" in result && typeof result.text === "string") {
|
|
32
|
+
return result.text;
|
|
33
|
+
}
|
|
34
|
+
if ("output" in result && typeof result.output === "string") {
|
|
35
|
+
return result.output;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(result);
|
|
39
|
+
}
|
|
40
|
+
|
|
4
41
|
/**
|
|
5
42
|
* @param {{ run: (prompt: string) => Promise<any> }} agent
|
|
6
43
|
* @returns {MemoryProcessor}
|
|
7
44
|
*/
|
|
8
|
-
export function Summarizer(
|
|
45
|
+
export function Summarizer(agent) {
|
|
9
46
|
/**
|
|
10
47
|
* @param {MemoryStore} store
|
|
11
48
|
* @returns {Effect.Effect<void, SmithersError>}
|
|
12
49
|
*/
|
|
13
|
-
function processEffect(
|
|
50
|
+
function processEffect(store) {
|
|
14
51
|
return Effect.gen(function* () {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
52
|
+
const threads = yield* store.listThreadsEffect();
|
|
53
|
+
let summarized = 0;
|
|
54
|
+
for (const thread of threads) {
|
|
55
|
+
const messages = yield* store.listMessagesEffect(thread.threadId);
|
|
56
|
+
if (messages.length <= RECENT_MESSAGE_COUNT) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const oldMessages = messages.slice(0, -RECENT_MESSAGE_COUNT);
|
|
60
|
+
const recentMessages = messages.slice(-RECENT_MESSAGE_COUNT);
|
|
61
|
+
if (oldMessages.length === 1 && oldMessages[0].role === "system") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const prompt = [
|
|
65
|
+
"Summarize these older conversation messages for future context.",
|
|
66
|
+
"Keep durable user preferences, decisions, facts, and unresolved tasks.",
|
|
67
|
+
"",
|
|
68
|
+
oldMessages.map(renderMessage).join("\n"),
|
|
69
|
+
].join("\n");
|
|
70
|
+
const result = yield* Effect.tryPromise(() => agent.run(prompt));
|
|
71
|
+
const summary = extractSummary(result);
|
|
72
|
+
// Save the summary BEFORE deleting the old messages so there is no
|
|
73
|
+
// data-loss window: if the summary write fails the originals are
|
|
74
|
+
// still present (recoverable), and if the delete fails afterwards
|
|
75
|
+
// the originals plus the summary coexist (harmless — the next run
|
|
76
|
+
// re-summarizes and deletes them). The state where the old
|
|
77
|
+
// messages are gone and no summary exists is never reachable.
|
|
78
|
+
yield* store.saveMessageEffect({
|
|
79
|
+
id: `summary-${crypto.randomUUID()}`,
|
|
80
|
+
threadId: thread.threadId,
|
|
81
|
+
role: "system",
|
|
82
|
+
contentJson: JSON.stringify({ type: "summary", text: summary }),
|
|
83
|
+
createdAtMs: oldMessages[0].createdAtMs,
|
|
84
|
+
});
|
|
85
|
+
yield* store.deleteMessagesEffect(thread.threadId, oldMessages.map((message) => message.id));
|
|
86
|
+
yield* Effect.logInfo(`Summarizer: compressed ${oldMessages.length} messages before ${recentMessages[0]?.id ?? "end"}`);
|
|
87
|
+
summarized += 1;
|
|
88
|
+
}
|
|
89
|
+
yield* Effect.logInfo(`Summarizer: summarized ${summarized} threads`);
|
|
19
90
|
}).pipe(Effect.annotateLogs({ processor: "Summarizer" }), Effect.withLogSpan("memory:processor:summarizer"));
|
|
20
91
|
}
|
|
21
92
|
return {
|
package/src/TokenLimiter.js
CHANGED
|
@@ -12,13 +12,24 @@ export function TokenLimiter(maxTokens) {
|
|
|
12
12
|
* @param {MemoryStore} store
|
|
13
13
|
* @returns {Effect.Effect<void, SmithersError>}
|
|
14
14
|
*/
|
|
15
|
-
function processEffect(
|
|
15
|
+
function processEffect(store) {
|
|
16
16
|
return Effect.gen(function* () {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const threads = yield* store.listThreadsEffect();
|
|
18
|
+
let deleted = 0;
|
|
19
|
+
for (const thread of threads) {
|
|
20
|
+
const messages = yield* store.listMessagesEffect(thread.threadId);
|
|
21
|
+
let charCount = messages.reduce((total, message) => total + message.contentJson.length, 0);
|
|
22
|
+
const deleteIds = [];
|
|
23
|
+
for (const message of messages) {
|
|
24
|
+
if (charCount <= charBudget) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
deleteIds.push(message.id);
|
|
28
|
+
charCount -= message.contentJson.length;
|
|
29
|
+
}
|
|
30
|
+
deleted += yield* store.deleteMessagesEffect(thread.threadId, deleteIds);
|
|
31
|
+
}
|
|
32
|
+
yield* Effect.logInfo(`TokenLimiter: deleted ${deleted} messages to enforce ${maxTokens} token budget`);
|
|
22
33
|
}).pipe(Effect.annotateLogs({ processor: "TokenLimiter", maxTokens }), Effect.withLogSpan("memory:processor:token-limiter"));
|
|
23
34
|
}
|
|
24
35
|
return {
|
package/src/index.d.ts
CHANGED
|
@@ -83,12 +83,14 @@ type MemoryStore$2 = {
|
|
|
83
83
|
listFacts: (ns: MemoryNamespace$3) => Promise<MemoryFact$1[]>;
|
|
84
84
|
createThread: (ns: MemoryNamespace$3, title?: string) => Promise<MemoryThread$1>;
|
|
85
85
|
getThread: (threadId: string) => Promise<MemoryThread$1 | undefined>;
|
|
86
|
+
listThreads: () => Promise<MemoryThread$1[]>;
|
|
86
87
|
deleteThread: (threadId: string) => Promise<void>;
|
|
87
88
|
saveMessage: (msg: Omit<MemoryMessage$1, "createdAtMs"> & {
|
|
88
89
|
createdAtMs?: number;
|
|
89
90
|
}) => Promise<void>;
|
|
90
91
|
listMessages: (threadId: string, limit?: number) => Promise<MemoryMessage$1[]>;
|
|
91
92
|
countMessages: (threadId: string) => Promise<number>;
|
|
93
|
+
deleteMessages: (threadId: string, messageIds: string[]) => Promise<number>;
|
|
92
94
|
deleteExpiredFacts: () => Promise<number>;
|
|
93
95
|
getFactEffect: (ns: MemoryNamespace$3, key: string) => Effect.Effect<MemoryFact$1 | undefined, SmithersError>;
|
|
94
96
|
setFactEffect: (ns: MemoryNamespace$3, key: string, value: unknown, ttlMs?: number) => Effect.Effect<void, SmithersError>;
|
|
@@ -96,12 +98,14 @@ type MemoryStore$2 = {
|
|
|
96
98
|
listFactsEffect: (ns: MemoryNamespace$3) => Effect.Effect<MemoryFact$1[], SmithersError>;
|
|
97
99
|
createThreadEffect: (ns: MemoryNamespace$3, title?: string) => Effect.Effect<MemoryThread$1, SmithersError>;
|
|
98
100
|
getThreadEffect: (threadId: string) => Effect.Effect<MemoryThread$1 | undefined, SmithersError>;
|
|
101
|
+
listThreadsEffect: () => Effect.Effect<MemoryThread$1[], SmithersError>;
|
|
99
102
|
deleteThreadEffect: (threadId: string) => Effect.Effect<void, SmithersError>;
|
|
100
103
|
saveMessageEffect: (msg: Omit<MemoryMessage$1, "createdAtMs"> & {
|
|
101
104
|
createdAtMs?: number;
|
|
102
105
|
}) => Effect.Effect<void, SmithersError>;
|
|
103
106
|
listMessagesEffect: (threadId: string, limit?: number) => Effect.Effect<MemoryMessage$1[], SmithersError>;
|
|
104
107
|
countMessagesEffect: (threadId: string) => Effect.Effect<number, SmithersError>;
|
|
108
|
+
deleteMessagesEffect: (threadId: string, messageIds: string[]) => Effect.Effect<number, SmithersError>;
|
|
105
109
|
deleteExpiredFactsEffect: () => Effect.Effect<number, SmithersError>;
|
|
106
110
|
};
|
|
107
111
|
|
package/src/store/MemoryStore.ts
CHANGED
|
@@ -20,12 +20,14 @@ export type MemoryStore = {
|
|
|
20
20
|
listFacts: (ns: MemoryNamespace) => Promise<MemoryFact[]>;
|
|
21
21
|
createThread: (ns: MemoryNamespace, title?: string) => Promise<MemoryThread>;
|
|
22
22
|
getThread: (threadId: string) => Promise<MemoryThread | undefined>;
|
|
23
|
+
listThreads: () => Promise<MemoryThread[]>;
|
|
23
24
|
deleteThread: (threadId: string) => Promise<void>;
|
|
24
25
|
saveMessage: (
|
|
25
26
|
msg: Omit<MemoryMessage, "createdAtMs"> & { createdAtMs?: number },
|
|
26
27
|
) => Promise<void>;
|
|
27
28
|
listMessages: (threadId: string, limit?: number) => Promise<MemoryMessage[]>;
|
|
28
29
|
countMessages: (threadId: string) => Promise<number>;
|
|
30
|
+
deleteMessages: (threadId: string, messageIds: string[]) => Promise<number>;
|
|
29
31
|
deleteExpiredFacts: () => Promise<number>;
|
|
30
32
|
getFactEffect: (
|
|
31
33
|
ns: MemoryNamespace,
|
|
@@ -51,6 +53,7 @@ export type MemoryStore = {
|
|
|
51
53
|
getThreadEffect: (
|
|
52
54
|
threadId: string,
|
|
53
55
|
) => Effect.Effect<MemoryThread | undefined, SmithersError>;
|
|
56
|
+
listThreadsEffect: () => Effect.Effect<MemoryThread[], SmithersError>;
|
|
54
57
|
deleteThreadEffect: (
|
|
55
58
|
threadId: string,
|
|
56
59
|
) => Effect.Effect<void, SmithersError>;
|
|
@@ -64,5 +67,9 @@ export type MemoryStore = {
|
|
|
64
67
|
countMessagesEffect: (
|
|
65
68
|
threadId: string,
|
|
66
69
|
) => Effect.Effect<number, SmithersError>;
|
|
70
|
+
deleteMessagesEffect: (
|
|
71
|
+
threadId: string,
|
|
72
|
+
messageIds: string[],
|
|
73
|
+
) => Effect.Effect<number, SmithersError>;
|
|
67
74
|
deleteExpiredFactsEffect: () => Effect.Effect<number, SmithersError>;
|
|
68
75
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, eq, sql } from "drizzle-orm";
|
|
1
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
2
2
|
import { Effect, Layer, Metric } from "effect";
|
|
3
3
|
import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
|
|
4
4
|
import { dbQueryDuration } from "@smithers-orchestrator/observability/metrics";
|
|
@@ -194,20 +194,38 @@ function makeMemoryStore(db) {
|
|
|
194
194
|
.limit(1)).pipe(Effect.map((rows) => rows[0]));
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
|
+
* @returns {Effect.Effect<MemoryThread[], SmithersError>}
|
|
198
|
+
*/
|
|
199
|
+
function listThreadsEffect() {
|
|
200
|
+
return readEffect("memory listThreads", () => db
|
|
201
|
+
.select()
|
|
202
|
+
.from(smithersMemoryThreads)
|
|
203
|
+
.orderBy(smithersMemoryThreads.createdAtMs)).pipe(Effect.map((rows) => rows.map((row) => ({
|
|
204
|
+
threadId: row.threadId,
|
|
205
|
+
namespace: row.namespace,
|
|
206
|
+
title: row.title,
|
|
207
|
+
metadataJson: row.metadataJson,
|
|
208
|
+
createdAtMs: row.createdAtMs,
|
|
209
|
+
updatedAtMs: row.updatedAtMs,
|
|
210
|
+
}))));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
197
213
|
* @param {string} threadId
|
|
198
214
|
* @returns {Effect.Effect<void, SmithersError>}
|
|
199
215
|
*/
|
|
200
216
|
function deleteThreadEffect(threadId) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.delete(smithersMemoryThreads)
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
// Delete the messages and the thread row atomically so a failure on the
|
|
218
|
+
// second write can't leave the thread without its messages (or vice versa).
|
|
219
|
+
return writeEffect("memory deleteThread", () => Promise.resolve(
|
|
220
|
+
db.transaction((tx) => {
|
|
221
|
+
tx.delete(smithersMemoryMessages)
|
|
222
|
+
.where(eq(smithersMemoryMessages.threadId, threadId))
|
|
223
|
+
.run();
|
|
224
|
+
tx.delete(smithersMemoryThreads)
|
|
225
|
+
.where(eq(smithersMemoryThreads.threadId, threadId))
|
|
226
|
+
.run();
|
|
227
|
+
}),
|
|
228
|
+
));
|
|
211
229
|
}
|
|
212
230
|
// --- Message Effects ---
|
|
213
231
|
/**
|
|
@@ -217,14 +235,32 @@ function makeMemoryStore(db) {
|
|
|
217
235
|
function saveMessageEffect(msg) {
|
|
218
236
|
return Effect.gen(function* () {
|
|
219
237
|
yield* Metric.increment(memoryMessageSaves);
|
|
220
|
-
|
|
238
|
+
const createdAtMs = msg.createdAtMs ?? nowMs();
|
|
239
|
+
// Idempotent: re-saving a message with the same id (e.g. on
|
|
240
|
+
// crash-resume, deterministic replay, or fork/restore where ids are
|
|
241
|
+
// derived deterministically) must be a safe no-op upsert rather than
|
|
242
|
+
// a UNIQUE-constraint crash. Mirrors setFact's onConflictDoUpdate.
|
|
243
|
+
yield* writeEffect("memory saveMessage", () => db
|
|
244
|
+
.insert(smithersMemoryMessages)
|
|
245
|
+
.values({
|
|
221
246
|
id: msg.id,
|
|
222
247
|
threadId: msg.threadId,
|
|
223
248
|
role: msg.role,
|
|
224
249
|
contentJson: msg.contentJson,
|
|
225
250
|
runId: msg.runId ?? null,
|
|
226
251
|
nodeId: msg.nodeId ?? null,
|
|
227
|
-
createdAtMs
|
|
252
|
+
createdAtMs,
|
|
253
|
+
})
|
|
254
|
+
.onConflictDoUpdate({
|
|
255
|
+
target: smithersMemoryMessages.id,
|
|
256
|
+
set: {
|
|
257
|
+
threadId: msg.threadId,
|
|
258
|
+
role: msg.role,
|
|
259
|
+
contentJson: msg.contentJson,
|
|
260
|
+
runId: msg.runId ?? null,
|
|
261
|
+
nodeId: msg.nodeId ?? null,
|
|
262
|
+
createdAtMs,
|
|
263
|
+
},
|
|
228
264
|
}));
|
|
229
265
|
});
|
|
230
266
|
}
|
|
@@ -240,7 +276,7 @@ function makeMemoryStore(db) {
|
|
|
240
276
|
.from(smithersMemoryMessages)
|
|
241
277
|
.where(eq(smithersMemoryMessages.threadId, threadId))
|
|
242
278
|
.orderBy(smithersMemoryMessages.createdAtMs);
|
|
243
|
-
if (limit) {
|
|
279
|
+
if (limit !== undefined) {
|
|
244
280
|
query = query.limit(limit);
|
|
245
281
|
}
|
|
246
282
|
return query;
|
|
@@ -264,6 +300,19 @@ function makeMemoryStore(db) {
|
|
|
264
300
|
.from(smithersMemoryMessages)
|
|
265
301
|
.where(eq(smithersMemoryMessages.threadId, threadId))).pipe(Effect.map((rows) => rows[0]?.count ?? 0));
|
|
266
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* @param {string} threadId
|
|
305
|
+
* @param {string[]} messageIds
|
|
306
|
+
* @returns {Effect.Effect<number, SmithersError>}
|
|
307
|
+
*/
|
|
308
|
+
function deleteMessagesEffect(threadId, messageIds) {
|
|
309
|
+
if (messageIds.length === 0) {
|
|
310
|
+
return Effect.succeed(0);
|
|
311
|
+
}
|
|
312
|
+
return writeEffect("memory deleteMessages", () => db
|
|
313
|
+
.delete(smithersMemoryMessages)
|
|
314
|
+
.where(and(eq(smithersMemoryMessages.threadId, threadId), inArray(smithersMemoryMessages.id, messageIds)))).pipe(Effect.map((result) => result?.changes ?? result?.rowsAffected ?? 0));
|
|
315
|
+
}
|
|
267
316
|
// --- Maintenance ---
|
|
268
317
|
/**
|
|
269
318
|
* @returns {Effect.Effect<number, SmithersError>}
|
|
@@ -283,10 +332,12 @@ function makeMemoryStore(db) {
|
|
|
283
332
|
listFacts: (ns) => Effect.runPromise(listFactsEffect(ns)),
|
|
284
333
|
createThread: (ns, title) => Effect.runPromise(createThreadEffect(ns, title)),
|
|
285
334
|
getThread: (threadId) => Effect.runPromise(getThreadEffect(threadId)),
|
|
335
|
+
listThreads: () => Effect.runPromise(listThreadsEffect()),
|
|
286
336
|
deleteThread: (threadId) => Effect.runPromise(deleteThreadEffect(threadId)),
|
|
287
337
|
saveMessage: (msg) => Effect.runPromise(saveMessageEffect(msg)),
|
|
288
338
|
listMessages: (threadId, limit) => Effect.runPromise(listMessagesEffect(threadId, limit)),
|
|
289
339
|
countMessages: (threadId) => Effect.runPromise(countMessagesEffect(threadId)),
|
|
340
|
+
deleteMessages: (threadId, messageIds) => Effect.runPromise(deleteMessagesEffect(threadId, messageIds)),
|
|
290
341
|
deleteExpiredFacts: () => Effect.runPromise(deleteExpiredFactsEffect()),
|
|
291
342
|
// Effect variants
|
|
292
343
|
getFactEffect,
|
|
@@ -295,10 +346,12 @@ function makeMemoryStore(db) {
|
|
|
295
346
|
listFactsEffect,
|
|
296
347
|
createThreadEffect,
|
|
297
348
|
getThreadEffect,
|
|
349
|
+
listThreadsEffect,
|
|
298
350
|
deleteThreadEffect,
|
|
299
351
|
saveMessageEffect,
|
|
300
352
|
listMessagesEffect,
|
|
301
353
|
countMessagesEffect,
|
|
354
|
+
deleteMessagesEffect,
|
|
302
355
|
deleteExpiredFactsEffect,
|
|
303
356
|
};
|
|
304
357
|
}
|
package/src/react-types.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type { TaskMemoryConfig } from "@smithers-orchestrator/graph/types";
|