@anuma/sdk 3.1.3 → 3.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.
@@ -108,7 +108,7 @@ import {
108
108
  updateVaultMemoryOp,
109
109
  upsertMessageOp,
110
110
  vaultMemoryToStored
111
- } from "./chunk-A4V7FRZ6.mjs";
111
+ } from "./chunk-Y3ZS4OI3.mjs";
112
112
  import {
113
113
  createStreamingDeAnonymizer,
114
114
  resolvePiiRedactor
@@ -1111,6 +1111,9 @@ __decorateClass([
1111
1111
  __decorateClass([
1112
1112
  (0, import_decorators6.field)("topics_extracted_version")
1113
1113
  ], VaultMemory.prototype, "topicsExtractedVersion", 2);
1114
+ __decorateClass([
1115
+ (0, import_decorators6.field)("last_observed_at")
1116
+ ], VaultMemory.prototype, "lastObservedAt", 2);
1114
1117
  __decorateClass([
1115
1118
  import_decorators6.readonly,
1116
1119
  (0, import_decorators6.date)("created_at")
@@ -1258,7 +1261,7 @@ __decorateClass([
1258
1261
  ], VaultFolder.prototype, "context", 2);
1259
1262
 
1260
1263
  // src/lib/db/schema.ts
1261
- var SDK_SCHEMA_VERSION = 38;
1264
+ var SDK_SCHEMA_VERSION = 39;
1262
1265
  var sdkSchema = (0, import_watermelondb12.appSchema)({
1263
1266
  version: SDK_SCHEMA_VERSION,
1264
1267
  tables: [
@@ -1396,7 +1399,14 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
1396
1399
  // The extraction-logic version this memory was last stamped under. Null
1397
1400
  // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
1398
1401
  // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
1399
- { name: "topics_extracted_version", type: "number", isOptional: true }
1402
+ { name: "topics_extracted_version", type: "number", isOptional: true },
1403
+ // Re-observation watermark (C3). Unix ms of the last time retain() merged
1404
+ // a duplicate observation into this fact (proof_count++). Distinct from
1405
+ // updated_at, which merges deliberately preserve (preserveUpdatedAt) so a
1406
+ // re-observation doesn't reorder the vault by edit time. Null = never
1407
+ // re-observed since the column was added. Indexed so recency-weighted
1408
+ // synthesis can filter/sort on it cheaply.
1409
+ { name: "last_observed_at", type: "number", isOptional: true, isIndexed: true }
1400
1410
  ]
1401
1411
  }),
1402
1412
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -2035,6 +2045,20 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
2035
2045
  columns: [{ name: "topics_extracted_version", type: "number", isOptional: true }]
2036
2046
  })
2037
2047
  ]
2048
+ },
2049
+ // v38 -> v39: re-observation watermark on memory_vault (C3). Existing rows
2050
+ // keep NULL (= never re-observed since the column was added); synthesis
2051
+ // treats NULL as "fall back to updated_at" so no backfill is needed.
2052
+ {
2053
+ toVersion: 39,
2054
+ steps: [
2055
+ (0, import_migrations.addColumns)({
2056
+ table: "memory_vault",
2057
+ columns: [
2058
+ { name: "last_observed_at", type: "number", isOptional: true, isIndexed: true }
2059
+ ]
2060
+ })
2061
+ ]
2038
2062
  }
2039
2063
  ]
2040
2064
  });
@@ -4770,6 +4794,8 @@ async function retain(content, ctx, options = {}) {
4770
4794
  proofCountIncrement: 1,
4771
4795
  sourceChunkIds: mergedSourceIds,
4772
4796
  preserveUpdatedAt: true,
4797
+ // C3: record the re-observation without touching updated_at.
4798
+ lastObservedAt: Date.now(),
4773
4799
  ...eventTimeUpdate && { eventTime: eventTimeUpdate }
4774
4800
  });
4775
4801
  if (updated) {
@@ -4920,6 +4946,8 @@ async function tryConsolidate(trimmed, ctx, options) {
4920
4946
  proofCountIncrement: 1,
4921
4947
  sourceChunkIds: mergedSourceIds,
4922
4948
  preserveUpdatedAt: true,
4949
+ // C3: record the re-observation without touching updated_at.
4950
+ lastObservedAt: Date.now(),
4923
4951
  ...eventTimeUpdate && { eventTime: eventTimeUpdate }
4924
4952
  });
