@anuma/sdk 4.2.1 → 5.0.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.
@@ -6401,11 +6401,11 @@ async function decomposeQuery(query, options) {
6401
6401
  userMessage: `Classify the following memory query and decompose if composite. Respond with JSON only \u2014 do not answer the question, do not ask for clarification.
6402
6402
 
6403
6403
  Query: ${trimmed}`,
6404
- // Decompose runs on the recall hot path — tighter than the
6404
+ // Decompose runs ahead of recall in the tool layer — tighter than the
6405
6405
  // portalLlm default (consolidate/extract can wait longer).
6406
6406
  timeoutMs: 2e4,
6407
6407
  // No internal retry: a failure degrades to the safe `{specific,[query]}`
6408
- // fallback below, so retrying would only add latency on the hot path.
6408
+ // fallback below. The agent/tool loop is the retry surface (719/B4).
6409
6409
  maxAttempts: 1,
6410
6410
  tag: "memory/decompose",
6411
6411
  ...options.fetchFn && { fetchFn: options.fetchFn }
@@ -6417,12 +6417,28 @@ Query: ${trimmed}`,
6417
6417
  if (parsed === null) return fallback;
6418
6418
  return validate(parsed, trimmed) ?? fallback;
6419
6419
  }
6420
+ function normalizeSubQueries(raw) {
6421
+ if (!raw || raw.length === 0) return [];
6422
+ const seen = /* @__PURE__ */ new Set();
6423
+ const out = [];
6424
+ for (const s of raw) {
6425
+ if (typeof s !== "string") continue;
6426
+ const t = s.trim();
6427
+ if (t.length === 0) continue;
6428
+ const key = t.toLowerCase();
6429
+ if (seen.has(key)) continue;
6430
+ seen.add(key);
6431
+ out.push(t);
6432
+ if (out.length >= MAX_SUB_QUERIES) break;
6433
+ }
6434
+ return out;
6435
+ }
6420
6436
  function validate(parsed, originalQuery) {
6421
6437
  if (typeof parsed !== "object" || parsed === null) return null;
6422
6438
  const obj = parsed;
6423
6439
  const mode = obj.mode === "composite" ? "composite" : "specific";
6424
6440
  if (!Array.isArray(obj.subQueries)) return null;
6425
- const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
6441
+ const subQueries = normalizeSubQueries(obj.subQueries);
6426
6442
  if (subQueries.length === 0) return null;
6427
6443
  if (mode === "specific") {
6428
6444
  return { mode: "specific", subQueries: [originalQuery] };
@@ -7039,12 +7055,12 @@ __export(recall_exports, {
7039
7055
  function flagsForBudget(budget) {
7040
7056
  switch (budget) {
7041
7057
  case "high":
7042
- return { rerank: true, decompose: true, traverse: true };
7058
+ return { rerank: true, traverse: true };
7043
7059
  case "mid":
7044
- return { rerank: true, decompose: false, traverse: false };
7060
+ return { rerank: true, traverse: false };
7045
7061
  case "low":
7046
7062
  default:
7047
- return { rerank: false, decompose: false, traverse: false };
7063
+ return { rerank: false, traverse: false };
7048
7064
  }
7049
7065
  }
7050
7066
  function dedupeBy(items, ...keys) {
@@ -7063,10 +7079,10 @@ function dedupeBy(items, ...keys) {
7063
7079
  async function recall(query, ctx, options = {}) {
7064
7080
  const types = options.types ?? ["fact"];
7065
7081
  const limit = options.limit ?? DEFAULT_LIMIT;
7066
- const requestedBudget = options.budget ?? DEFAULT_BUDGET;
7067
- const flags = flagsForBudget(requestedBudget);
7068
- const decomposeAvailable = flags.decompose && !!options.decomposeOptions;
7069
- const usedBudget = requestedBudget === "high" && !decomposeAvailable ? "mid" : requestedBudget;
7082
+ const usedBudget = options.budget ?? DEFAULT_BUDGET;
7083
+ const flags = flagsForBudget(usedBudget);
7084
+ const normalizedFacets = normalizeSubQueries(options.subQueries);
7085
+ const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
7070
7086
  const t0 = nowMs();
7071
7087
  let prepMs = 0;
7072
7088
  let factLaneMs = 0;
@@ -7087,7 +7103,9 @@ async function recall(query, ctx, options = {}) {
7087
7103
  if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
7088
7104
  degraded.push("rerank-unavailable");
7089
7105
  }
7090
- if (flags.decompose && !decomposeAvailable) degraded.push("decompose-unavailable");
7106
+ if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
7107
+ degraded.push("decompose-moved");
7108
+ }
7091
7109
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
7092
7110
  const diagnostics = {
7093
7111
  usedBudget,
@@ -7200,10 +7218,8 @@ async function recall(query, ctx, options = {}) {
7200
7218
  },
7201
7219
  ...options.rrfK !== void 0 && { rrfK: options.rrfK },
7202
7220
  ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
7203
- ...decomposeAvailable && {
7204
- decompose: "llm",
7205
- decomposeOptions: options.decomposeOptions
7206
- },
7221
+ // 719/B4 — composite facets arrive pre-built; no LLM call here.
7222
+ ...subQueries && { subQueries },
7207
7223
  ...options.scopes && { scopes: options.scopes },
7208
7224
  ...options.folderId !== void 0 && { folderId: options.folderId },
7209
7225
  ...options.factTypes?.length && { factTypes: options.factTypes },
@@ -7440,6 +7456,7 @@ var init_recall = __esm({
7440
7456
  init_logger();
7441
7457
  init_constants();
7442
7458
  init_embeddings();
7459
+ init_decomposeQuery();
7443
7460
  init_searchTool();
7444
7461
  init_graphTraversal();
7445
7462
  init_observationTrend();
@@ -8385,52 +8402,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
8385
8402
  ...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
8386
8403
  ...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
8387
8404
  };
8388
- if (useFusion && !embeddingsUnavailable && searchOptions?.decompose === "llm" && searchOptions.decomposeOptions) {
8389
- const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8390
- if (decomp.mode === "composite") {
8391
- let subEmbeddings;
8392
- try {
8393
- subEmbeddings = await generateEmbeddings(decomp.subQueries, embeddingOptions);
8394
- } catch (err) {
8395
- getLogger().warn(
8396
- "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8397
- );
8398
- subEmbeddings = [];
8399
- }
8400
- const subEmbeddingsUsable = decomp.subQueries.length > 0 && subEmbeddings.length === decomp.subQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8401
- if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8402
- getLogger().warn(
8403
- `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${decomp.subQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
8404
- );
8405
- }
8406
- if (subEmbeddingsUsable) {
8407
- const subQueries = decomp.subQueries.map((sq, i) => ({
8408
- query: sq,
8409
- embedding: subEmbeddings[i]
8410
- }));
8411
- const v2HeadStats = { hadResults: false };
8412
- const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8413
- limit,
8414
- minSimilarity,
8415
- rerank: !!searchOptions.rerank,
8416
- ...searchOptions.rerankTopN !== void 0 && {
8417
- rerankTopN: searchOptions.rerankTopN
8418
- },
8419
- ...searchOptions.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8420
- ...searchOptions.mmr !== void 0 && { mmr: searchOptions.mmr },
8421
- ...tuning,
8422
- ...searchOptions.entityRanking && { entityRanking: searchOptions.entityRanking },
8423
- ...searchOptions.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8424
- rerankStats,
8425
- v2HeadStats
8426
- });
8427
- return stampTimestamps({
8428
- results: composite,
8429
- vaultSize,
8430
- reranked: rerankStats.applied,
8431
- hadV2Head: v2HeadStats.hadResults
8432
- });
8433
- }
8405
+ const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
8406
+ const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
8407
+ if (useFusion && !embeddingsUnavailable && facetQueries) {
8408
+ let subEmbeddings;
8409
+ try {
8410
+ subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
8411
+ } catch (err) {
8412
+ getLogger().warn(
8413
+ "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8414
+ );
8415
+ subEmbeddings = [];
8416
+ }
8417
+ const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8418
+ if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8419
+ getLogger().warn(
8420
+ `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${facetQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
8421
+ );
8422
+ }
8423
+ if (subEmbeddingsUsable) {
8424
+ const subQueries = facetQueries.map((sq, i) => ({
8425
+ query: sq,
8426
+ embedding: subEmbeddings[i]
8427
+ }));
8428
+ const v2HeadStats = { hadResults: false };
8429
+ const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8430
+ limit,
8431
+ minSimilarity,
8432
+ rerank: !!searchOptions?.rerank,
8433
+ ...searchOptions?.rerankTopN !== void 0 && {
8434
+ rerankTopN: searchOptions.rerankTopN
8435
+ },
8436
+ ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8437
+ ...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
8438
+ ...tuning,
8439
+ ...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
8440
+ ...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8441
+ rerankStats,
8442
+ v2HeadStats
8443
+ });
8444
+ return stampTimestamps({
8445
+ results: composite,
8446
+ vaultSize,
8447
+ reranked: rerankStats.applied,
8448
+ hadV2Head: v2HeadStats.hadResults
8449
+ });
8434
8450
  }
8435
8451
  }
8436
8452
  if (useFusion && searchOptions?.rerank) {
@@ -8490,6 +8506,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
8490
8506
  rankedOnCosine: false
8491
8507
  };
8492
8508
  }
8509
+ if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
8510
+ warnedProgrammaticDecomposeIgnored = true;
8511
+ getLogger().warn(
8512
+ 'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
8513
+ );
8514
+ }
8493
8515
  const prepared = await prepareVaultCandidates(
8494
8516
  query,
8495
8517
  vaultCtx,
@@ -8594,7 +8616,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8594
8616
  return "Error: A search query is required.";
8595
8617
  }
8596
8618
  try {
8597
- const budget = searchOptions?.decompose === "llm" && searchOptions.decomposeOptions ? "high" : searchOptions?.rerank ? "mid" : "low";
8619
+ const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
8620
+ const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
8598
8621
  const folderId = searchOptions?.folderId ?? argFolderId;
8599
8622
  if (searchOptions?.useFusion === false) {
8600
8623
  const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
@@ -8618,6 +8641,13 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8618
8641
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
8619
8642
  );
8620
8643
  }
8644
+ let subQueries;
8645
+ if (wantsDecompose && searchOptions?.decomposeOptions) {
8646
+ const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8647
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
8648
+ subQueries = decomp.subQueries;
8649
+ }
8650
+ }
8621
8651
  const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
8622
8652
  let recallDegraded = [];
8623
8653
  const result = await recall2(
@@ -8634,9 +8664,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8634
8664
  ...tuningForward,
8635
8665
  ...folderId !== void 0 && { folderId },
8636
8666
  ...searchOptions?.scopes && { scopes: searchOptions.scopes },
8637
- ...searchOptions?.decompose === "llm" && searchOptions.decomposeOptions && {
8638
- decomposeOptions: searchOptions.decomposeOptions
8639
- }
8667
+ ...subQueries && { subQueries }
8640
8668
  }
8641
8669
  );
8642
8670
  if (result.vaultSize === 0) {
@@ -8666,7 +8694,7 @@ ${formatted}`;
8666
8694
  }
8667
8695
  };
