@batalabs/virlow-mcp-core 3.19.0 → 3.68.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/LICENSE +8 -18
- package/README.md +1 -1
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -0
- package/dist/api.js.map +1 -1
- package/dist/attrs-extract.d.ts +48 -0
- package/dist/attrs-extract.d.ts.map +1 -0
- package/dist/attrs-extract.js +153 -0
- package/dist/attrs-extract.js.map +1 -0
- package/dist/attrs.d.ts +445 -0
- package/dist/attrs.d.ts.map +1 -0
- package/dist/attrs.js +604 -0
- package/dist/attrs.js.map +1 -0
- package/dist/cjs/api.d.ts +14 -0
- package/dist/cjs/api.js +6 -0
- package/dist/cjs/attrs-extract.d.ts +47 -0
- package/dist/cjs/attrs-extract.js +159 -0
- package/dist/cjs/attrs.d.ts +444 -0
- package/dist/cjs/attrs.js +635 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +22 -1
- package/dist/cjs/memories.d.ts +79 -1
- package/dist/cjs/memories.js +249 -15
- package/dist/cjs/memory-graph.d.ts +105 -0
- package/dist/cjs/memory-graph.js +270 -0
- package/dist/cjs/memory-note.d.ts +57 -0
- package/dist/cjs/memory-note.js +174 -10
- package/dist/cjs/memory-secrets.d.ts +19 -0
- package/dist/cjs/memory-secrets.js +108 -0
- package/dist/cjs/notes.d.ts +208 -2
- package/dist/cjs/notes.js +721 -12
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/memories.d.ts +79 -1
- package/dist/memories.d.ts.map +1 -1
- package/dist/memories.js +249 -15
- package/dist/memories.js.map +1 -1
- package/dist/memory-graph.d.ts +106 -0
- package/dist/memory-graph.d.ts.map +1 -0
- package/dist/memory-graph.js +258 -0
- package/dist/memory-graph.js.map +1 -0
- package/dist/memory-note.d.ts +57 -0
- package/dist/memory-note.d.ts.map +1 -1
- package/dist/memory-note.js +170 -10
- package/dist/memory-note.js.map +1 -1
- package/dist/memory-secrets.d.ts +20 -0
- package/dist/memory-secrets.d.ts.map +1 -0
- package/dist/memory-secrets.js +103 -0
- package/dist/memory-secrets.js.map +1 -0
- package/dist/notes.d.ts +208 -2
- package/dist/notes.d.ts.map +1 -1
- package/dist/notes.js +722 -13
- package/dist/notes.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +464 -9
- package/dist/tools.js.map +1 -1
- package/package.json +15 -5
package/dist/cjs/memories.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type Embedder } from '@batalabs/virlow-memory';
|
|
|
2
2
|
import { type VirlowApi } from './api.js';
|
|
3
3
|
import type { EmbeddingCacheLike } from './embedding-cache.js';
|
|
4
4
|
import { type MemoryMeta } from './memory-note.js';
|
|
5
|
+
import { type MemoryGraph } from './memory-graph.js';
|
|
6
|
+
import type { MemoryRelation } from './memory-note.js';
|
|
5
7
|
import type { Vault } from './vault.js';
|
|
6
8
|
/**
|
|
7
9
|
* Page size for the sync walk over each memory folder.
|
|
@@ -33,6 +35,13 @@ export interface SimilarMemory {
|
|
|
33
35
|
text: string;
|
|
34
36
|
similarity: number;
|
|
35
37
|
}
|
|
38
|
+
/** A fact the new memory ended: it said something single-valued (who leads
|
|
39
|
+
* a project, where someone works) that the new one replaces. */
|
|
40
|
+
export interface ClosedFact {
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
relation: MemoryRelation;
|
|
44
|
+
}
|
|
36
45
|
export type SyncResult = {
|
|
37
46
|
added: number;
|
|
38
47
|
updated: number;
|
|
@@ -66,10 +75,79 @@ export declare class MemoryStore {
|
|
|
66
75
|
action: 'stored' | 'updated';
|
|
67
76
|
id: string;
|
|
68
77
|
similar: SimilarMemory[];
|
|
78
|
+
closed: ClosedFact[];
|
|
69
79
|
}>;
|
|
70
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Ends the facts a new memory replaces. Each superseded relation gets a
|
|
82
|
+
* `to` date (the new fact's start); a memory whose relations have all
|
|
83
|
+
* ended gets `valid_to` and `superseded_by`. Nothing is deleted, so "who
|
|
84
|
+
* led Atlas before Sam?" stays answerable.
|
|
85
|
+
*/
|
|
86
|
+
private closeSuperseded;
|
|
87
|
+
/** The graph over what is cached now. Cheap: rebuilt from the notes on
|
|
88
|
+
* demand, never stored. */
|
|
89
|
+
graph(namespace?: string): MemoryGraph;
|
|
90
|
+
/** New names spelled the way existing memories already spell them, and
|
|
91
|
+
* predicates in snake_case, so one entity does not split in two. */
|
|
92
|
+
private canonicalize;
|
|
93
|
+
/**
|
|
94
|
+
* Hybrid recall: similarity, then the graph (memories about an entity the
|
|
95
|
+
* query names, and one relation out from it) and recency. Current facts
|
|
96
|
+
* only, unless `includeHistory`, or as they stood on `asOf`.
|
|
97
|
+
*/
|
|
98
|
+
search(query: string, namespace?: string, limit?: number, opts?: {
|
|
99
|
+
asOf?: string;
|
|
100
|
+
includeHistory?: boolean;
|
|
101
|
+
}): Promise<Array<Omit<CachedMemory, 'embedding' | 'folderId' | 'updatedAt'> & {
|
|
71
102
|
similarity: number;
|
|
72
103
|
}>>;
|
|
104
|
+
/** Everything known about one entity: its current and past relations and
|
|
105
|
+
* the facts behind them. Null when no memory names it. */
|
|
106
|
+
recallEntity(name: string, namespace?: string): Promise<{
|
|
107
|
+
name: string;
|
|
108
|
+
current: CachedMemory[];
|
|
109
|
+
past: CachedMemory[];
|
|
110
|
+
relations: {
|
|
111
|
+
memoryId: string;
|
|
112
|
+
s: string;
|
|
113
|
+
p: string;
|
|
114
|
+
o: string;
|
|
115
|
+
from?: string;
|
|
116
|
+
to?: string;
|
|
117
|
+
}[];
|
|
118
|
+
} | null>;
|
|
119
|
+
/** Adds entities and relations to an existing memory: how a memory saved
|
|
120
|
+
* before the graph existed gets linked. Also ends what it replaces. */
|
|
121
|
+
link(id: string, add: {
|
|
122
|
+
about?: string[];
|
|
123
|
+
relations?: MemoryRelation[];
|
|
124
|
+
}): Promise<ClosedFact[]>;
|
|
125
|
+
/** One entity spelled two ways becomes one: every memory naming `from`
|
|
126
|
+
* is rewritten to name `into`. Returns how many memories changed. */
|
|
127
|
+
mergeEntities(from: string, into: string): Promise<number>;
|
|
128
|
+
/**
|
|
129
|
+
* What to load at the start of a session: the entities this namespace
|
|
130
|
+
* talks about most, and its most recent current facts, within `limit`
|
|
131
|
+
* facts. Global memories are included alongside a project's.
|
|
132
|
+
*/
|
|
133
|
+
context(namespace?: string, limit?: number): Promise<{
|
|
134
|
+
entities: {
|
|
135
|
+
name: string;
|
|
136
|
+
facts: number;
|
|
137
|
+
}[];
|
|
138
|
+
facts: CachedMemory[];
|
|
139
|
+
}>;
|
|
140
|
+
/** Findings for a tidy pass: unlinked memories, likely duplicate names and
|
|
141
|
+
* single-valued facts holding two values at once. */
|
|
142
|
+
health(namespace?: string): Promise<{
|
|
143
|
+
unlinked: string[];
|
|
144
|
+
possibleDuplicates: [string, string][];
|
|
145
|
+
contradictions: {
|
|
146
|
+
subject: string;
|
|
147
|
+
predicate: string;
|
|
148
|
+
objects: string[];
|
|
149
|
+
}[];
|
|
150
|
+
}>;
|
|
73
151
|
/** Requires a prior sync() in the same unlock to have populated the cache. */
|
|
74
152
|
list(namespace?: string): CachedMemory[];
|
|
75
153
|
update(id: string, changes: {
|
package/dist/cjs/memories.js
CHANGED
|
@@ -6,6 +6,8 @@ const virlow_memory_1 = require("@batalabs/virlow-memory");
|
|
|
6
6
|
const api_js_1 = require("./api.js");
|
|
7
7
|
const memory_note_js_1 = require("./memory-note.js");
|
|
8
8
|
const memory_tree_js_1 = require("./memory-tree.js");
|
|
9
|
+
const memory_graph_js_1 = require("./memory-graph.js");
|
|
10
|
+
const memory_secrets_js_1 = require("./memory-secrets.js");
|
|
9
11
|
/**
|
|
10
12
|
* Page size for the sync walk over each memory folder.
|
|
11
13
|
*
|
|
@@ -20,6 +22,25 @@ const memory_tree_js_1 = require("./memory-tree.js");
|
|
|
20
22
|
* lowering it only costs round trips.
|
|
21
23
|
*/
|
|
22
24
|
exports.SYNC_PAGE_LIMIT = 50;
|
|
25
|
+
/** Today as YYYY-MM-DD, the default start of a fact learned now. */
|
|
26
|
+
function today() {
|
|
27
|
+
return new Date().toISOString().slice(0, 10);
|
|
28
|
+
}
|
|
29
|
+
/** Merge two relation lists, one entry per subject, predicate and object. */
|
|
30
|
+
function unionRelations(a = [], b = []) {
|
|
31
|
+
const byKey = new Map();
|
|
32
|
+
for (const r of [...a, ...b]) {
|
|
33
|
+
byKey.set(`${(0, memory_graph_js_1.entityKey)(r.s)}|${(0, memory_graph_js_1.normalizePredicate)(r.p)}|${(0, memory_graph_js_1.entityKey)(r.o)}`, r);
|
|
34
|
+
}
|
|
35
|
+
return [...byKey.values()];
|
|
36
|
+
}
|
|
37
|
+
function unionNames(a = [], b = []) {
|
|
38
|
+
const byKey = new Map();
|
|
39
|
+
for (const n of [...a, ...b])
|
|
40
|
+
if (!byKey.has((0, memory_graph_js_1.entityKey)(n)))
|
|
41
|
+
byKey.set((0, memory_graph_js_1.entityKey)(n), n);
|
|
42
|
+
return [...byKey.values()];
|
|
43
|
+
}
|
|
23
44
|
const round3 = (n) => Math.round(n * 1000) / 1000;
|
|
24
45
|
class MemoryStore {
|
|
25
46
|
api;
|
|
@@ -156,17 +177,32 @@ class MemoryStore {
|
|
|
156
177
|
}
|
|
157
178
|
add(label, text, namespace, meta = {}) {
|
|
158
179
|
return this.enqueue(async () => {
|
|
180
|
+
(0, memory_secrets_js_1.assertNoSecret)(label, text, ...(meta.relations ?? []).map((r) => `${r.s} ${r.p} ${r.o}`));
|
|
159
181
|
await this.sync();
|
|
160
182
|
const key = this.requireSession();
|
|
161
183
|
const startEpoch = this.epoch;
|
|
184
|
+
meta = this.canonicalize(meta);
|
|
162
185
|
const embedding = await this.embedder.embed(embeddingInput(label, text));
|
|
163
186
|
const candidates = this.candidatesFor(namespace);
|
|
164
|
-
|
|
187
|
+
// A near-identical sentence that changes a single-valued fact ("Sam
|
|
188
|
+
// leads Atlas" after "Dana leads Atlas") is not a restatement: merging
|
|
189
|
+
// would overwrite the old fact and lose its history. It is stored as a
|
|
190
|
+
// new memory, which then ends the old one.
|
|
191
|
+
const found = (0, virlow_memory_1.findDuplicate)(embedding, candidates);
|
|
192
|
+
const contradicts = found !== null &&
|
|
193
|
+
found !== undefined &&
|
|
194
|
+
(meta.relations ?? []).some((r) => (0, memory_graph_js_1.supersededBy)((0, memory_graph_js_1.buildGraph)([this.cache.get(found.id)]), r, Date.now()).length > 0);
|
|
195
|
+
const hit = contradicts ? null : found;
|
|
165
196
|
if (hit) {
|
|
166
197
|
// Same memory said again: fold it into the existing note rather than
|
|
167
|
-
// accumulating near-identical copies. Its metadata carries forward
|
|
198
|
+
// accumulating near-identical copies. Its metadata carries forward,
|
|
199
|
+
// and its entities and relations gain whatever the new one names.
|
|
168
200
|
const existing = this.cache.get(hit.id);
|
|
169
201
|
const merged = (0, memory_note_js_1.mergeMeta)(existing.meta, meta);
|
|
202
|
+
if (meta.about)
|
|
203
|
+
merged.about = unionNames(existing.meta.about, meta.about);
|
|
204
|
+
if (meta.relations)
|
|
205
|
+
merged.relations = unionRelations(existing.meta.relations, meta.relations);
|
|
170
206
|
const row = await this.writeNote(key, hit.id, label, text, merged);
|
|
171
207
|
if (this.epoch === startEpoch) {
|
|
172
208
|
this.cache.set(hit.id, {
|
|
@@ -180,7 +216,7 @@ class MemoryStore {
|
|
|
180
216
|
this.embeddings?.set(hit.id, row.updatedAt, embedding);
|
|
181
217
|
await this.saveEmbeddings(key, startEpoch);
|
|
182
218
|
}
|
|
183
|
-
return { action: 'updated', id: hit.id, similar: [] };
|
|
219
|
+
return { action: 'updated', id: hit.id, similar: [], closed: [] };
|
|
184
220
|
}
|
|
185
221
|
const tree = await this.requireTree();
|
|
186
222
|
const folderId = namespace !== undefined
|
|
@@ -191,6 +227,8 @@ class MemoryStore {
|
|
|
191
227
|
source: meta.source ?? 'virlow-mcp',
|
|
192
228
|
created: meta.created ?? new Date().toISOString(),
|
|
193
229
|
};
|
|
230
|
+
// The graph as it stood before this memory, to find what it replaces.
|
|
231
|
+
const before = this.graph();
|
|
194
232
|
const row = await this.writeNote(key, null, label, text, stamped, folderId);
|
|
195
233
|
if (this.epoch === startEpoch) {
|
|
196
234
|
this.cache.set(row.id, {
|
|
@@ -216,26 +254,221 @@ class MemoryStore {
|
|
|
216
254
|
similarity: round3(similarity),
|
|
217
255
|
};
|
|
218
256
|
});
|
|
219
|
-
|
|
257
|
+
const closed = await this.closeSuperseded(key, startEpoch, before, row.id, stamped);
|
|
258
|
+
return { action: 'stored', id: row.id, similar, closed };
|
|
220
259
|
});
|
|
221
260
|
}
|
|
222
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Ends the facts a new memory replaces. Each superseded relation gets a
|
|
263
|
+
* `to` date (the new fact's start); a memory whose relations have all
|
|
264
|
+
* ended gets `valid_to` and `superseded_by`. Nothing is deleted, so "who
|
|
265
|
+
* led Atlas before Sam?" stays answerable.
|
|
266
|
+
*/
|
|
267
|
+
async closeSuperseded(key, startEpoch, graph, newId, meta) {
|
|
268
|
+
const closed = [];
|
|
269
|
+
const byMemory = new Map();
|
|
270
|
+
for (const relation of meta.relations ?? []) {
|
|
271
|
+
const start = relation.from ?? meta.validFrom ?? today();
|
|
272
|
+
const at = Date.parse(start.length === 10 ? `${start}T00:00:00Z` : start);
|
|
273
|
+
for (const edge of (0, memory_graph_js_1.supersededBy)(graph, relation, Number.isNaN(at) ? Date.now() : at)) {
|
|
274
|
+
if (edge.memoryId === newId)
|
|
275
|
+
continue;
|
|
276
|
+
const list = byMemory.get(edge.memoryId) ?? [];
|
|
277
|
+
list.push({ s: edge.s, p: edge.p, o: edge.o, ...(edge.from && { from: edge.from }), to: start });
|
|
278
|
+
byMemory.set(edge.memoryId, list);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const [memoryId, ended] of byMemory) {
|
|
282
|
+
const memory = this.cache.get(memoryId);
|
|
283
|
+
if (!memory)
|
|
284
|
+
continue;
|
|
285
|
+
const relations = (memory.meta.relations ?? []).map((r) => {
|
|
286
|
+
const hit = ended.find((e) => (0, memory_graph_js_1.entityKey)(e.s) === (0, memory_graph_js_1.entityKey)(r.s) &&
|
|
287
|
+
(0, memory_graph_js_1.normalizePredicate)(e.p) === (0, memory_graph_js_1.normalizePredicate)(r.p) &&
|
|
288
|
+
(0, memory_graph_js_1.entityKey)(e.o) === (0, memory_graph_js_1.entityKey)(r.o));
|
|
289
|
+
return hit ? { ...r, to: hit.to } : r;
|
|
290
|
+
});
|
|
291
|
+
const next = { ...memory.meta, relations };
|
|
292
|
+
if (relations.every((r) => r.to)) {
|
|
293
|
+
next.validTo = ended[0].to;
|
|
294
|
+
next.supersededBy = newId;
|
|
295
|
+
}
|
|
296
|
+
const row = await this.writeNote(key, memoryId, memory.label, memory.text, next);
|
|
297
|
+
if (this.epoch === startEpoch) {
|
|
298
|
+
this.cache.set(memoryId, { ...memory, meta: next, updatedAt: row.updatedAt });
|
|
299
|
+
this.embeddings?.set(memoryId, row.updatedAt, memory.embedding);
|
|
300
|
+
}
|
|
301
|
+
for (const relation of ended)
|
|
302
|
+
closed.push({ id: memoryId, label: memory.label, relation });
|
|
303
|
+
}
|
|
304
|
+
if (closed.length > 0)
|
|
305
|
+
await this.saveEmbeddings(key, startEpoch);
|
|
306
|
+
return closed;
|
|
307
|
+
}
|
|
308
|
+
/** The graph over what is cached now. Cheap: rebuilt from the notes on
|
|
309
|
+
* demand, never stored. */
|
|
310
|
+
graph(namespace) {
|
|
311
|
+
return (0, memory_graph_js_1.buildGraph)([...this.cache.values()].filter((m) => namespace === undefined || m.namespace === namespace || m.namespace === undefined));
|
|
312
|
+
}
|
|
313
|
+
/** New names spelled the way existing memories already spell them, and
|
|
314
|
+
* predicates in snake_case, so one entity does not split in two. */
|
|
315
|
+
canonicalize(meta) {
|
|
316
|
+
if (!meta.about && !meta.relations)
|
|
317
|
+
return meta;
|
|
318
|
+
const graph = this.graph();
|
|
319
|
+
const name = (n) => graph.entities.get((0, memory_graph_js_1.entityKey)(n))?.name ?? n.trim();
|
|
320
|
+
const next = { ...meta };
|
|
321
|
+
if (meta.about)
|
|
322
|
+
next.about = unionNames([], meta.about.map(name));
|
|
323
|
+
if (meta.relations) {
|
|
324
|
+
next.relations = meta.relations.map((r) => ({
|
|
325
|
+
...r,
|
|
326
|
+
s: name(r.s),
|
|
327
|
+
p: (0, memory_graph_js_1.normalizePredicate)(r.p),
|
|
328
|
+
o: graph.entities.has((0, memory_graph_js_1.entityKey)(r.o)) ? name(r.o) : r.o.trim(),
|
|
329
|
+
}));
|
|
330
|
+
// Everything a relation names is also what the memory is about.
|
|
331
|
+
next.about = unionNames(next.about, next.relations.flatMap((r) => [r.s, ...(graph.entities.has((0, memory_graph_js_1.entityKey)(r.o)) || /^[A-Z0-9]/.test(r.o) ? [r.o] : [])]));
|
|
332
|
+
}
|
|
333
|
+
return next;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Hybrid recall: similarity, then the graph (memories about an entity the
|
|
337
|
+
* query names, and one relation out from it) and recency. Current facts
|
|
338
|
+
* only, unless `includeHistory`, or as they stood on `asOf`.
|
|
339
|
+
*/
|
|
340
|
+
async search(query, namespace, limit = 10, opts = {}) {
|
|
223
341
|
await this.sync();
|
|
224
342
|
this.requireSession();
|
|
225
343
|
const embedding = await this.embedder.embed(query);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
344
|
+
const candidates = this.candidatesFor(namespace);
|
|
345
|
+
const scored = (0, virlow_memory_1.rankBySimilarity)(embedding, candidates, candidates.length).map(({ id, similarity }) => ({ memory: this.cache.get(id), similarity }));
|
|
346
|
+
const at = opts.asOf ? Date.parse(opts.asOf.length === 10 ? `${opts.asOf}T23:59:59Z` : opts.asOf) : undefined;
|
|
347
|
+
const ranked = (0, memory_graph_js_1.rankMemories)(this.graph(namespace), query, scored, {
|
|
348
|
+
...(at !== undefined && !Number.isNaN(at) && { at }),
|
|
349
|
+
includeHistory: opts.includeHistory === true,
|
|
350
|
+
limit,
|
|
351
|
+
});
|
|
352
|
+
return ranked.map(({ memory, similarity }) => ({
|
|
353
|
+
id: memory.id,
|
|
354
|
+
label: memory.label,
|
|
355
|
+
text: memory.text,
|
|
356
|
+
meta: memory.meta,
|
|
357
|
+
...namespaceField(memory.namespace),
|
|
358
|
+
similarity: round3(similarity),
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
/** Everything known about one entity: its current and past relations and
|
|
362
|
+
* the facts behind them. Null when no memory names it. */
|
|
363
|
+
async recallEntity(name, namespace) {
|
|
364
|
+
await this.sync();
|
|
365
|
+
this.requireSession();
|
|
366
|
+
const graph = this.graph(namespace);
|
|
367
|
+
const entity = graph.entities.get((0, memory_graph_js_1.entityKey)(name));
|
|
368
|
+
if (!entity)
|
|
369
|
+
return null;
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
const memories = [...entity.memoryIds]
|
|
372
|
+
.map((id) => this.cache.get(id))
|
|
373
|
+
.filter(Boolean)
|
|
374
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
375
|
+
return {
|
|
376
|
+
name: entity.name,
|
|
377
|
+
current: memories.filter((m) => (0, memory_graph_js_1.memoryHeldAt)(m, now)),
|
|
378
|
+
past: memories.filter((m) => !(0, memory_graph_js_1.memoryHeldAt)(m, now)),
|
|
379
|
+
relations: entity.edges.map((e) => ({ ...e })),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/** Adds entities and relations to an existing memory: how a memory saved
|
|
383
|
+
* before the graph existed gets linked. Also ends what it replaces. */
|
|
384
|
+
link(id, add) {
|
|
385
|
+
return this.enqueue(async () => {
|
|
386
|
+
await this.sync();
|
|
387
|
+
const key = this.requireSession();
|
|
388
|
+
const startEpoch = this.epoch;
|
|
389
|
+
const existing = this.cache.get(id);
|
|
390
|
+
if (!existing)
|
|
391
|
+
throw new Error(`Memory ${id} not found`);
|
|
392
|
+
(0, memory_secrets_js_1.assertNoSecret)(...(add.relations ?? []).map((r) => `${r.s} ${r.p} ${r.o}`));
|
|
393
|
+
const before = this.graph();
|
|
394
|
+
const extra = this.canonicalize({
|
|
395
|
+
...(add.about && { about: add.about }),
|
|
396
|
+
...(add.relations && { relations: add.relations }),
|
|
397
|
+
});
|
|
398
|
+
const meta = {
|
|
399
|
+
...existing.meta,
|
|
400
|
+
about: unionNames(existing.meta.about, extra.about),
|
|
401
|
+
relations: unionRelations(existing.meta.relations, extra.relations),
|
|
236
402
|
};
|
|
403
|
+
const row = await this.writeNote(key, id, existing.label, existing.text, meta);
|
|
404
|
+
if (this.epoch === startEpoch) {
|
|
405
|
+
this.cache.set(id, { ...existing, meta, updatedAt: row.updatedAt });
|
|
406
|
+
this.embeddings?.set(id, row.updatedAt, existing.embedding);
|
|
407
|
+
await this.saveEmbeddings(key, startEpoch);
|
|
408
|
+
}
|
|
409
|
+
return this.closeSuperseded(key, startEpoch, before, id, { relations: extra.relations ?? [] });
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/** One entity spelled two ways becomes one: every memory naming `from`
|
|
413
|
+
* is rewritten to name `into`. Returns how many memories changed. */
|
|
414
|
+
mergeEntities(from, into) {
|
|
415
|
+
return this.enqueue(async () => {
|
|
416
|
+
await this.sync();
|
|
417
|
+
const key = this.requireSession();
|
|
418
|
+
const startEpoch = this.epoch;
|
|
419
|
+
const fromKey = (0, memory_graph_js_1.entityKey)(from);
|
|
420
|
+
const swap = (n) => ((0, memory_graph_js_1.entityKey)(n) === fromKey ? into.trim() : n);
|
|
421
|
+
let changed = 0;
|
|
422
|
+
for (const memory of [...this.cache.values()]) {
|
|
423
|
+
const names = [...(memory.meta.about ?? []), ...(memory.meta.relations ?? []).flatMap((r) => [r.s, r.o])];
|
|
424
|
+
if (!names.some((n) => (0, memory_graph_js_1.entityKey)(n) === fromKey))
|
|
425
|
+
continue;
|
|
426
|
+
const meta = { ...memory.meta };
|
|
427
|
+
if (memory.meta.about)
|
|
428
|
+
meta.about = unionNames([], memory.meta.about.map(swap));
|
|
429
|
+
if (memory.meta.relations) {
|
|
430
|
+
meta.relations = unionRelations([], memory.meta.relations.map((r) => ({ ...r, s: swap(r.s), o: swap(r.o) })));
|
|
431
|
+
}
|
|
432
|
+
const row = await this.writeNote(key, memory.id, memory.label, memory.text, meta);
|
|
433
|
+
if (this.epoch === startEpoch) {
|
|
434
|
+
this.cache.set(memory.id, { ...memory, meta, updatedAt: row.updatedAt });
|
|
435
|
+
this.embeddings?.set(memory.id, row.updatedAt, memory.embedding);
|
|
436
|
+
}
|
|
437
|
+
changed++;
|
|
438
|
+
}
|
|
439
|
+
if (changed > 0)
|
|
440
|
+
await this.saveEmbeddings(key, startEpoch);
|
|
441
|
+
return changed;
|
|
237
442
|
});
|
|
238
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* What to load at the start of a session: the entities this namespace
|
|
446
|
+
* talks about most, and its most recent current facts, within `limit`
|
|
447
|
+
* facts. Global memories are included alongside a project's.
|
|
448
|
+
*/
|
|
449
|
+
async context(namespace, limit = 12) {
|
|
450
|
+
await this.sync();
|
|
451
|
+
this.requireSession();
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
const graph = this.graph(namespace);
|
|
454
|
+
const entities = [...graph.entities.values()]
|
|
455
|
+
.sort((a, b) => b.memoryIds.size - a.memoryIds.size)
|
|
456
|
+
.slice(0, 8)
|
|
457
|
+
.map((e) => ({ name: e.name, facts: e.memoryIds.size }));
|
|
458
|
+
const facts = this.list(namespace)
|
|
459
|
+
.filter((m) => (0, memory_graph_js_1.memoryHeldAt)(m, now))
|
|
460
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
461
|
+
.slice(0, limit);
|
|
462
|
+
return { entities, facts };
|
|
463
|
+
}
|
|
464
|
+
/** Findings for a tidy pass: unlinked memories, likely duplicate names and
|
|
465
|
+
* single-valued facts holding two values at once. */
|
|
466
|
+
async health(namespace) {
|
|
467
|
+
await this.sync();
|
|
468
|
+
this.requireSession();
|
|
469
|
+
const memories = this.list(namespace);
|
|
470
|
+
return (0, memory_graph_js_1.graphHealth)(this.graph(namespace), memories);
|
|
471
|
+
}
|
|
239
472
|
/** Requires a prior sync() in the same unlock to have populated the cache. */
|
|
240
473
|
list(namespace) {
|
|
241
474
|
this.requireSession();
|
|
@@ -255,6 +488,7 @@ class MemoryStore {
|
|
|
255
488
|
throw new Error(`Memory ${id} not found`);
|
|
256
489
|
const label = changes.label ?? existing.label;
|
|
257
490
|
const text = changes.text ?? existing.text;
|
|
491
|
+
(0, memory_secrets_js_1.assertNoSecret)(label, text);
|
|
258
492
|
const meta = (0, memory_note_js_1.mergeMeta)(existing.meta, changes.meta ?? {});
|
|
259
493
|
const row = await this.writeNote(key, id, label, text, meta);
|
|
260
494
|
const embedding = await this.embedder.embed(embeddingInput(label, text));
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { MemoryMeta, MemoryRelation } from './memory-note.js';
|
|
2
|
+
/**
|
|
3
|
+
* The memory graph, built from memories rather than stored beside them.
|
|
4
|
+
*
|
|
5
|
+
* A memory's frontmatter names the entities it is about (`about`) and the
|
|
6
|
+
* relations it asserts (`relations`). This module turns a set of memories
|
|
7
|
+
* into entities and edges, answers "is this still true" for a date, and
|
|
8
|
+
* ranks recall with the graph and recency as well as similarity. Pure: no
|
|
9
|
+
* network, no crypto, so every rule here is tested directly.
|
|
10
|
+
*
|
|
11
|
+
* Entities are names. Two mentions are the same entity when their names
|
|
12
|
+
* match ignoring case, spacing and a leading "the"; anything looser (Dana
|
|
13
|
+
* and Dana Whitfield) is a merge the AI or the person asks for, never a
|
|
14
|
+
* guess. See docs/memory-graph.md.
|
|
15
|
+
*/
|
|
16
|
+
export interface GraphMemory {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
text: string;
|
|
20
|
+
meta: MemoryMeta;
|
|
21
|
+
namespace?: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface Edge extends MemoryRelation {
|
|
25
|
+
/** The memory that asserted it. */
|
|
26
|
+
memoryId: string;
|
|
27
|
+
}
|
|
28
|
+
export interface Entity {
|
|
29
|
+
key: string;
|
|
30
|
+
/** The spelling most memories use. */
|
|
31
|
+
name: string;
|
|
32
|
+
memoryIds: Set<string>;
|
|
33
|
+
edges: Edge[];
|
|
34
|
+
}
|
|
35
|
+
export interface MemoryGraph {
|
|
36
|
+
entities: Map<string, Entity>;
|
|
37
|
+
edges: Edge[];
|
|
38
|
+
}
|
|
39
|
+
/** The matching key for a name: case, spacing and a leading article do not
|
|
40
|
+
* make a different entity. */
|
|
41
|
+
export declare function entityKey(name: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Predicates where one side holds one value at a time, so a new fact ends
|
|
44
|
+
* the old one. Which side matters:
|
|
45
|
+
* - one object per subject: a person works at one place, reports to one
|
|
46
|
+
* manager (Dana works_at Acme, then Dana works_at Initech);
|
|
47
|
+
* - one subject per object: a project has one lead (Dana leads Atlas, then
|
|
48
|
+
* Sam leads Atlas).
|
|
49
|
+
* Anything else accumulates (prefers, uses, knows).
|
|
50
|
+
*/
|
|
51
|
+
export declare const ONE_OBJECT_PER_SUBJECT: Set<string>;
|
|
52
|
+
export declare const ONE_SUBJECT_PER_OBJECT: Set<string>;
|
|
53
|
+
/** Either kind: a predicate that holds one value at a time. */
|
|
54
|
+
export declare const SINGLE_VALUED: Set<string>;
|
|
55
|
+
/** snake_case, so "Works at" and "works_at" are one predicate. */
|
|
56
|
+
export declare function normalizePredicate(p: string): string;
|
|
57
|
+
/** Whether a span held at `at`: started on or before it, ended after it. */
|
|
58
|
+
export declare function heldAt(from: string | undefined, to: string | undefined, at: number): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a memory's fact held at `at`. It starts at `valid_from`, else the
|
|
61
|
+
* earliest date its relations give (a fact about "since January" learned
|
|
62
|
+
* today started in January), else when it was learned.
|
|
63
|
+
*/
|
|
64
|
+
export declare function memoryHeldAt(memory: GraphMemory, at: number): boolean;
|
|
65
|
+
export declare function buildGraph(memories: Iterable<GraphMemory>): MemoryGraph;
|
|
66
|
+
/** Entities named in a piece of text (a prompt, a query), longest names
|
|
67
|
+
* first so "Dana Whitfield" wins over "Dana". Whole words only. */
|
|
68
|
+
export declare function entitiesMentioned(graph: MemoryGraph, text: string): Entity[];
|
|
69
|
+
/** Entities one relation away from any of `from`. */
|
|
70
|
+
export declare function neighbours(graph: MemoryGraph, from: Entity[]): Entity[];
|
|
71
|
+
export interface RankInput {
|
|
72
|
+
memory: GraphMemory;
|
|
73
|
+
similarity: number;
|
|
74
|
+
}
|
|
75
|
+
/** How much the graph and recency move a similarity score. Small on
|
|
76
|
+
* purpose: similarity still decides, the graph breaks ties and pulls in
|
|
77
|
+
* facts that share an entity with the query but none of its words. */
|
|
78
|
+
export declare const DIRECT_BOOST = 0.15;
|
|
79
|
+
export declare const NEIGHBOUR_BOOST = 0.07;
|
|
80
|
+
export declare function rankMemories(graph: MemoryGraph, query: string, scored: RankInput[], opts: {
|
|
81
|
+
at?: number;
|
|
82
|
+
includeHistory?: boolean;
|
|
83
|
+
limit: number;
|
|
84
|
+
}): Array<RankInput & {
|
|
85
|
+
score: number;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Current edges a new relation replaces: same subject and single-valued
|
|
89
|
+
* predicate, a different object, still holding when the new one starts.
|
|
90
|
+
*/
|
|
91
|
+
export declare function supersededBy(graph: MemoryGraph, relation: MemoryRelation, at: number): Edge[];
|
|
92
|
+
/**
|
|
93
|
+
* Housekeeping findings for a tidy pass: memories linked to nothing, names
|
|
94
|
+
* that look like one entity spelled two ways, and single-valued predicates
|
|
95
|
+
* holding two values at once.
|
|
96
|
+
*/
|
|
97
|
+
export declare function graphHealth(graph: MemoryGraph, memories: GraphMemory[], at?: number): {
|
|
98
|
+
unlinked: string[];
|
|
99
|
+
possibleDuplicates: [string, string][];
|
|
100
|
+
contradictions: {
|
|
101
|
+
subject: string;
|
|
102
|
+
predicate: string;
|
|
103
|
+
objects: string[];
|
|
104
|
+
}[];
|
|
105
|
+
};
|