@anuma/sdk 2.1.0 → 2.2.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.
@@ -53,6 +53,7 @@ import {
53
53
  getConversationsLazyOp,
54
54
  getConversationsOp,
55
55
  getEncryptionKey,
56
+ getMemoriesNeedingTopicExtractionOp,
56
57
  getMessagesOp,
57
58
  getUnfiledVaultMemoriesOp,
58
59
  getVaultMemoryOp,
@@ -66,6 +67,7 @@ import {
66
67
  onKeyAvailable,
67
68
  preEmbedVaultMemories,
68
69
  recall,
70
+ replaceMemoryEntitiesGuardedOp,
69
71
  requestEncryptionKey,
70
72
  requestKeyPair,
71
73
  require_QueryDescription,
@@ -85,6 +87,7 @@ import {
85
87
  searchMessagesOp,
86
88
  searchVaultMemories,
87
89
  shouldChunkMessage,
90
+ stampTopicsExtractedAtOp,
88
91
  stripToolCalls,
89
92
  updateConversationProjectOp,
90
93
  updateConversationTitleOp,
@@ -94,8 +97,9 @@ import {
94
97
  updateMessageFeedbackOp,
95
98
  updateVaultMemoryEmbeddingOp,
96
99
  updateVaultMemoryOp,
97
- upsertMessageOp
98
- } from "./chunk-45GOZ3AB.mjs";
100
+ upsertMessageOp,
101
+ vaultMemoryToStored
102
+ } from "./chunk-KEMPJE6C.mjs";
99
103
  import {
100
104
  createStreamingDeAnonymizer,
101
105
  resolvePiiRedactor
@@ -1086,6 +1090,9 @@ __decorateClass([
1086
1090
  __decorateClass([
1087
1091
  (0, import_decorators6.field)("topics_user_managed")
1088
1092
  ], VaultMemory.prototype, "topicsUserManaged", 2);
1093
+ __decorateClass([
1094
+ (0, import_decorators6.field)("topics_extracted_at")
1095
+ ], VaultMemory.prototype, "topicsExtractedAt", 2);
1089
1096
  __decorateClass([
1090
1097
  import_decorators6.readonly,
1091
1098
  (0, import_decorators6.date)("created_at")
@@ -1233,7 +1240,7 @@ __decorateClass([
1233
1240
  ], VaultFolder.prototype, "context", 2);
1234
1241
 
1235
1242
  // src/lib/db/schema.ts
1236
- var SDK_SCHEMA_VERSION = 35;
1243
+ var SDK_SCHEMA_VERSION = 36;
1237
1244
  var sdkSchema = (0, import_watermelondb12.appSchema)({
1238
1245
  version: SDK_SCHEMA_VERSION,
1239
1246
  tables: [
@@ -1354,7 +1361,11 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
1354
1361
  // When true, the user has manually set this memory's topics (entity
1355
1362
  // links). Auto-extraction then leaves its links alone — the user owns
1356
1363
  // them. Null/false = topics are auto-derived (default).
1357
- { name: "topics_user_managed", type: "boolean", isOptional: true }
1364
+ { name: "topics_user_managed", type: "boolean", isOptional: true },
1365
+ // Unix ms of the last LLM topic-extraction pass over this memory's
1366
+ // content. Null = never extracted standalone (legacy rows with entity
1367
+ // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
1368
+ { name: "topics_extracted_at", type: "number", isOptional: true }
1358
1369
  ]
1359
1370
  }),
1360
1371
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -1952,6 +1963,19 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
1952
1963
  ]
1953
1964
  })
1954
1965
  ]
1966
+ },
1967
+ // v35 -> v36: topics_extracted_at watermark on memory_vault. Existing rows
1968
+ // keep it NULL — rows that already have entity links are grandfathered as
1969
+ // extracted (no mass re-extraction on upgrade), rows without links are the
1970
+ // backfill target. See getMemoriesNeedingTopicExtractionOp.
1971
+ {
1972
+ toVersion: 36,
1973
+ steps: [
1974
+ (0, import_migrations.addColumns)({
1975
+ table: "memory_vault",
1976
+ columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
1977
+ })
1978
+ ]
1955
1979
  }
1956
1980
  ]
1957
1981
  });
@@ -4839,7 +4863,7 @@ async function isMemoryTopicsUserManaged(ctx, memoryId) {
4839
4863
  return true;
4840
4864
  }
4841
4865
  }
4842
- var DEFAULT_MODEL = "gpt-oss/gpt-oss-120b";
4866
+ var DEFAULT_EXTRACTION_MODEL = "gpt-oss/gpt-oss-120b";
4843
4867
  var DEFAULT_MIN_CONFIDENCE = 0.7;
