@anuma/sdk 9.1.4 → 9.1.6

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.
@@ -183,6 +183,51 @@ interface EntityOperationsContext {
183
183
  canWrite?: () => Promise<boolean>;
184
184
  }
185
185
 
186
+ /**
187
+ * Class-B task types — the vocabulary of the `X-Anuma-Task-Type` header.
188
+ *
189
+ * A Class-B flow has ONE fixed purpose and no conversation: extract memories
190
+ * from a snippet, pick a topic, consolidate duplicates, classify a query. Today
191
+ * each of those builds its own system prompt on the client and sends it as a
192
+ * `role:system` message, which makes the prompt a suggestion rather than a
193
+ * contract — a caller can reword it into "you are a general assistant" and get
194
+ * general chat at internal-endpoint prices.
195
+ *
196
+ * The fix is that the SERVER owns the text and the client only names the task.
197
+ * This header is the name. The portal allowlists it against its own copy of this
198
+ * enum (ai-portal `internal/systemprompt/tasks.go`) and DROPS anything it does
199
+ * not recognise, so an unknown value degrades to "no task declared" rather than
200
+ * failing the request.
201
+ *
202
+ * ⚠ These string values are a wire contract with ai-portal. Adding one here
203
+ * before the portal knows it is harmless (it is dropped); renaming or removing
204
+ * one silently stops that flow being counted. Keep them in step with
205
+ * `AllTaskTypes` on the portal side, and see ai-portal
206
+ * `docs/BACKEND_OWNED_TASK_PROMPTS.md` for the rollout.
207
+ *
208
+ * ⚠ RELEASE ORDER. This union is a PUBLIC type, so a split is breaking in both
209
+ * directions and has to be sequenced:
210
+ *
211
+ * 1. merge + publish this package;
212
+ * 2. bump `@anuma/sdk` in the consumer and switch its call sites to the new
213
+ * names in the same commit.
214
+ *
215
+ * A consumer on the OLD version cannot compile the new names, and a consumer on
216
+ * the NEW version cannot compile the retired ones (`app_inspiration` is gone
217
+ * here, replaced by the three `app_inspiration_*` names). Both halves of that
218
+ * are type-only — the wire itself always degrades safely, because the portal
219
+ * drops any value it does not recognise. Concretely: ai-memoryless-client#5782
220
+ * is blocked on this publish for exactly that reason.
221
+ *
222
+ * Deliberately ABSENT: agent personas, `reflect()` when it answers the user, and
223
+ * the app-idea chat. Those carry an open conversation with the user, so a fixed
224
+ * server-owned prompt would be wrong rather than merely premature — the same
225
+ * boundary {@link INTERNAL_FLOW_MARKER} draws.
226
+ *
227
+ * @public
228
+ */
229
+ type TaskType = "title" | "commit_message" | "memory_extract" | "memory_import_extract" | "memory_dedup" | "memory_decay" | "memory_injection_check" | "memory_verify_support" | "memory_autosort" | "memory_topic" | "memory_consolidate" | "memory_decompose" | "memory_profile_synth" | "memory_graph" | "memory_recall_embed" | "memory_retain_embed" | "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";
230
+
186
231
  /**
187
232
  * Auth for portal LLM calls (extraction, consolidation, decomposition,
188
233
  * reflection). Mirrors `memoryEngine`'s `EmbeddingOptions` dual-auth:
@@ -202,6 +247,76 @@ interface PortalLlmAuth {
202
247
  getToken?: () => Promise<string | null>;
203
248
  }
204
249
 
250
+ /**
251
+ * Options for embedding generation
252
+ *
253
+ * Supports two auth methods:
254
+ * - `getToken`: For Privy identity tokens (uses Authorization: Bearer header)
255
+ * - `apiKey`: For direct API keys (uses X-API-Key header)
256
+ *
257
+ * At least one of `getToken` or `apiKey` must be provided.
258
+ */
259
+ interface EmbeddingOptions {
260
+ taskType?: TaskType;
261
+ /** Function to get auth token (e.g., Privy's getIdentityToken). Uses Authorization: Bearer header. */
262
+ getToken?: () => Promise<string | null>;
263
+ /** Direct API key for server-side usage. Uses X-API-Key header. */
264
+ apiKey?: string;
265
+ /** Base URL for the API */
266
+ baseUrl?: string;
267
+ /** Embedding model to use */
268
+ model?: string;
269
+ /** Max texts per API call for batch embeddings (default: 100). Larger arrays are split into chunks. */
270
+ batchSize?: number;
271
+ /**
272
+ * Optional in-memory cache for embedding vectors. When provided, texts
273
+ * are looked up in this map before calling the API, and new embeddings
274
+ * are stored after generation. Useful when the same texts are embedded
275
+ * repeatedly (e.g., across eval iterations or re-indexing runs).
276
+ *
277
+ * Values are stored as `Float32Array` (the embedding model's native
278
+ * precision) rather than a float64 `number[]`, halving the resident RAM of
279
+ * the cache with no precision loss. `generateEmbedding(s)` still return
280
+ * `number[]` at the API boundary, so callers are unaffected.
281
+ */
282
+ cache?: Map<string, Float32Array>;
283
+ /** Called after each embedding API call with the token usage from the response. */
284
+ onUsage?: (usage: {
285
+ promptTokens: number;
286
+ totalTokens: number;
287
+ }) => void;
288
+ /**
289
+ * Optional transform applied to each text immediately before it is sent to
290
+ * the embeddings endpoint (e.g. `PiiRedactor.maskText`). The cache and result
291
+ * ordering still key on the original text — only the API request body is
292
+ * transformed — so callers can keep storing/displaying the original value
293
+ * while real PII never reaches the server. Used when PII redaction is active.
294
+ */
295
+ maskInput?: (text: string) => string;
296
+ /**
297
+ * Deadline, in ms, for EACH embeddings HTTP attempt (default 15000). An
298
+ * attempt that exceeds it is aborted and counts as a transient failure, so the
299
+ * bounded retry still applies. `0` disables the deadline.
300
+ */
301
+ timeoutMs?: number;
302
+ /**
303
+ * Deadline, in ms, for the `getToken()` read that precedes a request (default
304
+ * 10000). A provider that never settles rejects instead of hanging the
305
+ * embedding — and the recall waiting on it. `0` disables the deadline.
306
+ */
307
+ tokenTimeoutMs?: number;
308
+ /**
309
+ * Overall deadline, in ms, for one `generateEmbedding` or `generateEmbeddings` call — the token read,
310
+ * every retry attempt and the backoff between them. Unset (the default) means
311
+ * only the per-attempt deadlines apply, which is right for background/bulk
312
+ * embeds. The recall query path sets it (see
313
+ * `RecallOptions.queryEmbedTotalTimeoutMs`) so an outage degrades a turn to
314
+ * BM25 within a few seconds instead of ~4 x `timeoutMs`. Batch calls share
315
+ * one budget across authentication, all chunks, retries and backoff.
316
+ */
317
+ totalTimeoutMs?: number;
318
+ }
319
+
205
320
  interface RecencyOptions {
206
321
  /** Override "now" — useful for deterministic tests and back-dated benchmarks. */
207
322
  now?: Date;
@@ -213,6 +328,13 @@ interface RecencyOptions {
213
328
  noDateMultiplier?: number;
214
329
  }
215
330
 
331
+ type VaultCorpusCache = WeakMap<VaultMemoryOperationsContext, Map<string, Promise<StoredVaultMemory[]>>>;
332
+ type RowRequests = Map<EmbeddingOptions["maskInput"], Map<string, Promise<number[]>>>;
333
+ interface VaultPreparation {
334
+ corpora: VaultCorpusCache;
335
+ rows: WeakMap<VaultMemoryOperationsContext, RowRequests>;
336
+ }
337
+
216
338
  /**
217
339
  * How a fact's evidence has been evolving.
218
340
  *
@@ -300,6 +422,7 @@ interface RankedMemory {
300
422
  updatedAt: Date;
301
423
  }
302
424
  interface RecallOptions {
425
+ /** @internal */ vaultPreparation?: VaultPreparation;
303
426
  /** Embedding cache shared with the rest of the turn, so the query is embedded once across lanes and the send. */
304
427
  embeddingCache?: Map<string, Float32Array>;
305
428
  /** Which kinds to search. Default: ['fact']. */
@@ -183,6 +183,51 @@ interface EntityOperationsContext {
183
183
  canWrite?: () => Promise<boolean>;
184
184
  }
185
185
 
186
+ /**
187
+ * Class-B task types — the vocabulary of the `X-Anuma-Task-Type` header.
188
+ *
189
+ * A Class-B flow has ONE fixed purpose and no conversation: extract memories
190
+ * from a snippet, pick a topic, consolidate duplicates, classify a query. Today
191
+ * each of those builds its own system prompt on the client and sends it as a
192
+ * `role:system` message, which makes the prompt a suggestion rather than a
193
+ * contract — a caller can reword it into "you are a general assistant" and get
194
+ * general chat at internal-endpoint prices.
195
+ *
196
+ * The fix is that the SERVER owns the text and the client only names the task.
197
+ * This header is the name. The portal allowlists it against its own copy of this
198
+ * enum (ai-portal `internal/systemprompt/tasks.go`) and DROPS anything it does
199
+ * not recognise, so an unknown value degrades to "no task declared" rather than
200
+ * failing the request.
201
+ *
202
+ * ⚠ These string values are a wire contract with ai-portal. Adding one here
203
+ * before the portal knows it is harmless (it is dropped); renaming or removing
204
+ * one silently stops that flow being counted. Keep them in step with
205
+ * `AllTaskTypes` on the portal side, and see ai-portal
206
+ * `docs/BACKEND_OWNED_TASK_PROMPTS.md` for the rollout.
207
+ *
208
+ * ⚠ RELEASE ORDER. This union is a PUBLIC type, so a split is breaking in both
209
+ * directions and has to be sequenced:
210
+ *
211
+ * 1. merge + publish this package;
212
+ * 2. bump `@anuma/sdk` in the consumer and switch its call sites to the new
213
+ * names in the same commit.
214
+ *
215
+ * A consumer on the OLD version cannot compile the new names, and a consumer on
216
+ * the NEW version cannot compile the retired ones (`app_inspiration` is gone
217
+ * here, replaced by the three `app_inspiration_*` names). Both halves of that
218
+ * are type-only — the wire itself always degrades safely, because the portal
219
+ * drops any value it does not recognise. Concretely: ai-memoryless-client#5782
220
+ * is blocked on this publish for exactly that reason.
221
+ *
222
+ * Deliberately ABSENT: agent personas, `reflect()` when it answers the user, and
223
+ * the app-idea chat. Those carry an open conversation with the user, so a fixed
224
+ * server-owned prompt would be wrong rather than merely premature — the same
225
+ * boundary {@link INTERNAL_FLOW_MARKER} draws.
226
+ *
227
+ * @public
228
+ */
229
+ type TaskType = "title" | "commit_message" | "memory_extract" | "memory_import_extract" | "memory_dedup" | "memory_decay" | "memory_injection_check" | "memory_verify_support" | "memory_autosort" | "memory_topic" | "memory_consolidate" | "memory_decompose" | "memory_profile_synth" | "memory_graph" | "memory_recall_embed" | "memory_retain_embed" | "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";
230
+
186
231
  /**
187
232
  * Auth for portal LLM calls (extraction, consolidation, decomposition,
188
233
  * reflection). Mirrors `memoryEngine`'s `EmbeddingOptions` dual-auth:
@@ -202,6 +247,76 @@ interface PortalLlmAuth {
202
247
  getToken?: () => Promise<string | null>;
203
248
  }
204
249
 
250
+ /**
251
+ * Options for embedding generation
252
+ *
253
+ * Supports two auth methods:
254
+ * - `getToken`: For Privy identity tokens (uses Authorization: Bearer header)
255
+ * - `apiKey`: For direct API keys (uses X-API-Key header)
256
+ *
257
+ * At least one of `getToken` or `apiKey` must be provided.
258
+ */
259
+ interface EmbeddingOptions {
260
+ taskType?: TaskType;
261
+ /** Function to get auth token (e.g., Privy's getIdentityToken). Uses Authorization: Bearer header. */
262
+ getToken?: () => Promise<string | null>;
263
+ /** Direct API key for server-side usage. Uses X-API-Key header. */
264
+ apiKey?: string;
265
+ /** Base URL for the API */
266
+ baseUrl?: string;
267
+ /** Embedding model to use */
268
+ model?: string;
269
+ /** Max texts per API call for batch embeddings (default: 100). Larger arrays are split into chunks. */
270
+ batchSize?: number;
271
+ /**
272
+ * Optional in-memory cache for embedding vectors. When provided, texts
273
+ * are looked up in this map before calling the API, and new embeddings
274
+ * are stored after generation. Useful when the same texts are embedded
275
+ * repeatedly (e.g., across eval iterations or re-indexing runs).
276
+ *
277
+ * Values are stored as `Float32Array` (the embedding model's native
278
+ * precision) rather than a float64 `number[]`, halving the resident RAM of
279
+ * the cache with no precision loss. `generateEmbedding(s)` still return
280
+ * `number[]` at the API boundary, so callers are unaffected.
281
+ */
282
+ cache?: Map<string, Float32Array>;
283
+ /** Called after each embedding API call with the token usage from the response. */
284
+ onUsage?: (usage: {
285
+ promptTokens: number;
286
+ totalTokens: number;
287
+ }) => void;
288
+ /**
289
+ * Optional transform applied to each text immediately before it is sent to
290
+ * the embeddings endpoint (e.g. `PiiRedactor.maskText`). The cache and result
291
+ * ordering still key on the original text — only the API request body is
292
+ * transformed — so callers can keep storing/displaying the original value
293
+ * while real PII never reaches the server. Used when PII redaction is active.
294
+ */
295
+ maskInput?: (text: string) => string;
296
+ /**
297
+ * Deadline, in ms, for EACH embeddings HTTP attempt (default 15000). An
298
+ * attempt that exceeds it is aborted and counts as a transient failure, so the
299
+ * bounded retry still applies. `0` disables the deadline.
300
+ */
301
+ timeoutMs?: number;
302
+ /**
303
+ * Deadline, in ms, for the `getToken()` read that precedes a request (default
304
+ * 10000). A provider that never settles rejects instead of hanging the
305
+ * embedding — and the recall waiting on it. `0` disables the deadline.
306
+ */
307
+ tokenTimeoutMs?: number;
308
+ /**
309
+ * Overall deadline, in ms, for one `generateEmbedding` or `generateEmbeddings` call — the token read,
310
+ * every retry attempt and the backoff between them. Unset (the default) means
311
+ * only the per-attempt deadlines apply, which is right for background/bulk
312
+ * embeds. The recall query path sets it (see
313
+ * `RecallOptions.queryEmbedTotalTimeoutMs`) so an outage degrades a turn to
314
+ * BM25 within a few seconds instead of ~4 x `timeoutMs`. Batch calls share
315
+ * one budget across authentication, all chunks, retries and backoff.
316
+ */
317
+ totalTimeoutMs?: number;
318
+ }
319
+
205
320
  interface RecencyOptions {
206
321
  /** Override "now" — useful for deterministic tests and back-dated benchmarks. */
207
322
  now?: Date;
@@ -213,6 +328,13 @@ interface RecencyOptions {
213
328
  noDateMultiplier?: number;
214
329
  }
215
330
 
331
+ type VaultCorpusCache = WeakMap<VaultMemoryOperationsContext, Map<string, Promise<StoredVaultMemory[]>>>;
332
+ type RowRequests = Map<EmbeddingOptions["maskInput"], Map<string, Promise<number[]>>>;
333
+ interface VaultPreparation {
334
+ corpora: VaultCorpusCache;
335
+ rows: WeakMap<VaultMemoryOperationsContext, RowRequests>;
336
+ }
337
+
216
338
  /**
217
339
  * How a fact's evidence has been evolving.
218
340
  *
@@ -300,6 +422,7 @@ interface RankedMemory {
300
422
  updatedAt: Date;
301
423
  }
302
424
  interface RecallOptions {
425
+ /** @internal */ vaultPreparation?: VaultPreparation;
303
426
  /** Embedding cache shared with the rest of the turn, so the query is embedded once across lanes and the send. */
304
427
  embeddingCache?: Map<string, Float32Array>;
305
428
  /** Which kinds to search. Default: ['fact']. */
@@ -723,6 +723,12 @@ function assertTransportMatchesEndpoint(transport, endpoint) {
723
723
  `endpointOverride "${endpoint}" is the other transport's endpoint, but transport is "${transport}". The request body is built for the transport, so this sends the wrong shape and 400s without retry.`
724
724
  );
725
725
  }
726
+ var TIME_BUDGET_EXHAUSTED = {
727
+ kind: "terminal",
728
+ code: "time-budget-exhausted",
729
+ reason: "time budget exhausted"
730
+ };
731
+ var MAX_TIMER_DELAY_MS = 2147483647;
726
732
  var RETRYABLE_HTTP = /* @__PURE__ */ new Set([408, 409, 425, 429]);
727
733
  function isRetryableStatus(status) {
728
734
  return status >= 500 || RETRYABLE_HTTP.has(status);
@@ -778,7 +784,8 @@ async function callPortalJsonCompletion(req) {
778
784
  }
779
785
  const maxAttempts = Math.max(1, req.maxAttempts ?? 3);
780
786
  const startedAt = Date.now();
781
- const overBudget = () => req.totalTimeoutMs !== void 0 && Date.now() - startedAt >= req.totalTimeoutMs;
787
+ const deadlineAt = req.totalTimeoutMs === void 0 ? void 0 : startedAt + req.totalTimeoutMs;
788
+ const overBudget = () => deadlineAt !== void 0 && Date.now() >= deadlineAt;
782
789
  let lastFailure;
783
790
  let reinforce = false;
784
791
  const giveUp = () => {
@@ -791,17 +798,11 @@ async function callPortalJsonCompletion(req) {
791
798
  lastFailure = { reason: "time-budget-exhausted", attempts: attempt - 1 };
792
799
  break;
793
800
  }
794
- const attemptReq = req.totalTimeoutMs !== void 0 ? {
795
- ...req,
796
- timeoutMs: Math.max(
797
- 1,
798
- Math.min(req.timeoutMs ?? 6e4, req.totalTimeoutMs - (Date.now() - startedAt))
799
- ),
800
- ...reinforce && { reinforceJsonContract: true }
801
- } : reinforce ? { ...req, reinforceJsonContract: true } : req;
801
+ const attemptReq = reinforce ? { ...req, reinforceJsonContract: true } : req;
802
802
  const outcome = await attemptPortalJson(
803
803
  { ...attemptReq, transport },
804
- endpoint
804
+ endpoint,
805
+ deadlineAt
805
806
  );
806
807
  if (outcome.kind === "ok") {
807
808
  req.onAttempt?.({ attempt, ok: true });
@@ -841,13 +842,38 @@ async function callPortalJsonCompletion(req) {
841
842
  }
842
843
  return giveUp();
843
844
  }
844
- async function attemptPortalJson(req, endpoint) {
845
+ async function attemptPortalJson(req, endpoint, deadlineAt) {
846
+ const controller = new AbortController();
847
+ if (deadlineAt === void 0 || deadlineAt - Date.now() > MAX_TIMER_DELAY_MS) {
848
+ return requestPortalJson(req, endpoint, controller);
849
+ }
850
+ const remainingMs = deadlineAt - Date.now();
851
+ if (remainingMs <= 0) return TIME_BUDGET_EXHAUSTED;
852
+ let timer;
853
+ try {
854
+ const outcome = await Promise.race([
855
+ new Promise((resolve) => {
856
+ timer = setTimeout(() => {
857
+ resolve(TIME_BUDGET_EXHAUSTED);
858
+ controller.abort();
859
+ }, remainingMs);
860
+ }),
861
+ requestPortalJson(req, endpoint, controller, deadlineAt)
862
+ ]);
863
+ return Date.now() >= deadlineAt ? TIME_BUDGET_EXHAUSTED : outcome;
864
+ } finally {
865
+ clearTimeout(timer);
866
+ }
867
+ }
868
+ async function requestPortalJson(req, endpoint, controller, deadlineAt) {
845
869
  const baseUrl = req.baseUrl ?? defaultBaseUrl();
846
870
  const fetchImpl = req.fetchFn ?? fetch;
847
- const timeoutMs = req.timeoutMs ?? 6e4;
848
871
  const authHeaders = await resolvePortalAuthHeaders(req, req.tag);
872
+ const remainingMs = deadlineAt === void 0 ? Infinity : deadlineAt - Date.now();
873
+ if (controller.signal.aborted || remainingMs <= 0) return TIME_BUDGET_EXHAUSTED;
849
874
  if (authHeaders === null)
850
875
  return { kind: "terminal", code: "auth-unavailable", reason: "auth unavailable (no token)" };
876
+ const timeoutMs = Math.min(req.timeoutMs ?? 6e4, remainingMs);
851
877
  const usesPrefill = req.transport !== "responses" && req.model.startsWith("anthropic/");
852
878
  const prefill = usesPrefill ? "{" : "";
853
879
  const messages = [
@@ -857,7 +883,6 @@ async function attemptPortalJson(req, endpoint) {
857
883
  ];
858
884
  if (prefill) messages.push({ role: "assistant", content: prefill });
859
885
  const supportsJsonObjectFormat = supportsResponseFormat(req.model);
860
- const controller = new AbortController();
861
886
  const timer = setTimeout(() => controller.abort(), timeoutMs);
862
887
  const requestBody = req.transport === "responses" ? {
863
888
  model: req.model,
@@ -4,7 +4,7 @@ import {
4
4
  notifyConsolidationFallback,
5
5
  requiresResponsesTransport,
6
6
  resolvePiiRedactor
7
- } from "./chunk-7QX4J7KV.mjs";
7
+ } from "./chunk-RSA3S6OA.mjs";
8
8
  import "./chunk-GYKJOWEO.mjs";
9
9
 
10
10
  // src/lib/memory/consolidate.ts