mnemocyte 0.1.0 → 0.1.2

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/dist/index.mjs CHANGED
@@ -24,12 +24,16 @@ const vector = customType({
24
24
  return `vector(${typeof config === "number" ? config : 1536})`;
25
25
  },
26
26
  toDriver(value) {
27
- return `[${value.join(",")}]`;
27
+ return `[${value.map(formatVectorComponent$1).join(",")}]`;
28
28
  },
29
29
  fromDriver(value) {
30
30
  return value.slice(1, -1).split(",").filter(Boolean).map(Number);
31
31
  }
32
32
  });
33
+ function formatVectorComponent$1(value) {
34
+ if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
35
+ return Object.is(value, -0) ? "0" : value.toFixed(17);
36
+ }
33
37
  const memoriesTable = pgTable("mnemocyte_memories", {
34
38
  id: text("id").primaryKey(),
35
39
  entityId: text("entity_id").notNull(),
@@ -43,7 +47,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
43
47
  embedding: vector("embedding", { dimensions: 1536 }),
44
48
  embeddingModel: text("embedding_model").notNull(),
45
49
  embeddingDimensions: integer("embedding_dimensions").notNull(),
46
- supersededBy: text("superseded_by"),
50
+ supersededBy: text("superseded_by").references(() => memoriesTable.id),
47
51
  supersededAt: timestamp("superseded_at", { withTimezone: true }),
48
52
  expiresAt: timestamp("expires_at", { withTimezone: true }),
49
53
  lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
@@ -138,7 +142,11 @@ function formatMetadata(memory) {
138
142
  if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
139
143
  return parts.join("; ");
140
144
  }
141
- function formatMarkdown(memories, query) {
145
+ function markdownFence(value) {
146
+ const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
147
+ return "`".repeat(Math.max(3, longestBacktickRun + 1));
148
+ }
149
+ function formatMarkdown(memories, query, omittedCount) {
142
150
  const lines = [
143
151
  "# Memory Context",
144
152
  "",
@@ -146,10 +154,14 @@ function formatMarkdown(memories, query) {
146
154
  "",
147
155
  "## Relevant Memories"
148
156
  ];
149
- for (const memory of memories) lines.push("", `- ${memory.content}`, ` - ${formatMetadata(memory)}`);
157
+ for (const memory of memories) {
158
+ const fence = markdownFence(memory.content);
159
+ lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
160
+ }
161
+ if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
150
162
  return lines.join("\n");
151
163
  }
152
- function formatPlain(memories, query) {
164
+ function formatPlain(memories, query, omittedCount) {
153
165
  const lines = [
154
166
  "MEMORY CONTEXT",
155
167
  "",
@@ -157,20 +169,22 @@ function formatPlain(memories, query) {
157
169
  "",
158
170
  "RELEVANT MEMORIES"
159
171
  ];
160
- for (const memory of memories) lines.push("", memory.content, formatMetadata(memory));
172
+ for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
173
+ if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
161
174
  return lines.join("\n");
162
175
  }
163
- function formatXml(memories, query) {
176
+ function formatXml(memories, query, omittedCount) {
164
177
  const lines = [`<memory_context query="${escapeXml(query)}">`];
165
178
  for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
179
+ if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
166
180
  lines.push("</memory_context>");
167
181
  return lines.join("\n");
168
182
  }
169
- function formatContext(memories, query, format) {
183
+ function formatContext(memories, query, format, omittedCount = 0) {
170
184
  switch (format) {
171
- case "markdown": return formatMarkdown(memories, query);
172
- case "plain": return formatPlain(memories, query);
173
- case "xml": return formatXml(memories, query);
185
+ case "markdown": return formatMarkdown(memories, query, omittedCount);
186
+ case "plain": return formatPlain(memories, query, omittedCount);
187
+ case "xml": return formatXml(memories, query, omittedCount);
174
188
  }
175
189
  }
176
190
  //#endregion
@@ -199,7 +213,21 @@ async function buildContext(options) {
199
213
  const format = options.input.format ?? "markdown";
200
214
  const maxTokens = options.input.maxTokens ?? 1200;
201
215
  const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
202
- return trimToTokenBudget(formatContext(await options.recall(options.input), options.input.query, format), maxTokens, tokenCounter);
216
+ const memories = await options.recall(options.input);
217
+ let low = 0;
218
+ let high = memories.length;
219
+ let bestContext = "";
220
+ while (low <= high) {
221
+ const mid = Math.floor((low + high) / 2);
222
+ const omittedCount = memories.length - mid;
223
+ const context = formatContext(memories.slice(0, mid), options.input.query, format, omittedCount);
224
+ if (tokenCounter.count(context) <= maxTokens) {
225
+ bestContext = context;
226
+ low = mid + 1;
227
+ } else high = mid - 1;
228
+ }
229
+ if (bestContext || memories.length === 0) return bestContext;
230
+ return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
203
231
  }
204
232
  //#endregion
205
233
  //#region src/observability.ts
@@ -499,6 +527,22 @@ async function embedOne(embedder, text, options = {}) {
499
527
  if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
500
528
  return embedding;
501
529
  }
530
+ async function embedMany(embedder, texts, options = {}) {
531
+ if (texts.length === 0) return [];
532
+ let embeddings;
533
+ try {
534
+ embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed(texts) : embedder.embed(texts, { signal }), {
535
+ ...options.signal === void 0 ? {} : { signal: options.signal },
536
+ ...options.resilience === void 0 ? {} : { resilience: options.resilience }
537
+ });
538
+ } catch (error) {
539
+ if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
540
+ throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
541
+ }
542
+ if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
543
+ for (const embedding of embeddings) if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
544
+ return embeddings;
545
+ }
502
546
  function validateRememberInput(input) {
503
547
  assertNonEmptyString(input.entityId, "entityId");
504
548
  assertNonEmptyString(input.content, "content");
@@ -778,8 +822,45 @@ function createInMemoryClient(config) {
778
822
  async rememberMany(inputs) {
779
823
  return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
780
824
  assertOpen();
825
+ if (inputs.length === 0) return [];
826
+ for (const input of inputs) validateRememberInput(input);
827
+ const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
828
+ ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
829
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
830
+ });
831
+ const now = /* @__PURE__ */ new Date();
781
832
  const result = [];
782
- for (const input of inputs) result.push(await remember(input));
833
+ for (let i = 0; i < inputs.length; i++) {
834
+ const input = inputs[i];
835
+ const memory = {
836
+ id: createId(),
837
+ entityId: input.entityId,
838
+ content: input.content,
839
+ type: input.type ?? "fact",
840
+ importance: input.importance ?? "normal",
841
+ tags: normalizeTags(input.tags),
842
+ source: input.source ?? null,
843
+ metadata: input.metadata ?? {},
844
+ confidence: input.confidence ?? 1,
845
+ embeddingModel: config.embedder.model,
846
+ embeddingDimensions: config.embedder.dimensions,
847
+ supersededBy: null,
848
+ supersededAt: null,
849
+ expiresAt: input.expiresAt ?? null,
850
+ lastAccessedAt: null,
851
+ accessCount: 0,
852
+ createdAt: now,
853
+ updatedAt: now,
854
+ embedding: embeddings[i]
855
+ };
856
+ memories.set(memory.id, memory);
857
+ recordAudit(memory.entityId, "memory.created", {
858
+ memoryId: memory.id,
859
+ type: memory.type,
860
+ importance: memory.importance
861
+ });
862
+ result.push(cloneMemory(memory));
863
+ }
783
864
  return result;
784
865
  }, (result) => ({ count: result.length }));
785
866
  },
@@ -1009,16 +1090,24 @@ async function listEvents(db, filter) {
1009
1090
  //#endregion
1010
1091
  //#region src/db/queries/memories.ts
1011
1092
  function vectorLiteral(embedding) {
1012
- return `[${embedding.join(",")}]`;
1093
+ return `[${embedding.map(formatVectorComponent).join(",")}]`;
1094
+ }
1095
+ function formatVectorComponent(value) {
1096
+ if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
1097
+ return Object.is(value, -0) ? "0" : value.toFixed(17);
1013
1098
  }
1014
1099
  function filterConditions(filter) {
1015
- return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} && ${filter.tags}` : void 0, filter.before ? lt(memoriesTable.createdAt, filter.before) : void 0, filter.after ? gt(memoriesTable.createdAt, filter.after) : void 0);
1100
+ return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} @> ${filter.tags}` : void 0, filter.before ? lt(memoriesTable.createdAt, filter.before) : void 0, filter.after ? gt(memoriesTable.createdAt, filter.after) : void 0);
1016
1101
  }
1017
1102
  async function insertMemory(db, row) {
1018
1103
  const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
1019
1104
  if (!inserted) throw new Error("Memory insert returned no rows.");
1020
1105
  return inserted;
1021
1106
  }
1107
+ async function insertMemories(db, rows) {
1108
+ if (rows.length === 0) return [];
1109
+ return db.insert(memoriesTable).values(rows).returning();
1110
+ }
1022
1111
  async function getMemoryById(db, entityId, memoryId) {
1023
1112
  return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
1024
1113
  }
@@ -1210,7 +1299,7 @@ async function vectorSearch(db, input) {
1210
1299
  ${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
1211
1300
  ${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
1212
1301
  ${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
1213
- ${input.tags && input.tags.length > 0 ? sql`AND tags && ${input.tags}` : sql``}
1302
+ ${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
1214
1303
  ${input.before ? sql`AND created_at < ${input.before}` : sql``}
1215
1304
  ${input.after ? sql`AND created_at > ${input.after}` : sql``}
1216
1305
  AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
@@ -1247,7 +1336,7 @@ async function lexicalSearch(db, input) {
1247
1336
  ${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
1248
1337
  ${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
1249
1338
  ${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
1250
- ${input.tags && input.tags.length > 0 ? sql`AND tags && ${input.tags}` : sql``}
1339
+ ${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
1251
1340
  ${input.before ? sql`AND created_at < ${input.before}` : sql``}
1252
1341
  ${input.after ? sql`AND created_at > ${input.after}` : sql``}
1253
1342
  AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
@@ -1277,16 +1366,20 @@ async function hybridRecall(input) {
1277
1366
  for (const row of vectorRows) merged.set(row.id, {
1278
1367
  row,
1279
1368
  vectorScore: row.vectorScore,
1280
- lexicalScore: 0
1369
+ lexicalScore: lexicalScore(row.content, input.input.query)
1281
1370
  });
1282
1371
  for (const row of lexicalRows) {
1283
1372
  const existing = merged.get(row.id);
1284
1373
  if (existing) existing.lexicalScore = row.lexicalScore;
1285
- else merged.set(row.id, {
1286
- row,
1287
- vectorScore: 0,
1288
- lexicalScore: row.lexicalScore
1289
- });
1374
+ else {
1375
+ const embedding = row.embedding;
1376
+ const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
1377
+ merged.set(row.id, {
1378
+ row,
1379
+ vectorScore: Math.max(0, sim),
1380
+ lexicalScore: row.lexicalScore
1381
+ });
1382
+ }
1290
1383
  }
1291
1384
  const scored = Array.from(merged.values()).map((entry) => toScoredMemory(rowToMemory(entry.row), Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input.input, input.retrieval)).filter((memory) => memory.score >= input.minScore).sort((a, b) => b.score - a.score).slice(0, input.limit);
1292
1385
  await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
@@ -1387,9 +1480,41 @@ function createPostgresClient(config, handle) {
1387
1480
  async rememberMany(inputs) {
1388
1481
  return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
1389
1482
  assertOpen();
1390
- const result = [];
1391
- for (const input of inputs) result.push(await remember(input));
1392
- return result;
1483
+ if (inputs.length === 0) return [];
1484
+ for (const input of inputs) validateRememberInput(input);
1485
+ const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
1486
+ ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
1487
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
1488
+ });
1489
+ const now = /* @__PURE__ */ new Date();
1490
+ const rows = inputs.map((input, idx) => ({
1491
+ id: createId(),
1492
+ entityId: input.entityId,
1493
+ content: input.content,
1494
+ type: input.type ?? "fact",
1495
+ importance: input.importance ?? "normal",
1496
+ tags: normalizeTags(input.tags),
1497
+ source: input.source ?? null,
1498
+ metadata: input.metadata ?? {},
1499
+ confidence: input.confidence ?? 1,
1500
+ embedding: embeddings[idx],
1501
+ embeddingModel: config.embedder.model,
1502
+ embeddingDimensions: config.embedder.dimensions,
1503
+ supersededBy: null,
1504
+ supersededAt: null,
1505
+ expiresAt: input.expiresAt ?? null,
1506
+ lastAccessedAt: null,
1507
+ accessCount: 0,
1508
+ createdAt: now,
1509
+ updatedAt: now
1510
+ }));
1511
+ const memories = (await insertMemories(handle.db, rows)).map(rowToMemory);
1512
+ for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
1513
+ memoryId: memory.id,
1514
+ type: memory.type,
1515
+ importance: memory.importance
1516
+ });
1517
+ return memories;
1393
1518
  }, (result) => ({ count: result.length }));
1394
1519
  },
1395
1520
  async recall(input) {
@@ -1557,27 +1682,39 @@ function createPostgresClient(config, handle) {
1557
1682
  supersededIds: []
1558
1683
  };
1559
1684
  const now = /* @__PURE__ */ new Date();
1560
- const updated = await markMemoriesSuperseded(handle.db, {
1561
- survivorId: survivor.id,
1562
- entityId: input.entityId,
1563
- ids: losers.map((loser) => loser.id),
1564
- now
1565
- });
1566
- const newSupersededIds = updated.map((row) => row.id);
1567
- for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
1568
- memoryId: id,
1569
- supersededBy: survivor.id
1570
- });
1571
- if (input.mergeTags !== false && updated.length > 0) {
1572
- const mergedTags = new Set(survivor.tags);
1573
- for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1574
- if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
1685
+ const { newSupersededIds } = await handle.db.transaction(async (tx) => {
1686
+ const updated = await markMemoriesSuperseded(tx, {
1687
+ survivorId: survivor.id,
1575
1688
  entityId: input.entityId,
1576
- memoryId: survivor.id,
1577
- tags: [...mergedTags],
1689
+ ids: losers.map((loser) => loser.id),
1578
1690
  now
1579
1691
  });
1580
- }
1692
+ const newSupersededIds = updated.map((row) => row.id);
1693
+ for (const id of newSupersededIds) await insertEvent(tx, {
1694
+ id: createEventId(),
1695
+ entityId: input.entityId,
1696
+ description: "memory.superseded",
1697
+ metadata: {
1698
+ memoryId: id,
1699
+ supersededBy: survivor.id
1700
+ },
1701
+ timestamp: now
1702
+ });
1703
+ if (input.mergeTags !== false && updated.length > 0) {
1704
+ const mergedTags = new Set(survivor.tags);
1705
+ for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1706
+ if (mergedTags.size !== survivor.tags.length) await setMemoryTags(tx, {
1707
+ entityId: input.entityId,
1708
+ memoryId: survivor.id,
1709
+ tags: [...mergedTags],
1710
+ now
1711
+ });
1712
+ }
1713
+ return {
1714
+ updated,
1715
+ newSupersededIds
1716
+ };
1717
+ });
1581
1718
  return {
1582
1719
  survivorId: survivor.id,
1583
1720
  supersededCount: newSupersededIds.length,