@anuma/sdk 6.10.0 → 6.12.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.
@@ -6507,9 +6507,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
6507
6507
  const updatedMs = updatedAt.getTime();
6508
6508
  if (!Number.isFinite(updatedMs)) return noDate;
6509
6509
  const now = opts.now ?? /* @__PURE__ */ new Date();
6510
- const nowMs2 = now.getTime();
6511
- if (!Number.isFinite(nowMs2)) return noDate;
6512
- const ageDays2 = Math.max(0, (nowMs2 - updatedMs) / DAY_MS2);
6510
+ const nowMs3 = now.getTime();
6511
+ if (!Number.isFinite(nowMs3)) return noDate;
6512
+ const ageDays2 = Math.max(0, (nowMs3 - updatedMs) / DAY_MS2);
6513
6513
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
6514
6514
  const floor = opts.floor ?? DEFAULT_FLOOR;
6515
6515
  const decay = perYearDecay * (ageDays2 / 365);
@@ -6864,6 +6864,9 @@ async function decomposeQuery(query, options) {
6864
6864
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
6865
6865
  model: options.model ?? DEFAULT_MODEL2,
6866
6866
  systemPrompt: SYSTEM_PROMPT2,
6867
+ // The query itself rides the user turn below, so SYSTEM_PROMPT is the whole
6868
+ // fixed instruction — which is what lets the portal own it outright.
6869
+ taskType: "memory_decompose",
6867
6870
  // Wrap the bare query in a task framing so the model treats it as
6868
6871
  // input to classify, not as a question to answer conversationally.
6869
6872
  // Bare-query inputs caused Anthropic models to respond with prose
@@ -7207,24 +7210,19 @@ function createLlmNeighborRefiner(options) {
7207
7210
  async refine(query, candidates, limit) {
7208
7211
  if (candidates.length === 0) return [];
7209
7212
  const numbered = candidates.map((name, i) => `[${i + 1}] ${name}`).join("\n");
7210
- const systemPrompt = `You help a memory-retrieval system decide which related topics to explore.
7211
-
7212
- Given a user's question and a numbered list of candidate topics/entities linked to memories found so far, pick the ones most likely to lead to memories that help ANSWER the question. Choose at most ${limit}. Prefer topics semantically related to the question; ignore incidental ones.
7213
-
7214
- Output strict JSON, no prose: { "expand": [<entity names to expand, verbatim from the list>] }`;
7215
7213
  const parsed = await callPortalJsonCompletion({
7216
7214
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
7217
7215
  ...options.getToken !== void 0 && { getToken: options.getToken },
7218
7216
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
7219
7217
  taskType: "memory_graph",
7220
7218
  model: options.model ?? DEFAULT_REFINER_MODEL,
7221
- systemPrompt,
7219
+ systemPrompt: REFINER_SYSTEM_PROMPT,
7222
7220
  userMessage: `Question: ${query}
7223
7221
 
7224
7222
  Candidate topics:
7225
7223
  ${numbered}
7226
7224
 
7227
- Which should be expanded?`,
7225
+ Which should be expanded? Choose at most ${limit}.`,
7228
7226
  tag: "memory/graph-refine",
7229
7227
  maxAttempts: options.maxAttempts ?? DEFAULT_REFINER_ATTEMPTS,
7230
7228
  totalTimeoutMs: options.totalTimeoutMs ?? DEFAULT_REFINER_TOTAL_TIMEOUT_MS,
@@ -7245,7 +7243,7 @@ Which should be expanded?`,
7245
7243
  }
7246
7244
  };
7247
7245
  }
7248
- var MAX_HOPS, ENTITY_FANOUT, MIN_REFINER_CANDIDATES, MAX_REFINER_CANDIDATES, NODE_BUDGET, VAULT_SIZE_HOP_CAP, DEFAULT_REFINER_MODEL, DEFAULT_REFINER_ATTEMPTS, DEFAULT_REFINER_TOTAL_TIMEOUT_MS;
7246
+ var MAX_HOPS, ENTITY_FANOUT, MIN_REFINER_CANDIDATES, MAX_REFINER_CANDIDATES, NODE_BUDGET, VAULT_SIZE_HOP_CAP, DEFAULT_REFINER_MODEL, DEFAULT_REFINER_ATTEMPTS, DEFAULT_REFINER_TOTAL_TIMEOUT_MS, REFINER_SYSTEM_PROMPT;
7249
7247
  var init_graphTraversal = __esm({
7250
7248
  "src/lib/memory/graphTraversal.ts"() {
7251
7249
  "use strict";
@@ -7264,6 +7262,11 @@ var init_graphTraversal = __esm({
7264
7262
  DEFAULT_REFINER_MODEL = "inclusionai/ling-2.6-flash";
7265
7263
  DEFAULT_REFINER_ATTEMPTS = 1;
7266
7264
  DEFAULT_REFINER_TOTAL_TIMEOUT_MS = 8e3;
7265
+ REFINER_SYSTEM_PROMPT = `You help a memory-retrieval system decide which related topics to explore.
7266
+
7267
+ Given a user's question and a numbered list of candidate topics/entities linked to memories found so far, pick the ones most likely to lead to memories that help ANSWER the question. Never return more than the maximum the user turn asks for. Prefer topics semantically related to the question; ignore incidental ones.
7268
+
7269
+ Output strict JSON, no prose: { "expand": [<entity names to expand, verbatim from the list>] }`;
7267
7270
  }
7268
7271
  });
7269
7272
 
@@ -7563,6 +7566,9 @@ async function recall(query, ctx, options = {}) {
7563
7566
  const chunkResults = [];
7564
7567
  let vaultSize;
7565
7568
  let didRerank = false;
7569
+ let rerankMs = 0;
7570
+ let queryEmbedMs = 0;
7571
+ let vaultRowsEmbedded;
7566
7572
  let hadV2Head = false;
7567
7573
  let embeddingsUnavailable = false;
7568
7574
  let chunkEmbedFailed = false;
@@ -7587,12 +7593,15 @@ async function recall(query, ctx, options = {}) {
7587
7593
  ...vaultSize !== void 0 && { vaultSize },
7588
7594
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
7589
7595
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
7596
+ ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
7590
7597
  factCount: factResults.length,
7591
7598
  chunkCount: chunkResults.length,
7592
7599
  timings: {
7593
7600
  total: nowMs() - t0,
7594
7601
  prep: prepMs,
7595
7602
  factLane: factLaneMs,
7603
+ rerank: rerankMs,
7604
+ queryEmbed: queryEmbedMs,
7596
7605
  chunkLane: chunkLaneMs,
7597
7606
  fuse: fuseMs
7598
7607
  },
@@ -7656,6 +7665,9 @@ async function recall(query, ctx, options = {}) {
7656
7665
  results,
7657
7666
  vaultSize: size,
7658
7667
  reranked,
7668
+ rerankMs: factRerankMs,
7669
+ queryEmbedMs: factQueryEmbedMs,
7670
+ rowsEmbedded: factRowsEmbedded,
7659
7671
  hadV2Head: v2Head,
7660
7672
  embeddingsUnavailable: factEmbeddingsUnavailable,
7661
7673
  rankedOnCosine: factRankedOnCosine,
@@ -7714,6 +7726,9 @@ async function recall(query, ctx, options = {}) {
7714
7726
  );
7715
7727
  vaultSize = size;
7716
7728
  didRerank = reranked;
7729
+ rerankMs = factRerankMs;
7730
+ queryEmbedMs = factQueryEmbedMs;
7731
+ vaultRowsEmbedded = factRowsEmbedded;
7717
7732
  hadV2Head = v2Head;
7718
7733
  if (factEmbeddingsUnavailable) embeddingsUnavailable = true;
7719
7734
  factLaneRankedOnCosine = factRankedOnCosine;
@@ -8178,6 +8193,7 @@ async function rankFusedVaultMemoriesAsync(query, queryEmbedding, items, options
8178
8193
  const rerankTopN = options.rerankTopN ?? 30;
8179
8194
  const ceWeight = options.ceWeight ?? 0.1;
8180
8195
  const headSlice = v2Ranked.slice(0, rerankTopN);
8196
+ const ceStart = nowMs2();
8181
8197
  try {
8182
8198
  const reranked = await rerankPairs(
8183
8199
  query,
@@ -8205,6 +8221,8 @@ async function rankFusedVaultMemoriesAsync(query, queryEmbedding, items, options
8205
8221
  getLogger().warn("[memory/search] cross-encoder rerank failed; using V2 ranking", err);
8206
8222
  }
8207
8223
  combined = v2Ranked;
8224
+ } finally {
8225
+ if (options.rerankStats) options.rerankStats.ms += nowMs2() - ceStart;
8208
8226
  }
8209
8227
  } else {
8210
8228
  combined = v2Ranked;
@@ -8371,6 +8389,7 @@ async function rankComposite(originalQuery, originalQueryEmbedding, subQueries,
8371
8389
  const ceWeight = options.ceWeight ?? 0.1;
8372
8390
  const headSlice = combined.slice(0, rerankTopN);
8373
8391
  const tailSlice = combined.slice(rerankTopN);
8392
+ const ceStart = nowMs2();
8374
8393
  try {
8375
8394
  const reranked = await rerankPairs(
8376
8395
  originalQuery,
@@ -8398,6 +8417,8 @@ async function rankComposite(originalQuery, originalQueryEmbedding, subQueries,
8398
8417
  err
8399
8418
  );
8400
8419
  }
8420
+ } finally {
8421
+ if (options.rerankStats) options.rerankStats.ms += nowMs2() - ceStart;
8401
8422
  }
8402
8423
  }
8403
8424
  if (options?.mmr) {
@@ -8524,11 +8545,13 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
8524
8545
  rowsDecrypted: 0
8525
8546
  };
8526
8547
  }
8548
+ const queryEmbedStart = nowMs2();
8527
8549
  const queryEmbedding = await embedQueryOrDegrade(
8528
8550
  query,
8529
8551
  embeddingOptions,
8530
8552
  opts.onEmbeddingDegraded
8531
8553
  );
8554
+ if (opts.embedStats) opts.embedStats.queryMs += nowMs2() - queryEmbedStart;
8532
8555
  const embeddingsDegraded = queryEmbedding.length === 0;
8533
8556
  const currentModel = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
8534
8557
  const vectored = [];
@@ -8581,6 +8604,7 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
8581
8604
  const laneRows = laneFetched.filter((m) => !isEncrypted(m.content));
8582
8605
  if (laneRows.length > 0) {
8583
8606
  let laneVecs;
8607
+ if (opts.embedStats) opts.embedStats.rowsEmbedded += laneRows.length;
8584
8608
  try {
8585
8609
  laneVecs = await generateEmbeddings(
8586
8610
  laneRows.map((m) => m.content),
@@ -8659,6 +8683,10 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
8659
8683
  async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache, searchOptions) {
8660
8684
  if (!query || typeof query !== "string") {
8661
8685
  return {
8686
+ // Nothing was read and nothing was embedded — the short-circuit fires
8687
+ // before either.
8688
+ queryEmbedMs: 0,
8689
+ rowsEmbedded: 0,
8662
8690
  memories: [],
8663
8691
  embeddedItems: [],
8664
8692
  queryEmbedding: [],
@@ -8691,6 +8719,7 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8691
8719
  embeddingsUnavailable = true;
8692
8720
  embeddingFailure = true;
8693
8721
  };
8722
+ const embedStats = { queryMs: 0, rowsEmbedded: 0 };
8694
8723
  if (searchOptions?.decryptLast) {
8695
8724
  const forceIncludeIds = [
8696
8725
  ...searchOptions.entityRanking ?? [],
@@ -8702,7 +8731,8 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8702
8731
  admitFloor: ADMIT_FLOOR,
8703
8732
  unembeddedCap: UNEMBEDDED_CAP,
8704
8733
  ...forceIncludeIds.length > 0 && { forceIncludeIds },
8705
- onEmbeddingDegraded
8734
+ onEmbeddingDegraded,
8735
+ embedStats
8706
8736
  });
8707
8737
  if (corpus.vaultSize === 0) {
8708
8738
  return {
@@ -8713,7 +8743,9 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8713
8743
  embeddingsUnavailable: false,
8714
8744
  embeddingFailure: false,
8715
8745
  decryptLast: true,
8716
- rowsDecrypted: corpus.rowsDecrypted
8746
+ rowsDecrypted: corpus.rowsDecrypted,
8747
+ queryEmbedMs: embedStats.queryMs,
8748
+ rowsEmbedded: embedStats.rowsEmbedded
8717
8749
  };
8718
8750
  }
8719
8751
  ({ memories, embeddedItems, queryEmbedding, vaultSize } = corpus);
@@ -8730,7 +8762,9 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8730
8762
  decryptLast: true,
8731
8763
  // Rows WERE decrypt-attempted even though none came back readable — that
8732
8764
  // is the whole point of reporting attempts rather than successes.
8733
- rowsDecrypted: corpus.rowsDecrypted
8765
+ rowsDecrypted: corpus.rowsDecrypted,
8766
+ queryEmbedMs: embedStats.queryMs,
8767
+ rowsEmbedded: embedStats.rowsEmbedded
8734
8768
  };
8735
8769
  }
8736
8770
  } else {
@@ -8755,10 +8789,14 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8755
8789
  embeddingsUnavailable: false,
8756
8790
  embeddingFailure: false,
8757
8791
  decryptLast: false,
8758
- rowsDecrypted: loaded.length
8792
+ rowsDecrypted: loaded.length,
8793
+ queryEmbedMs: embedStats.queryMs,
8794
+ rowsEmbedded: embedStats.rowsEmbedded
8759
8795
  };
8760
8796
  }
8797
+ const legacyEmbedStart = nowMs2();
8761
8798
  queryEmbedding = await embedQueryOrDegrade(query, embeddingOptions, onEmbeddingDegraded);
8799
+ embedStats.queryMs += nowMs2() - legacyEmbedStart;
8762
8800
  const currentModel = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
8763
8801
  const uncachedTexts = [];
8764
8802
  const uncachedIndices = [];
@@ -8792,6 +8830,7 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8792
8830
  }
8793
8831
  if (uncachedTexts.length > 0) {
8794
8832
  let newEmbeddings;
8833
+ embedStats.rowsEmbedded += uncachedTexts.length;
8795
8834
  try {
8796
8835
  newEmbeddings = await generateEmbeddings(uncachedTexts, embeddingOptions);
8797
8836
  } catch (err) {
@@ -8835,6 +8874,8 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
8835
8874
  embeddingsUnavailable = true;
8836
8875
  }
8837
8876
  return {
8877
+ queryEmbedMs: embedStats.queryMs,
8878
+ rowsEmbedded: embedStats.rowsEmbedded,
8838
8879
  memories,
8839
8880
  embeddedItems,
8840
8881
  queryEmbedding,
@@ -8885,11 +8926,15 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
8885
8926
  results: results2,
8886
8927
  vaultSize: out.vaultSize,
8887
8928
  reranked: out.reranked ?? false,
8929
+ // Read straight off the shared stats object rather than being passed in at
8930
+ // each call site: every return in this function funnels through here, so a
8931
+ // path added later reports the CE bill without having to remember to.
8932
+ rerankMs: rerankStats.ms,
8888
8933
  hadV2Head: out.hadV2Head ?? false,
8889
8934
  embeddingsUnavailable
8890
8935
  };
8891
8936
  };
8892
- const rerankStats = { applied: false };
8937
+ const rerankStats = { applied: false, ms: 0 };
8893
8938
  const useFusion = searchOptions?.useFusion ?? true;
8894
8939
  const tuning = {
8895
8940
  ...searchOptions?.recencyAlpha !== void 0 && { recencyAlpha: searchOptions.recencyAlpha },
@@ -9008,13 +9053,16 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
9008
9053
  results: [],
9009
9054
  vaultSize: 0,
9010
9055
  reranked: false,
9056
+ rerankMs: 0,
9011
9057
  hadV2Head: false,
9012
9058
  embeddingsUnavailable: false,
9013
9059
  rankedOnCosine: false,
9014
9060
  // No storage read happened, so nothing was decrypted; report the path the
9015
9061
  // caller asked for so this turn is still attributable.
9016
9062
  decryptLast: !!searchOptions?.decryptLast,
9017
- rowsDecrypted: 0
9063
+ rowsDecrypted: 0,
9064
+ queryEmbedMs: 0,
9065
+ rowsEmbedded: 0
9018
9066
  };
9019
9067
  }
9020
9068
  if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
@@ -9035,11 +9083,14 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
9035
9083
  results: [],
9036
9084
  vaultSize: 0,
9037
9085
  reranked: false,
9086
+ rerankMs: 0,
9038
9087
  hadV2Head: false,
9039
9088
  embeddingsUnavailable: prepared.embeddingsUnavailable,
9040
9089
  rankedOnCosine: false,
9041
9090
  decryptLast: prepared.decryptLast,
9042
- rowsDecrypted: prepared.rowsDecrypted
9091
+ rowsDecrypted: prepared.rowsDecrypted,
9092
+ queryEmbedMs: prepared.queryEmbedMs,
9093
+ rowsEmbedded: prepared.rowsEmbedded
9043
9094
  };
9044
9095
  }
9045
9096
  if (prepared.memories.length === 0) {
@@ -9047,11 +9098,14 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
9047
9098
  results: [],
9048
9099
  vaultSize: prepared.vaultSize,
9049
9100
  reranked: false,
9101
+ rerankMs: 0,
9050
9102
  hadV2Head: false,
9051
9103
  embeddingsUnavailable: prepared.embeddingsUnavailable,
9052
9104
  rankedOnCosine: false,
9053
9105
  decryptLast: prepared.decryptLast,
9054
- rowsDecrypted: prepared.rowsDecrypted
9106
+ rowsDecrypted: prepared.rowsDecrypted,
9107
+ queryEmbedMs: prepared.queryEmbedMs,
9108
+ rowsEmbedded: prepared.rowsEmbedded
9055
9109
  };
9056
9110
  }
9057
9111
  const ranked = await rankPreparedVaultCandidates(
@@ -9064,7 +9118,9 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
9064
9118
  ...ranked,
9065
9119
  rankedOnCosine: !ranked.embeddingsUnavailable,
9066
9120
  decryptLast: prepared.decryptLast,
9067
- rowsDecrypted: prepared.rowsDecrypted
9121
+ rowsDecrypted: prepared.rowsDecrypted,
9122
+ queryEmbedMs: prepared.queryEmbedMs,
9123
+ rowsEmbedded: prepared.rowsEmbedded
9068
9124
  };
9069
9125
  }
9070
9126
  async function searchVaultMemories(query, vaultCtx, embeddingOptions, cache, searchOptions) {
@@ -9214,7 +9270,7 @@ ${formatted}`;
9214
9270
  }
9215
9271
  };
9216
9272
  }
9217
- var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
9273
+ var nowMs2, warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
9218
9274
  var init_searchTool = __esm({
9219
9275
  "src/lib/memoryVault/searchTool.ts"() {
9220
9276
  "use strict";
@@ -9232,6 +9288,7 @@ var init_searchTool = __esm({
9232
9288
  init_bm25();
9233
9289
  init_decomposeQuery();
9234
9290
  init_lruCache();
9291
+ nowMs2 = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
9235
9292
  warnedProgrammaticDecomposeIgnored = false;
9236
9293
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
9237
9294
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -17686,6 +17743,7 @@ ${TRUNCATION_NOTICE}` : formatted;
17686
17743
  // src/lib/memory/reflect.ts
17687
17744
  init_clientConfig();
17688
17745
  init_logger();
17746
+ init_taskType();
17689
17747
  init_portalLlm();
17690
17748
  init_recall();
17691
17749
  var DEFAULT_BASE_URL2 = BASE_URL;
@@ -17731,11 +17789,13 @@ Respond with ONLY a single JSON object conforming to this JSON Schema, with no p
17731
17789
  ${JSON.stringify(
17732
17790
  options.responseSchema
17733
17791
  )}` : basePrompt;
17734
- const userMessage = `Question:
17735
- ${trimmed}
17736
-
17737
- Memories (use only these as evidence):
17738
- ${evidence}`;
17792
+ const userMessage = [
17793
+ `Question:
17794
+ ${trimmed}`,
17795
+ ...options.userInstructions ? [options.userInstructions] : [],
17796
+ `Memories (use only these as evidence):
17797
+ ${evidence}`
17798
+ ].join("\n\n");
17739
17799
  const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
17740
17800
  const fetchImpl = options.fetchFn ?? fetch;
17741
17801
  const log = getLogger();
@@ -17749,7 +17809,13 @@ ${evidence}`;
17749
17809
  try {
17750
17810
  response = await fetchImpl(`${baseUrl}/api/v1/chat/completions`, {
17751
17811
  method: "POST",
17752
- headers: { ...authHeaders, "Content-Type": "application/json" },
17812
+ // No task type unless the caller named one — an undeclared task adds no
17813
+ // header at all, which is what keeps user-facing reflect() unlabelled.
17814
+ headers: {
17815
+ ...authHeaders,
17816
+ ...taskTypeHeader(options.taskType),
17817
+ "Content-Type": "application/json"
17818
+ },
17753
17819
  body: JSON.stringify({
17754
17820
  model,
17755
17821
  // Modern OpenAI field; the portal reads only `max_completion_tokens`
@@ -18201,10 +18267,15 @@ async function synthesizeFacet(facet, ctx, options, prior) {
18201
18267
  limit,
18202
18268
  types: ["fact"],
18203
18269
  maxTokens: DEFAULT_FACET_MAX_TOKENS,
18204
- // Profile-facet synthesis is a background op, so it marks its own prompt.
18205
- // reflect() itself must NOT mark — it also serves the user's own questions,
18206
- // and that traffic is chat, not an internal flow. See ../internalFlowMarker.
18207
- systemPrompt: withInternalFlowMarker(buildFacetSystemPrompt(facet)),
18270
+ // Profile-facet synthesis is a background op, so it marks its own prompt AND
18271
+ // names its own task. reflect() itself must do neither — it also serves the
18272
+ // user's own questions, and that traffic is chat, not an internal flow. See
18273
+ // ../internalFlowMarker and ReflectOptions.taskType.
18274
+ taskType: "memory_profile_synth",
18275
+ systemPrompt: withInternalFlowMarker(FACET_SYSTEM_PROMPT),
18276
+ // The facet's label/guidance/response-fields, on the user turn — what keeps
18277
+ // the system half fixed and therefore server-ownable.
18278
+ userInstructions: buildFacetUserInstructions(facet),
18208
18279
  responseSchema: facetResponseSchema(facet.key),
18209
18280
  memories
18210
18281
  });
@@ -18262,19 +18333,22 @@ function fallbackSection(facet, prior) {
18262
18333
  stale: true
18263
18334
  };
18264
18335
  }
18265
- function buildFacetSystemPrompt(facet) {
18266
- const structuredField = FACET_STRUCTURED_RESPONSE_HINT[facet.key] ?? "";
18267
- 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.
18268
-
18269
- Task for this section:
18270
- ${facet.guidance}
18336
+ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable profile, using their private memories (supplied as evidence) as the only source of truth. The user turn names the section, states the task for it, and gives the exact JSON shape to respond in.
18271
18337
 
18272
18338
  Rules:
18273
18339
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
18274
18340
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
18275
18341
  - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
18276
18342
  - Be concise and specific; no preamble, hedging, or meta-commentary.
18277
- - Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false>${structuredField} }.`;
18343
+ - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
18344
+ function buildFacetUserInstructions(facet) {
18345
+ const structuredField = FACET_STRUCTURED_RESPONSE_HINT[facet.key] ?? "";
18346
+ return `Section: "${facet.label}"
18347
+
18348
+ Task for this section:
18349
+ ${facet.guidance}
18350
+
18351
+ Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false>${structuredField} }.`;
18278
18352
  }
18279
18353
  function extractFacetText(structured) {
18280
18354
  if (structured && typeof structured === "object") {
@@ -4880,6 +4880,51 @@ interface EntityOperationsContext {
4880
4880
  allowUnscopedRows?: boolean;
4881
4881
  }
4882
4882
 
4883
+ /**
4884
+ * Class-B task types — the vocabulary of the `X-Anuma-Task-Type` header.
4885
+ *
4886
+ * A Class-B flow has ONE fixed purpose and no conversation: extract memories
4887
+ * from a snippet, pick a topic, consolidate duplicates, classify a query. Today
4888
+ * each of those builds its own system prompt on the client and sends it as a
4889
+ * `role:system` message, which makes the prompt a suggestion rather than a
4890
+ * contract — a caller can reword it into "you are a general assistant" and get
4891
+ * general chat at internal-endpoint prices.
4892
+ *
4893
+ * The fix is that the SERVER owns the text and the client only names the task.
4894
+ * This header is the name. The portal allowlists it against its own copy of this
4895
+ * enum (ai-portal `internal/systemprompt/tasks.go`) and DROPS anything it does
4896
+ * not recognise, so an unknown value degrades to "no task declared" rather than
4897
+ * failing the request.
4898
+ *
4899
+ * ⚠ These string values are a wire contract with ai-portal. Adding one here
4900
+ * before the portal knows it is harmless (it is dropped); renaming or removing
4901
+ * one silently stops that flow being counted. Keep them in step with
4902
+ * `AllTaskTypes` on the portal side, and see ai-portal
4903
+ * `docs/BACKEND_OWNED_TASK_PROMPTS.md` for the rollout.
4904
+ *
4905
+ * ⚠ RELEASE ORDER. This union is a PUBLIC type, so a split is breaking in both
4906
+ * directions and has to be sequenced:
4907
+ *
4908
+ * 1. merge + publish this package;
4909
+ * 2. bump `@anuma/sdk` in the consumer and switch its call sites to the new
4910
+ * names in the same commit.
4911
+ *
4912
+ * A consumer on the OLD version cannot compile the new names, and a consumer on
4913
+ * the NEW version cannot compile the retired ones (`app_inspiration` is gone
4914
+ * here, replaced by the three `app_inspiration_*` names). Both halves of that
4915
+ * are type-only — the wire itself always degrades safely, because the portal
4916
+ * drops any value it does not recognise. Concretely: ai-memoryless-client#5782
4917
+ * is blocked on this publish for exactly that reason.
4918
+ *
4919
+ * Deliberately ABSENT: agent personas, `reflect()` when it answers the user, and
4920
+ * the app-idea chat. Those carry an open conversation with the user, so a fixed
4921
+ * server-owned prompt would be wrong rather than merely premature — the same
4922
+ * boundary {@link INTERNAL_FLOW_MARKER} draws.
4923
+ *
4924
+ * @public
4925
+ */
4926
+ type TaskType = "title" | "commit_message" | "memory_extract" | "memory_dedup" | "memory_decay" | "memory_injection_check" | "memory_verify_support" | "memory_autosort" | "memory_topic" | "memory_consolidate" | "memory_decompose" | "memory_profile_synth" | "memory_graph" | "folder_context" | "selection_memory" | "summarize" | "style_analysis" | "classify_search" | "app_inspiration_generate" | "app_inspiration_trends" | "app_inspiration_remix" | "spotlight_image" | "slide_image" | "slide_video" | "slide_music" | "slide_sfx" | "media_video" | "media_audio" | "media_bg_removal" | "media_image_preset" | "tool_app_builder" | "tool_document_builder" | "tool_slides" | "tool_slides_image_gen" | "connector_guidance" | "cf_task" | "xbot_intent" | "xbot_council" | "scheduled_task";
4927
+
4883
4928
  /**
4884
4929
  * Shared portal LLM call for the memory pipeline.
4885
4930
  *
@@ -5732,6 +5777,17 @@ interface RecallDiagnostics {
5732
5777
  * never the cost.
5733
5778
  */
5734
5779
  vaultRowsDecrypted?: number;
5780
+ /**
5781
+ * Rows the fact lane had to re-embed through the portal because their stored
5782
+ * vector was unusable — stale `embedding_model`, wrong dimension, or
5783
+ * unparseable. Absent when the fact lane didn't run.
5784
+ *
5785
+ * Expected to be 0 on a healthy vault: rows are embedded at write time and the
5786
+ * re-embed writes the current model back, so a persistently non-zero value
5787
+ * means the writeback is not sticking and every turn is paying for it. On the
5788
+ * LEGACY read path this batch is uncapped, so it can be the whole vault.
5789
+ */
5790
+ vaultRowsEmbedded?: number;
5735
5791
  /** Facts the fact lane returned (post-dedupe, pre-fusion). */
5736
5792
  factCount: number;
5737
5793
  /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
@@ -5744,6 +5800,37 @@ interface RecallDiagnostics {
5744
5800
  prep: number;
5745
5801
  /** Vault fact-lane search (`searchVaultMemoriesWithSize`). */
5746
5802
  factLane: number;
5803
+ /**
5804
+ * The cross-encoder's share of {@link factLane} — wall-clock spent inside
5805
+ * `rerankPairs`. Billed even when the CE threw partway through, because a
5806
+ * rerank that burned three seconds and then failed still cost them.
5807
+ *
5808
+ * A SUBSET of `factLane`, not a sibling: read `factLane - rerank` for
5809
+ * everything else the lane did (query embed, vault read, fused ranking).
5810
+ * 0 when the CE did not run; `reranked` is what distinguishes that from a
5811
+ * rerank that cost nothing.
5812
+ *
5813
+ * Exists because #845 spent three rounds arguing about which stage inside
5814
+ * the fact lane dominated — whole-vault read, then admission window, then
5815
+ * the CE — with one aggregate number for all of them. Every hypothesis was
5816
+ * an inference; this makes the question a query.
5817
+ */
5818
+ rerank: number;
5819
+ /**
5820
+ * The query embed's share of {@link factLane} — one portal round trip.
5821
+ *
5822
+ * Also a SUBSET of `factLane`. Together with `rerank` this makes the lane
5823
+ * decomposable: `factLane - queryEmbed - rerank` is the local work (vault
5824
+ * read, decrypt, BM25/cosine fusion), and each of the three implies a
5825
+ * completely different fix.
5826
+ *
5827
+ * 0 when the lane returned before embedding, which includes every empty
5828
+ * vault — `prepareVaultCandidates` short-circuits first. That is why the
5829
+ * fast `vault_size = 0` population never established a baseline for this
5830
+ * cost, and why the ~850ms floor on the smallest NON-empty vaults had no
5831
+ * attributable owner.
5832
+ */
5833
+ queryEmbed: number;
5747
5834
  /** Chunk-lane search (`searchChunksOp`). */
5748
5835
  chunkLane: number;
5749
5836
  /** Cross-lane RRF fusion + provenance dedup after both lanes. */
@@ -8180,6 +8267,28 @@ interface ReflectOptions extends RecallOptions, PortalLlmAuth {
8180
8267
  maxTokens?: number;
8181
8268
  /** Override the grounding system prompt. */
8182
8269
  systemPrompt?: string;
8270
+ /**
8271
+ * Extra caller instruction to carry on the USER turn, between the question and
8272
+ * the evidence block (see the `userMessage` assembly below). This is the slot a
8273
+ * background caller uses to keep its per-request data OUT of the system message
8274
+ * without colliding with the numbered evidence list — profile-facet synthesis
8275
+ * puts its section label, guidance and response-field hint here so its system
8276
+ * half can stay fixed and server-ownable.
8277
+ */
8278
+ userInstructions?: string;
8279
+ /**
8280
+ * Class-B task name for the `X-Anuma-Task-Type` header, or nothing.
8281
+ *
8282
+ * Deliberately OPTIONAL and unset by default. reflect() also answers the user's
8283
+ * OWN question, and that traffic is chat, not an internal flow — declaring a
8284
+ * task type unconditionally here would put an internal-flow name on real
8285
+ * conversation, which is the same boundary `INTERNAL_FLOW_MARKER` draws
8286
+ * (reflect is deliberately unmarked; its background caller marks its own
8287
+ * prompt — see ../internalFlowMarker.ts). So the name is per call: only a
8288
+ * caller with ONE fixed purpose passes
8289
+ * one, and today that is profile-facet synthesis (`memory_profile_synth`).
8290
+ */
8291
+ taskType?: TaskType;
8183
8292
  /** Endpoint for the answer LLM. */
8184
8293
  baseUrl?: string;
8185
8294
  /** Override fetch (for tests). */