4844
4868
  var MAX_CONTENT_LENGTH = 200;
4845
4869
  var MIN_CONTENT_LENGTH = 3;
@@ -4852,6 +4876,16 @@ var FACT_TYPES = [
4852
4876
  "constraint",
4853
4877
  "other"
4854
4878
  ];
4879
+ var ENTITY_KIND_GUIDELINES = `Choose the MOST SPECIFIC kind:
4880
+ - person: a named individual ("Sara", "Dr. Lee")
4881
+ - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
4882
+ - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
4883
+ - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
4884
+ - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
4885
+ - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
4886
+ - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
4887
+ - other: a named entity that genuinely fits none of the above \u2014 use sparingly
4888
+ Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits.`;
4855
4889
  var SYSTEM_PROMPT = `You extract durable user facts from conversations for a personal memory system.
4856
4890
 
4857
4891
  A "durable fact" is something the user shared about themselves that should still be true in future conversations. Examples that ARE durable:
@@ -4880,16 +4914,7 @@ For each durable fact, output:
4880
4914
  - type: one of identity | preference | relationship | plan | ongoing_context | constraint | other
4881
4915
  - confidence: 0.0-1.0; how sure you are this is durable AND true. Only include facts >= 0.7.
4882
4916
  - sourceMessageIds: which message IDs from the conversation contained the evidence
4883
- - entities: named entities mentioned, each as an object { "name": string, "kind": one of "person" | "organization" | "place" | "event" | "product" | "thing" | "concept" | "other" }. Optional \u2014 include only NAMED entities, skip generic/common nouns. Choose the MOST SPECIFIC kind:
4884
- - person: a named individual ("Sara", "Dr. Lee")
4885
- - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
4886
- - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
4887
- - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
4888
- - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
4889
- - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
4890
- - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
4891
- - other: a named entity that genuinely fits none of the above \u2014 use sparingly
4892
- Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits. Examples:
4917
+ - entities: named entities mentioned, each as an object { "name": string, "kind": one of "person" | "organization" | "place" | "event" | "product" | "thing" | "concept" | "other" }. Optional \u2014 include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES} Examples:
4893
4918
  - "started a new job at Hollowpoint Labs and I'm learning Haskell for it" \u2192 entities: [{ "name": "Hollowpoint Labs", "kind": "organization" }, { "name": "Haskell", "kind": "concept" }]
4894
4919
  - "running the Berlin Half in May and I track workouts in Whoop" \u2192 entities: [{ "name": "Berlin Half", "kind": "event" }, { "name": "Whoop", "kind": "product" }]
4895
4920
  - eventTime: when the event in this fact occurs/occurred. Resolve relative phrases ("yesterday", "next week") against the current date given in the user message. Output one of:
@@ -4915,7 +4940,7 @@ async function extractFacts(messages, options) {
4915
4940
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
4916
4941
  ...options.getToken !== void 0 && { getToken: options.getToken },
4917
4942
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
4918
- model: options.model ?? DEFAULT_MODEL,
4943
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
4919
4944
  systemPrompt: SYSTEM_PROMPT,
4920
4945
  userMessage: `Today's date is ${today}. Resolve any relative dates ("yesterday", "next week") against it.
4921
4946
 
@@ -5009,7 +5034,9 @@ async function extractAndRetain(messages, retainCtx, options) {
5009
5034
  if (options.entityCtx && candidate.entities.length > 0) {
5010
5035
  try {
5011
5036
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
5012
- await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities);
5037
+ await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
5038
+ unlessTopicsUserManaged: true
5039
+ });
5013
5040
  }