8668
8696
  }
8669
- var SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8697
+ var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8670
8698
  var init_searchTool = __esm({
8671
8699
  "src/lib/memoryVault/searchTool.ts"() {
8672
8700
  "use strict";
@@ -8684,6 +8712,7 @@ var init_searchTool = __esm({
8684
8712
  init_bm25();
8685
8713
  init_decomposeQuery();
8686
8714
  init_lruCache();
8715
+ warnedProgrammaticDecomposeIgnored = false;
8687
8716
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
8688
8717
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
8689
8718
  SUPERSESSION_BOOST_FACTOR = 0.8;
@@ -16715,6 +16744,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
16715
16744
  init_recall();
16716
16745
 
16717
16746
  // src/lib/memory/recallTool.ts
16747
+ init_decomposeQuery();
16718
16748
  init_recall();
16719
16749
  init_recallConstants();
16720
16750
  var DEFAULT_LIMIT2 = 8;
@@ -16812,7 +16842,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16812
16842
  type: "function",
16813
16843
  function: {
16814
16844
  name: RECALL_TOOL_NAME,
16815
- description: "Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an `id` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally.",
16845
+ description: `Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an \`id\` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally. For multi-faceted / overview questions ("tell me about the user", "what's my tech stack"), prefer several targeted searches \u2014 one facet each (e.g. name, work, hobbies) \u2014 over a single broad query; fuse the results yourself.`,
16816
16846
  arguments: {
16817
16847
  type: "object",
16818
16848
  properties: {
@@ -16867,6 +16897,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16867
16897
  let surfaced = 0;
16868
16898
  try {
16869
16899
  let recallDegraded = [];
16900
+ let subQueries;
16901
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
16902
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
16903
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
16904
+ subQueries = decomp.subQueries;
16905
+ }
16906
+ }
16870
16907
  const recallOpts = {
16871
16908
  onDiagnostics: (d) => {
16872
16909
  recallDegraded = d.degraded;
@@ -16880,10 +16917,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16880
16917
  ...toolOptions?.excludeConversationId && {
16881
16918
  excludeConversationId: toolOptions.excludeConversationId
16882
16919
  },
16883
- ...toolOptions?.decomposeOptions && {
16884
- decomposeOptions: toolOptions.decomposeOptions
16885
- },
16886
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
16920
+ // Do NOT forward decomposeOptions into recall() — rewrite already
16921
+ // ran above, and forwarding would falsely trip `decompose-moved`
16922
+ // on every specific-mode high-budget call. graphRefine auth can
16923
+ // be threaded later when the tool opts into that path.
16924
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
16925
+ ...subQueries && { subQueries }
16887
16926
  };
16888
16927
  const result = await recall(query, ctx, recallOpts);
16889
16928
  if (callbacks?.onChunksRetrieved) {
@@ -5009,15 +5009,26 @@ interface MemoryVaultSearchOptions {
5009
5009
  /** RRF smoothing constant for lane fusion. Default 60. */
5010
5010
  rrfK?: number;
5011
5011
  /**
5012
- * LLM-based query decomposition for composite/abstract queries. When set,
5013
- * each query is classified + (if composite) decomposed into 3–5 facet
5014
- * sub-queries via gpt-5-mini, then ranked via {@link rankComposite}.
5015
- * Requires `decomposeOptions` (auth) when set to "llm".
5012
+ * Pre-decomposed facet queries for the composite ranker (719/B4). When
5013
+ * ≥2 are supplied (and embeddings are available), runs {@link rankComposite}
5014
+ * over them — no LLM call inside the search path. Callers that want LLM
5015
+ * rewrite (e.g. `createRecallTool`) call `decomposeQuery` first and pass
5016
+ * the facets here.
5017
+ */
5018
+ subQueries?: string[];
5019
+ /**
5020
+ * @deprecated 719/B4 — ignored by {@link searchVaultMemories} /
5021
+ * {@link searchVaultMemoriesWithSize}. Pass {@link MemoryVaultSearchOptions.subQueries}
5022
+ * (or use `createRecallTool`). The legacy {@link createMemoryVaultSearchTool}
5023
+ * executor still honors `decompose: "llm"` + `decomposeOptions` for eval
5024
+ * parity, then forwards facets into the LLM-free search path.
5016
5025
  */
5017
5026
  decompose?: "off" | "llm";
5018
- /** Auth + endpoint for the decomposition LLM call. Required when
5019
- * decompose="llm". Auth is the dual pattern — one of `apiKey` /
5020
- * `getToken`; see {@link PortalLlmAuth}. */
5027
+ /**
5028
+ * @deprecated 719/B4 — see `decompose`. Ignored on the programmatic search
5029
+ * path; tool-layer rewrite still reads this from the search-tool options.
5030
+ * Prefer `RecallToolOptions.decomposeOptions` with `createRecallTool`.
5031
+ */
5021
5032
  decomposeOptions?: PortalLlmAuth & {
5022
5033
  baseUrl?: string;
5023
5034
  model?: string;
@@ -5182,9 +5193,11 @@ type MemoryKind = "fact" | "chunk";
5182
5193
  * Budget controls retrieval depth/cost. Higher budgets enable more
5183
5194
  * candidate sources and the cross-encoder reranker.
5184
5195
  *
5185
- * - `low`: cosine + BM25 fusion only. No reranker. Mobile default.
5186
- * - `mid`: + recency boost. Demo default.
5187
- * - `high`: + cross-encoder rerank stage. Demo `budget: high` path.
5196
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
5197
+ * - `mid`: + cross-encoder rerank.
5198
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
5199
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
5200
+ * not inside `recall()` itself (719/B4).
5188
5201
  */
5189
5202
  type Budget = "low" | "mid" | "high";
5190
5203
  interface ScoreBreakdown {
@@ -5284,10 +5297,28 @@ interface RecallOptions {
5284
5297
  /** Drop results below this score. Default: 0.1 for facts, 0.5 for chunks (mirrors today's defaults). */
5285
5298
  minScore?: number;
5286
5299
  /**
5287
- * Auth + endpoint for the LLM-based query decomposition pass. Without
5288
- * these, decompose is skipped even at `budget: 'high'`. Mirrors the
5289
- * shape used by `searchVaultMemories`. Auth is the dual pattern — one
5290
- * of `apiKey` / `getToken` is required; see {@link PortalLlmAuth}.
5300
+ * Pre-decomposed facet queries for the composite ranker. When ≥2 are
5301
+ * supplied, the vault lane runs `rankComposite` over them (no LLM call
5302
+ * inside `recall()` — 719/B4). Callers that still want LLM rewrite
5303
+ * (e.g. {@link createRecallTool} at `budget: 'high'`) call
5304
+ * `decomposeQuery` themselves and pass the result here. A single entry
5305
+ * (or omitting this) keeps the single-query path.
5306
+ */
5307
+ subQueries?: string[];
5308
+ /**
5309
+ * Auth + endpoint for optional LLM helpers that reuse portal auth —
5310
+ * currently {@link RecallOptions.graphRefine} neighbor selection.
5311
+ * Query decomposition is **not** driven by this field inside `recall()`
5312
+ * (719/B4); {@link createRecallTool} reads the same shape from
5313
+ * `RecallToolOptions.decomposeOptions` for tool-layer rewrite.
5314
+ *
5315
+ * Callers that still pass `{ budget: 'high', decomposeOptions }` without
5316
+ * {@link RecallOptions.subQueries} keep compiling but no longer rewrite —
5317
+ * `recall()` emits `decompose-moved` on {@link RecallDiagnostics.degraded}
5318
+ * so upgrades without a changelog read still leave a telemetry breadcrumb.
5319
+ *
5320
+ * Auth is the dual pattern — one of `apiKey` / `getToken` is required;
5321
+ * see {@link PortalLlmAuth}.
5291
5322
  */
5292
5323
  decomposeOptions?: PortalLlmAuth & {
5293
5324
  baseUrl?: string;
@@ -5391,9 +5422,22 @@ type RecallDegradation =
5391
5422
  /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5392
5423
  * this call — unavailable (e.g. React Native) or a transient failure. */
5393
5424
  "rerank-unavailable"
5394
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5395
- * decomposition was skipped and the budget downgraded to mid. */
5425
+ /**
5426
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
5427
+ * `high` no longer requires `decomposeOptions` and this signal is never
5428
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
5429
+ * the union so existing telemetry consumers stay typed.
5430
+ */
5396
5431
  | "decompose-unavailable"
5432
+ /**
5433
+ * `budget: 'high'` with `decomposeOptions` set but no usable
5434
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
5435
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
5436
+ * path (rerank + graph) and callers must pass facets (or use
5437
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
5438
+ * up in diagnostics instead of looking healthier than before.
5439
+ */
5440
+ | "decompose-moved"
5397
5441
  /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
5398
5442
  * alone: either the query embedding failed (or came back empty), or no
5399
5443
  * candidate had a vector to score against it because the row (re)embed failed.
@@ -7463,9 +7507,14 @@ declare function rankProfileCandidates(inputs: readonly ProfileSalienceInput[],
7463
7507
  * Unified Recall API — single retrieval surface above vault + engine.
7464
7508
  *
7465
7509
  * Budget maps to pipeline depth (mirrors Hindsight's tiered defaults):
7466
- * - `low` → V2 cosine + BM25 + recency (no rerank, no decompose)
7510
+ * - `low` → V2 cosine + BM25 + recency (no rerank)
7467
7511
  * - `mid` → V2 + cross-encoder rerank
7468
- * - `high` → V2 + rerank + LLM query decomposition for composite queries
7512
+ * - `high` → V2 + rerank + multi-hop graph traversal
7513
+ *
7514
+ * LLM-free by design (719/B4): query decomposition for composite asks
7515
+ * lives in the tool/agent layer (`createRecallTool`), which may pass
7516
+ * pre-built `subQueries` into this API. `recall()` itself never pays an
7517
+ * LLM RTT.
7469
7518
  *
7470
7519
  * Fact + chunk lanes are fused with RRF (k=60). The previous naive
7471
7520
  * "sort the union by raw score" path is gone — score scales differ
@@ -7520,7 +7569,8 @@ interface RecallToolOptions {
7520
7569
  folderId?: string | null;
7521
7570
  /** Exclude one conversation from chunk results (typically the active one). */
7522
7571
  excludeConversationId?: string;
7523
- /** LLM-decompose options; only used at budget="high". Auth follows the
7572
+ /** LLM-decompose options; only used at budget="high". Runs in THIS tool
7573
+ * executor (719/B4) — `recall()` itself is LLM-free. Auth follows the
7524
7574
  * dual pattern: apiKey (server/CLI) or getToken (browser identity
7525
7575
  * tokens) — at least one required. */
7526
7576
  decomposeOptions?: PortalLlmAuth & {
@@ -5009,15 +5009,26 @@ interface MemoryVaultSearchOptions {
5009
5009
  /** RRF smoothing constant for lane fusion. Default 60. */
5010
5010
  rrfK?: number;
5011
5011
  /**
5012
- * LLM-based query decomposition for composite/abstract queries. When set,
5013
- * each query is classified + (if composite) decomposed into 3–5 facet
5014
- * sub-queries via gpt-5-mini, then ranked via {@link rankComposite}.
5015
- * Requires `decomposeOptions` (auth) when set to "llm".
5012
+ * Pre-decomposed facet queries for the composite ranker (719/B4). When
5013
+ * ≥2 are supplied (and embeddings are available), runs {@link rankComposite}
5014
+ * over them — no LLM call inside the search path. Callers that want LLM
5015
+ * rewrite (e.g. `createRecallTool`) call `decomposeQuery` first and pass
5016
+ * the facets here.
5017
+ */
5018
+ subQueries?: string[];
5019
+ /**
5020
+ * @deprecated 719/B4 — ignored by {@link searchVaultMemories} /
5021
+ * {@link searchVaultMemoriesWithSize}. Pass {@link MemoryVaultSearchOptions.subQueries}
5022
+ * (or use `createRecallTool`). The legacy {@link createMemoryVaultSearchTool}
5023
+ * executor still honors `decompose: "llm"` + `decomposeOptions` for eval
5024
+ * parity, then forwards facets into the LLM-free search path.
5016
5025
  */
5017
5026
  decompose?: "off" | "llm";
5018
- /** Auth + endpoint for the decomposition LLM call. Required when
5019
- * decompose="llm". Auth is the dual pattern — one of `apiKey` /
5020
- * `getToken`; see {@link PortalLlmAuth}. */
5027
+ /**
5028
+ * @deprecated 719/B4 — see `decompose`. Ignored on the programmatic search
5029
+ * path; tool-layer rewrite still reads this from the search-tool options.
5030
+ * Prefer `RecallToolOptions.decomposeOptions` with `createRecallTool`.
5031
+ */
5021
5032
  decomposeOptions?: PortalLlmAuth & {
5022
5033
  baseUrl?: string;
5023
5034
  model?: string;
@@ -5182,9 +5193,11 @@ type MemoryKind = "fact" | "chunk";
5182
5193
  * Budget controls retrieval depth/cost. Higher budgets enable more
5183
5194
  * candidate sources and the cross-encoder reranker.
5184
5195
  *
5185
- * - `low`: cosine + BM25 fusion only. No reranker. Mobile default.
5186
- * - `mid`: + recency boost. Demo default.
5187
- * - `high`: + cross-encoder rerank stage. Demo `budget: high` path.
5196
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
5197
+ * - `mid`: + cross-encoder rerank.
5198
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
5199
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
5200
+ * not inside `recall()` itself (719/B4).
5188
5201
  */
5189
5202
  type Budget = "low" | "mid" | "high";
5190
5203
  interface ScoreBreakdown {
@@ -5284,10 +5297,28 @@ interface RecallOptions {
5284
5297
  /** Drop results below this score. Default: 0.1 for facts, 0.5 for chunks (mirrors today's defaults). */
5285
5298
  minScore?: number;
5286
5299
  /**
5287
- * Auth + endpoint for the LLM-based query decomposition pass. Without
5288
- * these, decompose is skipped even at `budget: 'high'`. Mirrors the
5289
- * shape used by `searchVaultMemories`. Auth is the dual pattern — one
5290
- * of `apiKey` / `getToken` is required; see {@link PortalLlmAuth}.
5300
+ * Pre-decomposed facet queries for the composite ranker. When ≥2 are
5301
+ * supplied, the vault lane runs `rankComposite` over them (no LLM call
5302
+ * inside `recall()` — 719/B4). Callers that still want LLM rewrite
5303
+ * (e.g. {@link createRecallTool} at `budget: 'high'`) call
5304
+ * `decomposeQuery` themselves and pass the result here. A single entry
5305
+ * (or omitting this) keeps the single-query path.
5306
+ */
5307
+ subQueries?: string[];
5308
+ /**
5309
+ * Auth + endpoint for optional LLM helpers that reuse portal auth —
5310
+ * currently {@link RecallOptions.graphRefine} neighbor selection.
5311
+ * Query decomposition is **not** driven by this field inside `recall()`
5312
+ * (719/B4); {@link createRecallTool} reads the same shape from
5313
+ * `RecallToolOptions.decomposeOptions` for tool-layer rewrite.
5314
+ *
5315
+ * Callers that still pass `{ budget: 'high', decomposeOptions }` without
5316
+ * {@link RecallOptions.subQueries} keep compiling but no longer rewrite —
5317
+ * `recall()` emits `decompose-moved` on {@link RecallDiagnostics.degraded}
5318
+ * so upgrades without a changelog read still leave a telemetry breadcrumb.
5319
+ *
5320
+ * Auth is the dual pattern — one of `apiKey` / `getToken` is required;
5321
+ * see {@link PortalLlmAuth}.
5291
5322
  */
5292
5323
  decomposeOptions?: PortalLlmAuth & {
5293
5324
  baseUrl?: string;
@@ -5391,9 +5422,22 @@ type RecallDegradation =
5391
5422
  /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5392
5423
  * this call — unavailable (e.g. React Native) or a transient failure. */
5393
5424
  "rerank-unavailable"
5394
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5395
- * decomposition was skipped and the budget downgraded to mid. */
5425
+ /**
5426
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
5427
+ * `high` no longer requires `decomposeOptions` and this signal is never
5428
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
5429
+ * the union so existing telemetry consumers stay typed.
5430
+ */
5396
5431
  | "decompose-unavailable"
5432
+ /**
5433
+ * `budget: 'high'` with `decomposeOptions` set but no usable
5434
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
5435
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
5436
+ * path (rerank + graph) and callers must pass facets (or use
5437
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
5438
+ * up in diagnostics instead of looking healthier than before.
5439
+ */
5440
+ | "decompose-moved"
5397
5441
  /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
5398
5442
  * alone: either the query embedding failed (or came back empty), or no
5399
5443
  * candidate had a vector to score against it because the row (re)embed failed.
@@ -7463,9 +7507,14 @@ declare function rankProfileCandidates(inputs: readonly ProfileSalienceInput[],
7463
7507
  * Unified Recall API — single retrieval surface above vault + engine.
7464
7508
  *
7465
7509
  * Budget maps to pipeline depth (mirrors Hindsight's tiered defaults):
7466
- * - `low` → V2 cosine + BM25 + recency (no rerank, no decompose)
7510
+ * - `low` → V2 cosine + BM25 + recency (no rerank)
7467
7511
  * - `mid` → V2 + cross-encoder rerank
7468
- * - `high` → V2 + rerank + LLM query decomposition for composite queries
7512
+ * - `high` → V2 + rerank + multi-hop graph traversal
7513
+ *
7514
+ * LLM-free by design (719/B4): query decomposition for composite asks
7515
+ * lives in the tool/agent layer (`createRecallTool`), which may pass
7516
+ * pre-built `subQueries` into this API. `recall()` itself never pays an
7517
+ * LLM RTT.
7469
7518
  *
7470
7519
  * Fact + chunk lanes are fused with RRF (k=60). The previous naive
7471
7520
  * "sort the union by raw score" path is gone — score scales differ
@@ -7520,7 +7569,8 @@ interface RecallToolOptions {
7520
7569
  folderId?: string | null;
7521
7570
  /** Exclude one conversation from chunk results (typically the active one). */
7522
7571
  excludeConversationId?: string;
7523
- /** LLM-decompose options; only used at budget="high". Auth follows the
7572
+ /** LLM-decompose options; only used at budget="high". Runs in THIS tool
7573
+ * executor (719/B4) — `recall()` itself is LLM-free. Auth follows the
7524
7574
  * dual pattern: apiKey (server/CLI) or getToken (browser identity
7525
7575
  * tokens) — at least one required. */
7526
7576
  decomposeOptions?: PortalLlmAuth & {
@@ -42,6 +42,7 @@ import {
42
42
  createVaultEmbeddingCache,
43
43
  createVaultMemoriesBatchOp,
44
44
  createVaultMemoryOp,
45
+ decomposeQuery,
45
46
  decryptField,
46
47
  decryptJsonField,
47
48
  deleteAllVaultMemoriesForUserOp,
@@ -119,7 +120,7 @@ import {
119
120
  upsertMessageOp,
120
121
  useEncryption,
121
122
  vaultMemoryToStored
122
- } from "./chunk-3CDSIN22.mjs";
123
+ } from "./chunk-SIMGZQ4P.mjs";
123
124
  import {
124
125
  PII_PATTERNS,
125
126
  PiiRedactor,
@@ -7814,7 +7815,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
7814
7815
  type: "function",
7815
7816
  function: {
7816
7817
  name: RECALL_TOOL_NAME,
7817
- description: "Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an `id` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally.",
7818
+ description: `Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an \`id\` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally. For multi-faceted / overview questions ("tell me about the user", "what's my tech stack"), prefer several targeted searches \u2014 one facet each (e.g. name, work, hobbies) \u2014 over a single broad query; fuse the results yourself.`,
7818
7819
  arguments: {
7819
7820
  type: "object",
7820
7821
  properties: {
@@ -7869,6 +7870,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
7869
7870
  let surfaced = 0;
7870
7871
  try {
7871
7872
  let recallDegraded = [];
7873
+ let subQueries;
7874
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
7875
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
7876
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
7877
+ subQueries = decomp.subQueries;
7878
+ }
7879
+ }
7872
7880
  const recallOpts = {
7873
7881
  onDiagnostics: (d) => {
7874
7882
  recallDegraded = d.degraded;
@@ -7882,10 +7890,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
7882
7890
  ...toolOptions?.excludeConversationId && {
7883
7891
  excludeConversationId: toolOptions.excludeConversationId
7884
7892
  },
7885
- ...toolOptions?.decomposeOptions && {
7886
- decomposeOptions: toolOptions.decomposeOptions
7887
- },
7888
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
7893
+ // Do NOT forward decomposeOptions into recall() — rewrite already
7894
+ // ran above, and forwarding would falsely trip `decompose-moved`
7895
+ // on every specific-mode high-budget call. graphRefine auth can
7896
+ // be threaded later when the tool opts into that path.
7897
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
7898
+ ...subQueries && { subQueries }
7889
7899
  };
7890
7900
  const result = await recall(query, ctx, recallOpts);
7891
7901
  if (callbacks?.onChunksRetrieved) {