4925
4953
  if (!updated) {
@@ -4956,6 +4984,8 @@ async function tryConsolidate(trimmed, ctx, options) {
4956
4984
  // one. Preserving updated_at keeps the recency multiplier honest
4957
4985
  // and matches the merge/noop paths above.
4958
4986
  preserveUpdatedAt: true,
4987
+ // C3: record the re-observation without touching updated_at.
4988
+ lastObservedAt: Date.now(),
4959
4989
  ...eventTimeUpdate && { eventTime: eventTimeUpdate }
4960
4990
  });
4961
4991
  if (!updated) {
@@ -5759,6 +5789,311 @@ function parseAnswer(body, base, parseSchema) {
5759
5789
  };
5760
5790
  }
5761
5791
 
5792
+ // src/lib/memory/synthesizeProfile.ts
5793
+ var DEFAULT_SYNTHESIS_MODEL = "inclusionai/ling-2.6-flash";
5794
+ var DEFAULT_FACET_RECALL_LIMIT = 20;
5795
+ var DEFAULT_FACET_MAX_TOKENS = 512;
5796
+ var DEFAULT_SCOPES = ["private"];
5797
+ var NEW_FACT_ATTRIBUTION_MIN_SCORE = 0.1;
5798
+ var PROFILE_DOC_VERSION = 1;
5799
+ var DEFAULT_PROFILE_FACETS = [
5800
+ {
5801
+ key: "bio",
5802
+ label: "Bio",
5803
+ query: "Who is this person? Their background, personality, values, and what defines them.",
5804
+ guidance: "Write a 1-2 sentence bio (max ~40 words) that captures what makes this person distinctive \u2014 their character, values, or a defining thread across the memories. Be specific and grounded; ban generic dating clich\xE9s ('loves to laugh', 'foodie', 'work hard play hard', 'living life to the fullest')."
5805
+ },
5806
+ {
5807
+ key: "interests",
5808
+ label: "Interests",
5809
+ query: "What are this person's hobbies, passions, pastimes, and interests?",
5810
+ guidance: "Return 3-6 specific interests as a comma-separated list (e.g. 'trail running, film photography, Thai cooking'). Prefer concrete activities the memories actually show over broad categories ('music', 'travel'). No sentence, just the list."
5811
+ },
5812
+ {
5813
+ key: "work_role",
5814
+ label: "Work & Role",
5815
+ query: "What does this person do for work \u2014 their profession, role, industry, or studies?",
5816
+ guidance: "State the person's current role/profession and field in one short line (e.g. 'Backend engineer at a fintech startup'). Use only what the memories support; if unclear or absent, return hasEvidence=false rather than guessing a title."
5817
+ },
5818
+ {
5819
+ key: "location_context",
5820
+ label: "Location",
5821
+ query: "Where does this person live, spend time, or come from?",
5822
+ guidance: "Summarize where the person is based or spends time at neighborhood-or-city granularity in one short line (e.g. 'Based in the Mission, SF'). PRIVACY: never emit a precise address, building, or workplace location \u2014 coarse-grain to the city/neighborhood."
5823
+ },
5824
+ {
5825
+ key: "communication_style",
5826
+ label: "Communication Style",
5827
+ query: "How does this person communicate, express themselves, and interact with others?",
5828
+ guidance: "Describe the person's communication and social style in one line using 2-4 concrete adjectives grounded in the memories (e.g. 'Direct, dry-humored, and a careful listener'). Avoid empty praise."
5829
+ },
5830
+ {
5831
+ key: "recent_activity",
5832
+ label: "Recently",
5833
+ query: "What has this person been doing, focused on, or working on recently?",
5834
+ guidance: "Summarize what the person has been up to lately in one line, favoring the most recently reinforced facts. Frame it as current/ongoing (e.g. 'Lately: training for a first half-marathon and learning Portuguese'). Omit if nothing recent stands out."
5835
+ }
5836
+ ];
5837
+ var FACET_RESPONSE_SCHEMA = {
5838
+ type: "object",
5839
+ properties: {
5840
+ summary: {
5841
+ type: "string",
5842
+ description: "The synthesized section prose. Empty string if the memories don't cover it."
5843
+ },
5844
+ hasEvidence: {
5845
+ type: "boolean",
5846
+ description: "False when the supplied memories don't support any claim for this facet."
5847
+ }
5848
+ },
5849
+ required: ["summary", "hasEvidence"],
5850
+ additionalProperties: false
5851
+ };
5852
+ async function synthesizeProfile(ctx, options = {}) {
5853
+ if (!ctx.vaultCtx) {
5854
+ throw new Error("synthesizeProfile requires ctx.vaultCtx (vault-backed facts).");
5855
+ }
5856
+ if (!ctx.vaultCache) {
5857
+ throw new Error("synthesizeProfile requires ctx.vaultCache (semantic fact recall).");
5858
+ }
5859
+ const facets = options.facets ?? DEFAULT_PROFILE_FACETS;
5860
+ const scopes = options.scopes ?? DEFAULT_SCOPES;
5861
+ const config = {
5862
+ facetKeys: facets.map((f) => f.key).sort(),
5863
+ facetsSignature: facetsSignature(facets),
5864
+ scopes: [...scopes].sort(),
5865
+ redacted: options.redactor !== void 0
5866
+ };
5867
+ const previous = options.previous && options.previous.version === PROFILE_DOC_VERSION && configMatches(options.previous.config, config) ? options.previous : void 0;
5868
+ const memories = await getAllVaultMemoriesOp(ctx.vaultCtx, {
5869
+ // Scope the snapshot to the same scopes synthesis recalls from, so the
5870
+ // watermark, delta, and new-fact attribution all track only facts that can
5871
+ // actually appear in the profile — an out-of-scope change/new-fact must not
5872
+ // trigger a regeneration that scoped recall would never reflect.
5873
+ scopes,
5874
+ includeDeleted: true,
5875
+ includeSuperseded: true
5876
+ });
5877
+ const watermark = computeVaultWatermark(memories);
5878
+ const presentIds = new Set(memories.map((m) => m.uniqueId));
5879
+ const hasStaleSections = previous?.sections.some((s) => s.stale);
5880
+ const citesMissingFact = previous?.sections.some(
5881
+ (s) => s.sourceMemoryIds.some((id) => !presentIds.has(id))
5882
+ );
5883
+ if (previous && previous.vaultWatermark === watermark && !hasStaleSections && !citesMissingFact) {
5884
+ const sameOrder = previous.sections.length === facets.length && facets.every((f, i) => previous.sections[i]?.key === f.key);
5885
+ if (sameOrder) return previous;
5886
+ return {
5887
+ ...previous,
5888
+ sections: facets.map((f) => previous.sections.find((s) => s.key === f.key))
5889
+ };
5890
+ }
5891
+ const staleKeys = await computeStaleFacetKeys(ctx, memories, facets, previous, watermark);
5892
+ const settled = await Promise.allSettled(
5893
+ facets.map(async (facet) => {
5894
+ const prior = previous?.sections.find((s) => s.key === facet.key);
5895
+ if (prior && !staleKeys.has(facet.key)) {
5896
+ return prior;
5897
+ }
5898
+ return synthesizeFacet(facet, ctx, options, prior);
5899
+ })
5900
+ );
5901
+ const sections = settled.map((r, i) => {
5902
+ if (r.status === "fulfilled") return r.value;
5903
+ const facet = facets[i];
5904
+ getLogger().warn(
5905
+ "[memory/synthesizeProfile] facet synthesis rejected; using fallback section",
5906
+ {
5907
+ facet: facet.key,
5908
+ error: r.reason instanceof Error ? r.reason.message : String(r.reason)
5909
+ }
5910
+ );
5911
+ return fallbackSection(
5912
+ facet,
5913
+ previous?.sections.find((s) => s.key === facet.key)
5914
+ );
5915
+ });
5916
+ return {
5917
+ version: PROFILE_DOC_VERSION,
5918
+ sections,
5919
+ vaultWatermark: watermark,
5920
+ config,
5921
+ generatedAt: Date.now()
5922
+ };
5923
+ }
5924
+ function configMatches(a, b) {
5925
+ if (!a) return false;
5926
+ return a.redacted === b.redacted && a.facetsSignature === b.facetsSignature && a.scopes.length === b.scopes.length && a.scopes.every((s, i) => s === b.scopes[i]);
5927
+ }
5928
+ function facetsSignature(facets) {
5929
+ return facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance])).sort().join("\n");
5930
+ }
5931
+ function changeTime(m) {
5932
+ return Math.max(m.updatedAt.getTime(), m.supersededAt ?? 0, m.lastObservedAt ?? 0);
5933
+ }
5934
+ function computeVaultWatermark(memories) {
5935
+ let max = 0;
5936
+ for (const m of memories) {
5937
+ const t = changeTime(m);
5938
+ if (t > max) max = t;
5939
+ }
5940
+ return max;
5941
+ }
5942
+ async function computeStaleFacetKeys(ctx, memories, facets, previous, watermark) {
5943
+ const allKeys = new Set(facets.map((f) => f.key));
5944
+ if (previous && watermark < previous.vaultWatermark) return allKeys;
5945
+ if (!previous) return allKeys;
5946
+ const changed = memories.filter((m) => changeTime(m) > previous.vaultWatermark);
5947
+ const changedIds = new Set(changed.map((m) => m.uniqueId));
5948
+ const presentIds = new Set(memories.map((m) => m.uniqueId));
5949
+ const stale = /* @__PURE__ */ new Set();
5950
+ for (const section of previous.sections) {
5951
+ if (section.sourceMemoryIds.some((id) => changedIds.has(id))) {
5952
+ stale.add(section.key);
5953
+ }
5954
+ if (section.sourceMemoryIds.some((id) => !presentIds.has(id))) {
5955
+ stale.add(section.key);
5956
+ }
5957
+ if (section.stale) {
5958
+ stale.add(section.key);
5959
+ }
5960
+ }
5961
+ for (const facet of facets) {
5962
+ if (!previous.sections.some((s) => s.key === facet.key)) stale.add(facet.key);
5963
+ }
5964
+ const citedIds = new Set(previous.sections.flatMap((s) => s.sourceMemoryIds));
5965
+ const toAttribute = changed.filter(
5966
+ (m) => !m.isDeleted && !m.supersededBy && !citedIds.has(m.uniqueId)
5967
+ );
5968
+ if (toAttribute.length > 0) {
5969
+ const attributed = await attributeFacts(ctx, toAttribute, facets);
5970
+ if (attributed === null) return allKeys;
5971
+ for (const k of attributed) stale.add(k);
5972
+ }
5973
+ return stale;
5974
+ }
5975
+ async function attributeFacts(ctx, candidates, facets) {
5976
+ const model = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
5977
+ const factVectors = [];
5978
+ for (const f of candidates) {
5979
+ if (!f.embedding || f.embeddingModel && f.embeddingModel !== model) {
5980
+ getLogger().warn(
5981
+ "[memory/synthesizeProfile] fact lacks a usable current-model embedding; regenerating all facets",
5982
+ { memoryId: f.uniqueId, hasEmbedding: !!f.embedding, embeddingModel: f.embeddingModel }
5983
+ );
5984
+ return null;
5985
+ }
5986
+ let vec;
5987
+ try {
5988
+ vec = JSON.parse(f.embedding);
5989
+ } catch {
5990
+ getLogger().warn(
5991
+ "[memory/synthesizeProfile] fact embedding failed to parse; regenerating all facets",
5992
+ { memoryId: f.uniqueId }
5993
+ );
5994
+ return null;
5995
+ }
5996
+ if (!Array.isArray(vec) || vec.length === 0) return null;
5997
+ factVectors.push(vec);
5998
+ }
5999
+ let queryVectors;
6000
+ try {
6001
+ queryVectors = await generateEmbeddings(
6002
+ facets.map((f) => f.query),
6003
+ ctx.embeddingOptions
6004
+ );
6005
+ } catch (err) {
6006
+ getLogger().warn(
6007
+ "[memory/synthesizeProfile] facet-query embedding failed; regenerating all facets",
6008
+ { error: err instanceof Error ? err.message : String(err) }
6009
+ );
6010
+ return null;
6011
+ }
6012
+ const keys = /* @__PURE__ */ new Set();
6013
+ for (const fv of factVectors) {
6014
+ let matchedAny = false;
6015
+ for (let i = 0; i < facets.length; i++) {
6016
+ if (cosineSimilarity(fv, queryVectors[i]) >= NEW_FACT_ATTRIBUTION_MIN_SCORE) {
6017
+ keys.add(facets[i].key);
6018
+ matchedAny = true;
6019
+ }
6020
+ }
6021
+ if (!matchedAny) return null;
6022
+ }
6023
+ return keys;
6024
+ }
6025
+ async function synthesizeFacet(facet, ctx, options, prior) {
6026
+ const result = await reflect(facet.query, ctx, {
6027
+ apiKey: options.apiKey,
6028
+ getToken: options.getToken,
6029
+ llmModel: options.llmModel ?? DEFAULT_SYNTHESIS_MODEL,
6030
+ baseUrl: options.baseUrl,
6031
+ fetchFn: options.fetchFn,
6032
+ scopes: options.scopes ?? DEFAULT_SCOPES,
6033
+ limit: options.limit ?? DEFAULT_FACET_RECALL_LIMIT,
6034
+ types: ["fact"],
6035
+ maxTokens: DEFAULT_FACET_MAX_TOKENS,
6036
+ systemPrompt: buildFacetSystemPrompt(facet),
6037
+ responseSchema: FACET_RESPONSE_SCHEMA
6038
+ });
6039
+ const noEvidence = result.basedOn.memoryIds.length === 0;
6040
+ const { text: text12, legitimateEmpty } = extractFacetText(result.structuredOutput);
6041
+ if (!text12 && !legitimateEmpty && !noEvidence) {
6042
+ getLogger().warn(
6043
+ "[memory/synthesizeProfile] facet synthesis returned degraded-empty; keeping prior section",
6044
+ { facet: facet.key, recalledCount: result.basedOn.memoryIds.length }
6045
+ );
6046
+ return fallbackSection(facet, prior);
6047
+ }
6048
+ const section = {
6049
+ key: facet.key,
6050
+ label: facet.label,
6051
+ text: text12,
6052
+ sourceMemoryIds: result.basedOn.memoryIds,
6053
+ generatedAt: Date.now()
6054
+ };
6055
+ if (options.redactor && text12) {
6056
+ const redacted = await options.redactor.redactTextAsync(text12);
6057
+ section.text = redacted.text;
6058
+ }
6059
+ return section;
6060
+ }
6061
+ function fallbackSection(facet, prior) {
6062
+ if (prior && prior.text) {
6063
+ return { ...prior, stale: true };
6064
+ }
6065
+ return {
6066
+ key: facet.key,
6067
+ label: facet.label,
6068
+ text: "",
6069
+ sourceMemoryIds: [],
6070
+ generatedAt: Date.now(),
6071
+ stale: true
6072
+ };
6073
+ }
6074
+ function buildFacetSystemPrompt(facet) {
6075
+ return `You are writing the "${facet.label}" section of a person's shareable profile, using their private memories (supplied as evidence) as the only source of truth.
6076
+
6077
+ Task for this section:
6078
+ ${facet.guidance}
6079
+
6080
+ Rules:
6081
+ - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
6082
+ - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
6083
+ - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
6084
+ - Be concise and specific; no preamble, hedging, or meta-commentary.
6085
+ - Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false> }.`;
6086
+ }
6087
+ function extractFacetText(structured) {
6088
+ if (structured && typeof structured === "object") {
6089
+ const obj = structured;
6090
+ if (obj.hasEvidence === false) return { text: "", legitimateEmpty: true };
6091
+ if (typeof obj.summary === "string")
6092
+ return { text: obj.summary.trim(), legitimateEmpty: false };
6093
+ }
6094
+ return { text: "", legitimateEmpty: false };
6095
+ }
6096
+
5762
6097
  // src/lib/memory/topicExtract.ts