5014
5041
  } catch (err) {
5015
5042
  log.warn("[memory/extract] linkMemoryEntitiesOp failed", err);
@@ -5404,7 +5431,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
5404
5431
 
5405
5432
  // src/lib/memory/reflect.ts
5406
5433
  var DEFAULT_BASE_URL = BASE_URL;
5407
- var DEFAULT_MODEL2 = "anthropic/claude-sonnet-4-6";
5434
+ var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
5408
5435
  var DEFAULT_MAX_TOKENS = 4096;
5409
5436
  var REQUEST_TIMEOUT_MS = 6e4;
5410
5437
  var DEFAULT_SYSTEM_PROMPT = `You are a personal assistant with access to the user's memory. Answer the user's question using the supplied memories as evidence.
@@ -5436,7 +5463,7 @@ async function reflect(query, ctx, options) {
5436
5463
  const evidence = recalled.memories.map((m, i) => `[${i + 1}] (id: ${m.id}, kind: ${m.kind})
5437
5464
  ${m.content}`).join("\n\n");
5438
5465
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
5439
- const model = options.llmModel ?? DEFAULT_MODEL2;
5466
+ const model = options.llmModel ?? DEFAULT_MODEL;
5440
5467
  const wantsStructured = !!options.responseSchema;
5441
5468
  const sendResponseFormat = wantsStructured && supportsResponseFormat(model, "json_schema");
5442
5469
  const basePrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -5546,6 +5573,163 @@ function parseAnswer(body, base, parseSchema) {
5546
5573
  };
5547
5574
  }
5548
5575
 
5576
+ // src/lib/memory/topicExtract.ts
5577
+ var TOPIC_EXTRACTION_BATCH_SIZE = 10;
5578
+ var MAX_CHARS_PER_MEMORY = 300;
5579
+ var MAX_VOCABULARY_NAMES = 100;
5580
+ var SYSTEM_PROMPT2 = `You assign topics to saved memories for a personal memory system.
5581
+
5582
+ Each memory is a short statement about the user. For each memory, list the NAMED entities it mentions \u2014 these become the memory's topics, used to connect related memories in a knowledge graph.
5583
+
5584
+ Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
5585
+
5586
+ Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
5587
+ - exactly one element per input memory, echoing its id verbatim
5588
+ - "entities" may be empty when a memory mentions no named entities \u2014 most short memories have 0-3
5589
+ No prose.`;
5590
+ async function extractEntitiesForMemories(memories, options) {
5591
+ const out = /* @__PURE__ */ new Map();
5592
+ if (memories.length === 0) return out;
5593
+ const redactor = resolvePiiRedactor(options.piiRedaction);
5594
+ const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
5595
+ const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
5596
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
5597
+
5598
+ ` : "";
5599
+ for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
5600
+ const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
5601
+ const listing = batch.map((m) => {
5602
+ const content = m.content.slice(0, MAX_CHARS_PER_MEMORY);
5603
+ return `[${m.id}] ${redactor ? redactor.redactText(content).text : content}`;
5604
+ }).join("\n");
5605
+ const parsed = await callPortalJsonCompletion({
5606
+ ...options.apiKey !== void 0 && { apiKey: options.apiKey },
5607
+ ...options.getToken !== void 0 && { getToken: options.getToken },
5608
+ ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
5609
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
5610
+ systemPrompt: SYSTEM_PROMPT2,
5611
+ userMessage: `${vocabularyNote}Memories:
5612
+ ${listing}
5613
+
5614
+ List each memory's named entities.`,
5615
+ tag: "memory/topics",
5616
+ ...options.fetchFn && { fetchFn: options.fetchFn },
5617
+ ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
5618
+ ...options.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
5619
+ ...options.totalTimeoutMs !== void 0 && { totalTimeoutMs: options.totalTimeoutMs },
5620
+ ...options.backoffMs && { backoffMs: options.backoffMs }
5621
+ });
5622
+ if (parsed === null) {
5623
+ getLogger().warn(
5624
+ `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
5625
+ );
5626
+ continue;
5627
+ }
5628
+ const validIds = new Set(batch.map((m) => m.id));
5629
+ const byId = parseTopicResponse(parsed, validIds);
5630
+ if (byId === null) {
5631
+ getLogger().warn(
5632
+ `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
5633
+ );
5634
+ continue;
5635
+ }
5636
+ for (const m of batch) {
5637
+ const entities = byId.get(m.id);
5638
+ if (entities === void 0) {
5639
+ continue;
5640
+ }
5641
+ out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
5642
+ }
5643
+ }
5644
+ return out;
5645
+ }
5646
+ function restoreEntities(entities, redactor) {
5647
+ return entities.map((e) => ({ kind: e.kind, restored: redactor.restoreForStorage(e.name) })).filter((e) => !e.restored.unresolved).map(
5648
+ (e) => e.kind !== void 0 ? { name: e.restored.text, kind: e.kind } : { name: e.restored.text }
5649
+ );
5650
+ }
5651
+ function parseTopicResponse(parsed, validIds) {
5652
+ if (typeof parsed !== "object" || parsed === null) return null;
5653
+ const list = parsed.memories;
5654
+ if (!Array.isArray(list)) return null;
5655
+ const out = /* @__PURE__ */ new Map();
5656
+ for (const raw of list) {
5657
+ if (typeof raw !== "object" || raw === null) continue;
5658
+ const obj = raw;
5659
+ if (typeof obj.id !== "string" || !validIds.has(obj.id) || out.has(obj.id)) continue;
5660
+ const entities = Array.isArray(obj.entities) ? parseEntities(obj.entities) : [];
5661
+ out.set(obj.id, entities);
5662
+ }
5663
+ return out;
5664
+ }
5665
+ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
5666
+ const entityCtx = ctx.entityCtx;
5667
+ if (!entityCtx) {
5668
+ throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
5669
+ }
5670
+ const log = getLogger();
5671
+ const extractedAt = options.now ?? Date.now();
5672
+ const skippedIds = [];
5673
+ const inputs = [];
5674
+ for (const id of memoryIds) {
5675
+ let record;
5676
+ try {
5677
+ record = await ctx.vaultMemoryCollection.find(id);
5678
+ } catch {
5679
+ skippedIds.push(id);
5680
+ continue;
5681
+ }
5682
+ if (record.isDeleted || ctx.userId !== void 0 && record.userId !== ctx.userId || record.topicsUserManaged) {
5683
+ skippedIds.push(id);
5684
+ continue;
5685
+ }
5686
+ try {
5687
+ const stored = await vaultMemoryToStored(
5688
+ record,
5689
+ ctx.walletAddress,
5690
+ ctx.signMessage,
5691
+ ctx.embeddedWalletSigner
5692
+ );
5693
+ inputs.push({ id, content: stored.content });
5694
+ } catch (err) {
5695
+ log.warn("[memory/topics] failed to load memory for extraction", err);
5696
+ skippedIds.push(id);
5697
+ }
5698
+ }
5699
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
5700
+ const toStamp = [];
5701
+ for (const input of inputs) {
5702
+ const entities = entitiesByMemory.get(input.id);
5703
+ if (entities === void 0) {
5704
+ skippedIds.push(input.id);
5705
+ continue;
5706
+ }
5707
+ try {
5708
+ const linked = await replaceMemoryEntitiesGuardedOp(entityCtx, input.id, entities);
5709
+ if (linked === null) {
5710
+ skippedIds.push(input.id);
5711
+ entitiesByMemory.delete(input.id);
5712
+ continue;
5713
+ }
5714
+ } catch (err) {
5715
+ log.warn("[memory/topics] replaceMemoryEntitiesGuardedOp failed", err);
5716
+ skippedIds.push(input.id);
5717
+ entitiesByMemory.delete(input.id);
5718
+ continue;
5719
+ }
5720
+ toStamp.push(input.id);
5721
+ }
5722
+ const stampedIds = await stampTopicsExtractedAtOp(ctx, toStamp, extractedAt);
5723
+ const stampedSet = new Set(stampedIds);
5724
+ for (const id of toStamp) {
5725
+ if (!stampedSet.has(id)) {
5726
+ skippedIds.push(id);
5727
+ entitiesByMemory.delete(id);
5728
+ }
5729
+ }
5730
+ return { entitiesByMemory, stampedIds, skippedIds };
5731
+ }
5732
+
5549
5733
  // src/tools/appBuilderPrompt.ts
5550
5734
  var APP_BUILDER_PROMPT = `App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.
5551
5735
 
@@ -11087,6 +11271,7 @@ export {
11087
11271
  STREAM_RESUMABLE_HEADER,
11088
11272
  StreamExpiredError,
11089
11273
  TOOL_CATALOG,
11274
+ TOPIC_EXTRACTION_BATCH_SIZE,
11090
11275
  TextProcessor,
11091
11276
  UserPreference,
11092
11277
  VaultFolder,
@@ -11167,7 +11352,9 @@ export {
11167
11352
  estimateMessagesTokens,
11168
11353
  estimateTokens,
11169
11354
  exportPublicKey,
11355
+ extractAndLinkEntitiesForMemoriesOp,
11170
11356
  extractAndRetain,
11357
+ extractEntitiesForMemories,
11171
11358
  extractFacts,
11172
11359
  findMatchingTools,
11173
11360
  generateConversationId,
@@ -11203,6 +11390,7 @@ export {
11203
11390
  getMediaCountsByTypeOp,
11204
11391
  getMediaOp,
11205
11392
  getMediaTypeFromMime,
11393
+ getMemoriesNeedingTopicExtractionOp,
11206
11394
  getMessagesOp,
11207
11395
  getModelPreferenceOp,
11208
11396
  getProjectConversationCountOp,
@@ -11262,6 +11450,7 @@ export {
11262
11450
  shouldChunkMessage,
11263
11451
  shouldRefreshTools,
11264
11452
  splitMessagesAtThreshold,
11453
+ stampTopicsExtractedAtOp,
11265
11454
  streamCancelPath,
11266
11455
  streamReplayPath,
11267
11456
  summaryToSystemMessage,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-45GOZ3AB.mjs";
3
+ } from "./chunk-KEMPJE6C.mjs";
4
4
  import "./chunk-AHJT66EO.mjs";
5
5
  import "./chunk-3RPGXXPZ.mjs";
6
6
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",