5763
6098
  var TOPIC_EXTRACTION_BATCH_SIZE = 10;
5764
6099
  var MAX_CHARS_PER_MEMORY = 300;
@@ -11626,6 +11961,7 @@ export {
11626
11961
  DEFAULT_MIN_CHUNK_SIZE,
11627
11962
  DEFAULT_MIN_MESSAGES,
11628
11963
  DEFAULT_PERSONALITY_SETTINGS,
11964
+ DEFAULT_PROFILE_FACETS,
11629
11965
  DEFAULT_PROMPT_TEMPLATES,
11630
11966
  DEFAULT_REFRESH_EVERY_MESSAGES,
11631
11967
  DEFAULT_SERVER_TOOLS_MATCH_OPTIONS,
@@ -11640,6 +11976,7 @@ export {
11640
11976
  Media,
11641
11977
  Message,
11642
11978
  ModelPreference,
11979
+ PROFILE_DOC_VERSION,
11643
11980
  PdfProcessor,
11644
11981
  PostgreSQLAdapter,
11645
11982
  ProcessorRegistry,
@@ -11846,6 +12183,7 @@ export {
11846
12183
  streamReplayPath,
11847
12184
  summaryToSystemMessage,
11848
12185
  supersedeVaultMemoryOp,
12186
+ synthesizeProfile,
11849
12187
  toolSetSystemPrompts,
11850
12188
  updateConversationProjectOp,
11851
12189
  updateConversationTitleOp,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-A4V7FRZ6.mjs";
3
+ } from "./chunk-Y3ZS4OI3.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": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",