@maximem/synap-js-sdk 0.3.0 → 0.4.1

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.
Files changed (55) hide show
  1. package/MIGRATING.md +147 -0
  2. package/README.md +95 -82
  3. package/bin/synap-js-sdk.js +45 -122
  4. package/dist/anticipation-cache-ARYEEKMU.cjs +10 -0
  5. package/dist/anticipation-cache-ARYEEKMU.cjs.map +1 -0
  6. package/dist/anticipation-cache-BMVF5sXb.d.cts +190 -0
  7. package/dist/anticipation-cache-BMVF5sXb.d.ts +190 -0
  8. package/dist/anticipation-cache-H7HEVWQC.js +10 -0
  9. package/dist/anticipation-cache-H7HEVWQC.js.map +1 -0
  10. package/dist/chunk-5SC3ELNK.js +856 -0
  11. package/dist/chunk-5SC3ELNK.js.map +1 -0
  12. package/dist/chunk-6LDUDKHS.js +832 -0
  13. package/dist/chunk-6LDUDKHS.js.map +1 -0
  14. package/dist/chunk-HR5Y3KXS.cjs +172 -0
  15. package/dist/chunk-HR5Y3KXS.cjs.map +1 -0
  16. package/dist/chunk-QBCVMPGK.js +28 -0
  17. package/dist/chunk-QBCVMPGK.js.map +1 -0
  18. package/dist/chunk-RB57I2IQ.cjs +832 -0
  19. package/dist/chunk-RB57I2IQ.cjs.map +1 -0
  20. package/dist/chunk-T3NZBIV2.js +172 -0
  21. package/dist/chunk-T3NZBIV2.js.map +1 -0
  22. package/dist/chunk-VXRCBMCY.cjs +856 -0
  23. package/dist/chunk-VXRCBMCY.cjs.map +1 -0
  24. package/dist/chunk-ZKOJDUN4.cjs +28 -0
  25. package/dist/chunk-ZKOJDUN4.cjs.map +1 -0
  26. package/dist/env-55ULHNJZ.cjs +11 -0
  27. package/dist/env-55ULHNJZ.cjs.map +1 -0
  28. package/dist/env-QCUKKW36.js +11 -0
  29. package/dist/env-QCUKKW36.js.map +1 -0
  30. package/dist/grpc/index.cjs +87 -0
  31. package/dist/grpc/index.cjs.map +1 -0
  32. package/dist/grpc/index.d.cts +162 -0
  33. package/dist/grpc/index.d.ts +162 -0
  34. package/dist/grpc/index.js +87 -0
  35. package/dist/grpc/index.js.map +1 -0
  36. package/dist/index.cjs +2702 -0
  37. package/dist/index.cjs.map +1 -0
  38. package/dist/index.d.cts +1491 -0
  39. package/dist/index.d.ts +1491 -0
  40. package/dist/index.js +2702 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/stream-client-ME5N5NK4.js +7 -0
  43. package/dist/stream-client-ME5N5NK4.js.map +1 -0
  44. package/dist/stream-client-ZN6ADZBR.cjs +7 -0
  45. package/dist/stream-client-ZN6ADZBR.cjs.map +1 -0
  46. package/package.json +68 -23
  47. package/.env.example +0 -39
  48. package/bridge/synap_bridge.py +0 -768
  49. package/src/bridge-manager.js +0 -262
  50. package/src/errors.js +0 -168
  51. package/src/index.js +0 -13
  52. package/src/runtime.js +0 -161
  53. package/src/setup-typescript.js +0 -161
  54. package/src/synap-client.js +0 -464
  55. package/types/index.d.ts +0 -475
package/dist/index.js ADDED
@@ -0,0 +1,2702 @@
1
+ import {
2
+ AgentUnavailableError,
3
+ AuthenticationError,
4
+ ConflictError,
5
+ ConnectionError,
6
+ ContextNotFoundError,
7
+ InsufficientCreditsError,
8
+ InvalidConversationIdError,
9
+ InvalidInputError,
10
+ InvalidInstanceIdError,
11
+ ListeningAlreadyActiveError,
12
+ ListeningNotActiveError,
13
+ NetworkTimeoutError,
14
+ PermanentError,
15
+ RateLimitError,
16
+ SDKError,
17
+ ServiceUnavailableError,
18
+ SessionExpiredError,
19
+ SynapError,
20
+ SynapPermanentError,
21
+ SynapTransientError,
22
+ TranscriptConflictError,
23
+ TransientError,
24
+ isSynapError
25
+ } from "./chunk-T3NZBIV2.js";
26
+ import {
27
+ ANY_SCOPE,
28
+ AnticipationCache,
29
+ BM25,
30
+ CONTRACT,
31
+ THRESHOLDS,
32
+ isRecallQuery,
33
+ stem,
34
+ tokenize
35
+ } from "./chunk-6LDUDKHS.js";
36
+ import {
37
+ getEnv
38
+ } from "./chunk-QBCVMPGK.js";
39
+
40
+ // src/util/correlation.ts
41
+ function newCorrelationId() {
42
+ const c = globalThis.crypto;
43
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
44
+ if (typeof c?.getRandomValues === "function") {
45
+ const b = c.getRandomValues(new Uint8Array(16));
46
+ b[6] = (b[6] ?? 0) & 15 | 64;
47
+ b[8] = (b[8] ?? 0) & 63 | 128;
48
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
49
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
50
+ }
51
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
52
+ const r = Math.random() * 16 | 0;
53
+ return (ch === "x" ? r : r & 3 | 8).toString(16);
54
+ });
55
+ }
56
+
57
+ // src/transport/endpoints.ts
58
+ var ENDPOINTS = {
59
+ whoami: { method: "GET", path: "/api/v1/auth/whoami", idempotent: true },
60
+ memories_create: { method: "POST", path: "/api/v1/memories/create", idempotent: false },
61
+ memories_batch: { method: "POST", path: "/api/v1/memories/batch", idempotent: false },
62
+ memories_upload: { method: "POST", path: "/api/v1/memories/upload", idempotent: false },
63
+ memories_status: { method: "GET", path: "/api/v1/memories/status/{ingestion_id}", idempotent: true },
64
+ memories_get: { method: "GET", path: "/api/v1/memories/{memory_id}", idempotent: true },
65
+ memories_update: { method: "PATCH", path: "/api/v1/memories/{memory_id}", idempotent: false },
66
+ memories_delete: { method: "DELETE", path: "/api/v1/memories/{memory_id}", idempotent: true },
67
+ context_fetch_user: { method: "POST", path: "/v1/context/user/fetch", idempotent: true },
68
+ context_fetch_customer: { method: "POST", path: "/v1/context/customer/fetch", idempotent: true },
69
+ context_fetch_client: { method: "POST", path: "/v1/context/client/fetch", idempotent: true },
70
+ context_fetch_conversation: { method: "POST", path: "/v1/context/conversation/fetch", idempotent: true },
71
+ context_for_prompt: { method: "GET", path: "/v1/conversations/{conversation_id}/context-for-prompt", idempotent: true },
72
+ conversations_ingest: { method: "POST", path: "/v1/conversations/ingest", idempotent: false },
73
+ conversations_messages: { method: "POST", path: "/v1/conversations/messages", idempotent: false },
74
+ conversations_messages_batch: { method: "POST", path: "/v1/conversations/messages/batch", idempotent: false },
75
+ conversations_compact: { method: "POST", path: "/v1/conversations/compact", idempotent: false },
76
+ conversations_compacted: { method: "GET", path: "/v1/conversations/{conversation_id}/compacted", idempotent: true },
77
+ conversations_compaction_status: { method: "GET", path: "/v1/conversations/{conversation_id}/compaction/status", idempotent: true },
78
+ user_profile: { method: "GET", path: "/v1/users/{user_id}/profile", idempotent: true },
79
+ credits_balance: { method: "GET", path: "/v1/credits/balance", idempotent: true },
80
+ credits_ledger: { method: "GET", path: "/v1/credits/ledger", idempotent: true },
81
+ credits_estimate: { method: "POST", path: "/v1/credits/estimate", idempotent: true },
82
+ credits_redeem: { method: "POST", path: "/v1/credits/redeem", idempotent: false },
83
+ telemetry_batch: { method: "POST", path: "/v1/telemetry/batch", idempotent: false }
84
+ };
85
+ function resolvePath(name, params = {}) {
86
+ return ENDPOINTS[name].path.replace(/\{(\w+)\}/g, (_m, key) => {
87
+ const value = params[key];
88
+ if (value === void 0 || value === null || value === "") {
89
+ throw new Error(
90
+ `Endpoint ${name} requires path parameter '${key}'.`
91
+ );
92
+ }
93
+ return encodeURIComponent(String(value));
94
+ });
95
+ }
96
+
97
+ // src/transport/retry.ts
98
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
99
+ "GET",
100
+ "HEAD",
101
+ "OPTIONS",
102
+ "PUT",
103
+ "DELETE"
104
+ ]);
105
+ var DEFAULT_RETRY_POLICY = {
106
+ maxAttempts: 3,
107
+ backoffBase: 1,
108
+ backoffMax: 10,
109
+ backoffJitter: true
110
+ };
111
+ function shouldRetry(input) {
112
+ const { error, attempt, policy, method, outcomeUnknown } = input;
113
+ if (attempt >= policy.maxAttempts) {
114
+ return { retry: false, delaySeconds: 0, reason: "max_attempts_exhausted" };
115
+ }
116
+ if (!isSynapError(error) || !error.transient) {
117
+ return { retry: false, delaySeconds: 0, reason: "not_transient" };
118
+ }
119
+ const idempotent = input.idempotent ?? IDEMPOTENT_METHODS.has(method.toUpperCase());
120
+ if (outcomeUnknown && !idempotent) {
121
+ return {
122
+ retry: false,
123
+ delaySeconds: 0,
124
+ reason: "non_idempotent_outcome_unknown"
125
+ };
126
+ }
127
+ return {
128
+ retry: true,
129
+ delaySeconds: backoffDelay(error, attempt, policy),
130
+ reason: "transient_retryable"
131
+ };
132
+ }
133
+ function backoffDelay(error, attempt, policy) {
134
+ if (error instanceof RateLimitError && error.retryAfterSeconds != null) {
135
+ return Math.min(error.retryAfterSeconds, policy.backoffMax);
136
+ }
137
+ const exponential = policy.backoffBase * 2 ** (attempt - 1);
138
+ const capped = Math.min(exponential, policy.backoffMax);
139
+ if (!policy.backoffJitter) return capped;
140
+ return Math.random() * capped;
141
+ }
142
+
143
+ // src/transport/keepalive.ts
144
+ var KEEPALIVE_EXPIRY_MS = 3e5;
145
+ var HEARTBEAT_INTERVAL_MS = 24e4;
146
+ var MAX_CONNECTIONS = 20;
147
+ var cached;
148
+ function supportsDispatcher() {
149
+ return typeof process !== "undefined" && process.versions?.node !== void 0 && // Edge builds of Next expose a partial `process` shim, but no real Node.
150
+ typeof globalThis.EdgeRuntime === "undefined";
151
+ }
152
+ async function getKeepAliveDispatcher(options = {}) {
153
+ if (cached !== void 0) return cached;
154
+ if (!supportsDispatcher()) {
155
+ cached = null;
156
+ return cached;
157
+ }
158
+ try {
159
+ const undici = await import(
160
+ /* @vite-ignore */
161
+ "undici"
162
+ );
163
+ cached = new undici.Agent({
164
+ keepAliveTimeout: options.keepAliveExpiryMs ?? KEEPALIVE_EXPIRY_MS,
165
+ // undici will not honour a keepAliveTimeout above this ceiling.
166
+ keepAliveMaxTimeout: options.keepAliveExpiryMs ?? KEEPALIVE_EXPIRY_MS,
167
+ connections: options.maxConnections ?? MAX_CONNECTIONS
168
+ });
169
+ } catch {
170
+ cached = null;
171
+ }
172
+ return cached;
173
+ }
174
+ var Heartbeat = class {
175
+ constructor(ping, intervalMs = HEARTBEAT_INTERVAL_MS) {
176
+ this.ping = ping;
177
+ this.intervalMs = intervalMs;
178
+ }
179
+ ping;
180
+ intervalMs;
181
+ timer = null;
182
+ start() {
183
+ if (this.timer !== null) return;
184
+ if (this.intervalMs <= 0) return;
185
+ if (typeof setInterval !== "function") return;
186
+ this.timer = setInterval(() => {
187
+ void Promise.resolve(this.ping()).catch(() => {
188
+ });
189
+ }, this.intervalMs);
190
+ this.timer.unref?.();
191
+ }
192
+ stop() {
193
+ if (this.timer !== null) {
194
+ clearInterval(this.timer);
195
+ this.timer = null;
196
+ }
197
+ }
198
+ get running() {
199
+ return this.timer !== null;
200
+ }
201
+ };
202
+
203
+ // src/transport/http.ts
204
+ var DEFAULT_BASE_URL = "https://synap-cloud-prod.maximem.ai";
205
+ var DEFAULT_TIMEOUTS = { connect: 5, read: 30, write: 10 };
206
+ var HttpTransport = class {
207
+ baseUrl;
208
+ // Mutable so `configure()` can adjust them before the first request, and so
209
+ // `initialize()` can write back the identity resolved from whoami.
210
+ timeouts;
211
+ retryPolicy;
212
+ #credentials;
213
+ userAgent;
214
+ fetchImpl;
215
+ keepAliveOptions;
216
+ heartbeat;
217
+ closed = false;
218
+ constructor(options) {
219
+ this.#credentials = options.credentials;
220
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
221
+ this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
222
+ this.retryPolicy = { ...DEFAULT_RETRY_POLICY, ...options.retryPolicy };
223
+ this.userAgent = options.userAgent ?? "@maximem/synap-js-sdk";
224
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
225
+ this.keepAliveOptions = options.keepAlive ?? {};
226
+ this.heartbeat = new Heartbeat(
227
+ () => this.pingHealth(),
228
+ options.keepAlive?.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS
229
+ );
230
+ }
231
+ /** Start keeping the connection warm. Opt-in: it is pointless in serverless. */
232
+ startHeartbeat() {
233
+ if (!this.closed) this.heartbeat.start();
234
+ }
235
+ /**
236
+ * Release timers. Safe to call more than once.
237
+ *
238
+ * This is the supported shutdown path, deliberately explicit rather than
239
+ * hooked to `process.on('exit')`: those handlers leak a listener per client,
240
+ * never fire in Lambda, and `process` does not exist in Workers (gotcha G-I).
241
+ */
242
+ /**
243
+ * A COPY of the current credentials.
244
+ *
245
+ * A copy, not the live object: handing out the internal reference is how
246
+ * `client.transport.credentials.apiKey` became readable and mutable from
247
+ * outside in the first place.
248
+ */
249
+ currentCredentials() {
250
+ return { ...this.#credentials };
251
+ }
252
+ /** Write back identity resolved from `GET /api/v1/auth/whoami`. */
253
+ updateCredentials(patch) {
254
+ this.#credentials = { ...this.#credentials, ...patch };
255
+ }
256
+ setTimeouts(timeouts) {
257
+ this.timeouts = { ...this.timeouts, ...timeouts };
258
+ }
259
+ /** `null` restores the default policy, matching Python's `retry_policy=None`. */
260
+ setRetryPolicy(policy) {
261
+ this.retryPolicy = policy === null ? { ...DEFAULT_RETRY_POLICY } : { ...this.retryPolicy, ...policy };
262
+ }
263
+ close() {
264
+ this.closed = true;
265
+ this.heartbeat.stop();
266
+ }
267
+ async request(name, options = {}) {
268
+ const spec = ENDPOINTS[name];
269
+ const correlationId = options.correlationId ?? newCorrelationId();
270
+ const idempotent = options.idempotent ?? spec.idempotent;
271
+ let attempt = 0;
272
+ let last = null;
273
+ while (attempt < this.retryPolicy.maxAttempts) {
274
+ attempt += 1;
275
+ try {
276
+ return await this.attempt(name, options, correlationId);
277
+ } catch (error) {
278
+ last = { error, outcomeUnknown: isOutcomeUnknown(error) };
279
+ const decision = shouldRetry({
280
+ error,
281
+ attempt,
282
+ policy: this.retryPolicy,
283
+ method: spec.method,
284
+ outcomeUnknown: last.outcomeUnknown,
285
+ idempotent
286
+ });
287
+ if (!decision.retry) throw error;
288
+ await sleep(decision.delaySeconds * 1e3, options.signal);
289
+ }
290
+ }
291
+ throw last?.error ?? new SynapError("Request failed with no recorded error");
292
+ }
293
+ async attempt(name, options, correlationId) {
294
+ const spec = ENDPOINTS[name];
295
+ const url = this.buildUrl(resolvePath(name, options.pathParams), options.query);
296
+ const dispatcher = await getKeepAliveDispatcher(this.keepAliveOptions);
297
+ const signal = withTimeout(this.timeouts.read * 1e3, options.signal);
298
+ const init = {
299
+ method: spec.method,
300
+ headers: this.buildHeaders(correlationId, { json: options.rawBody !== true }),
301
+ signal: signal.signal
302
+ };
303
+ if (options.body !== void 0 && spec.method !== "GET") {
304
+ init.body = options.rawBody === true ? options.body : JSON.stringify(options.body);
305
+ }
306
+ if (dispatcher) init["dispatcher"] = dispatcher;
307
+ let response;
308
+ try {
309
+ response = await this.fetchImpl(url, init);
310
+ } catch (error) {
311
+ throw toTransportError(error, correlationId);
312
+ } finally {
313
+ signal.dispose();
314
+ }
315
+ if (!response.ok) {
316
+ throw await toHttpError(response, correlationId);
317
+ }
318
+ if (response.status === 204) return void 0;
319
+ const text = await response.text();
320
+ if (!text) return void 0;
321
+ try {
322
+ return JSON.parse(text);
323
+ } catch {
324
+ throw new SynapError(`Malformed JSON in response from ${url}`, { correlationId });
325
+ }
326
+ }
327
+ buildUrl(path, query) {
328
+ const url = new URL(this.baseUrl + path);
329
+ for (const [k, v] of Object.entries(query ?? {})) {
330
+ if (v !== void 0) url.searchParams.set(k, String(v));
331
+ }
332
+ return url.toString();
333
+ }
334
+ buildHeaders(correlationId, options = {}) {
335
+ return {
336
+ Authorization: `Bearer ${this.#credentials.apiKey}`,
337
+ "X-Client-ID": this.#credentials.clientId,
338
+ "X-Instance-ID": this.#credentials.instanceId,
339
+ "X-Correlation-ID": correlationId,
340
+ ...options.json === false ? {} : { "Content-Type": "application/json" },
341
+ "User-Agent": this.userAgent
342
+ };
343
+ }
344
+ /** Unauthenticated and unmetered, exactly like the Python heartbeat. */
345
+ async pingHealth() {
346
+ const signal = withTimeout(1e4);
347
+ try {
348
+ const dispatcher = await getKeepAliveDispatcher(this.keepAliveOptions);
349
+ const init = { signal: signal.signal };
350
+ if (dispatcher) init["dispatcher"] = dispatcher;
351
+ await this.fetchImpl(`${this.baseUrl}/health`, init);
352
+ } finally {
353
+ signal.dispose();
354
+ }
355
+ }
356
+ };
357
+ function isOutcomeUnknown(error) {
358
+ if (error instanceof RateLimitError) return false;
359
+ if (error instanceof ServiceUnavailableError) return false;
360
+ if (error instanceof AuthenticationError) return false;
361
+ if (error instanceof InvalidInputError) return false;
362
+ if (error instanceof ConflictError) return false;
363
+ if (error instanceof InsufficientCreditsError) return false;
364
+ if (error instanceof ContextNotFoundError) return false;
365
+ if (error instanceof NetworkTimeoutError) return true;
366
+ if (isConnectionRefusal(error)) return false;
367
+ return true;
368
+ }
369
+ function isConnectionRefusal(error) {
370
+ const code = error?.cause?.code ?? error?.code;
371
+ return code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN" || code === "ECONNRESET" || code === "EHOSTUNREACH";
372
+ }
373
+ function toTransportError(error, correlationId) {
374
+ if (error instanceof SynapError) return error;
375
+ const name = error?.name;
376
+ if (name === "AbortError" || name === "TimeoutError") {
377
+ return new NetworkTimeoutError("Request timed out", { correlationId, cause: error });
378
+ }
379
+ return new ServiceUnavailableError(
380
+ `Network request failed: ${error?.message ?? String(error)}`,
381
+ { correlationId, cause: error }
382
+ );
383
+ }
384
+ async function toHttpError(response, correlationId) {
385
+ const body = await response.text().catch(() => "");
386
+ let parsed = {};
387
+ try {
388
+ parsed = JSON.parse(body);
389
+ } catch {
390
+ }
391
+ const rawDetail = parsed["detail"] ?? parsed["message"] ?? body ?? "";
392
+ const detailObject = typeof rawDetail === "object" && rawDetail !== null ? rawDetail : null;
393
+ const detail = detailObject !== null ? String(detailObject["message"] ?? JSON.stringify(detailObject)) : String(rawDetail);
394
+ const message = `HTTP ${response.status}: ${detail || response.statusText}`;
395
+ const opts = { correlationId };
396
+ switch (response.status) {
397
+ case 400:
398
+ case 422:
399
+ return new InvalidInputError(message, opts);
400
+ case 401:
401
+ case 403:
402
+ return new AuthenticationError(message, opts);
403
+ case 404:
404
+ return new ContextNotFoundError(message, opts);
405
+ case 402:
406
+ return new InsufficientCreditsError(message, {
407
+ ...opts,
408
+ balanceCredits: numberOrNull(parsed["balance_credits"]),
409
+ requiredCredits: numberOrNull(parsed["required_credits"])
410
+ });
411
+ case 409:
412
+ if (detailObject !== null && detailObject["code"] === "transcript_conflict") {
413
+ return new TranscriptConflictError(message, opts);
414
+ }
415
+ return new ConflictError(message, opts);
416
+ case 429: {
417
+ const header = response.headers.get("retry-after");
418
+ const retryAfter = header !== null && header.trim() !== "" ? Number(header) : null;
419
+ return new RateLimitError(message, {
420
+ ...opts,
421
+ retryAfterSeconds: retryAfter !== null && Number.isFinite(retryAfter) ? retryAfter : null
422
+ });
423
+ }
424
+ case 503:
425
+ return new ServiceUnavailableError(message, opts);
426
+ case 504:
427
+ return new AgentUnavailableError(message, opts);
428
+ default:
429
+ if (response.status >= 500) return new ServiceUnavailableError(message, opts);
430
+ return new SynapError(message, opts);
431
+ }
432
+ }
433
+ function numberOrNull(v) {
434
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
435
+ }
436
+ function withTimeout(ms, external) {
437
+ const controller = new AbortController();
438
+ const onExternalAbort = () => controller.abort(external?.reason);
439
+ const timer = setTimeout(() => {
440
+ controller.abort(new NetworkTimeoutError(`Timed out after ${ms}ms`));
441
+ }, ms);
442
+ timer.unref?.();
443
+ if (external) {
444
+ if (external.aborted) onExternalAbort();
445
+ else external.addEventListener("abort", onExternalAbort, { once: true });
446
+ }
447
+ return {
448
+ signal: controller.signal,
449
+ dispose() {
450
+ clearTimeout(timer);
451
+ external?.removeEventListener("abort", onExternalAbort);
452
+ }
453
+ };
454
+ }
455
+ function sleep(ms, signal) {
456
+ if (ms <= 0) return Promise.resolve();
457
+ return new Promise((resolve, reject) => {
458
+ const timer = setTimeout(() => {
459
+ signal?.removeEventListener("abort", onAbort);
460
+ resolve();
461
+ }, ms);
462
+ function onAbort() {
463
+ clearTimeout(timer);
464
+ reject(signal?.reason ?? new Error("Aborted"));
465
+ }
466
+ signal?.addEventListener("abort", onAbort, { once: true });
467
+ });
468
+ }
469
+
470
+ // src/context/fetch.ts
471
+ var ENDPOINT_BY_SCOPE = {
472
+ user: "context_fetch_user",
473
+ customer: "context_fetch_customer",
474
+ client: "context_fetch_client",
475
+ conversation: "context_fetch_conversation"
476
+ };
477
+ function opt(options, snake, camel) {
478
+ const a = options[snake];
479
+ if (a !== void 0) return a;
480
+ const b = options[camel];
481
+ return b === void 0 ? void 0 : b;
482
+ }
483
+ function buildFetchRequest(scope, options) {
484
+ const userId = opt(options, "user_id", "userId");
485
+ const customerId = opt(options, "customer_id", "customerId");
486
+ const conversationId = opt(options, "conversation_id", "conversationId");
487
+ const searchQuery = opt(options, "search_query", "searchQuery");
488
+ const maxResults = opt(options, "max_results", "maxResults") ?? 10;
489
+ const precisionLevel = opt(options, "precision_level", "precisionLevel") ?? "high";
490
+ const mode = options.mode ?? "fast";
491
+ if (searchQuery !== void 0 && !Array.isArray(searchQuery)) {
492
+ throw new InvalidInputError("search_query must be an array when provided");
493
+ }
494
+ if (options.types !== void 0 && !Array.isArray(options.types)) {
495
+ throw new InvalidInputError("types must be an array when provided");
496
+ }
497
+ const body = {
498
+ conversation_id: conversationId ?? null,
499
+ search_query: searchQuery ?? [],
500
+ max_results: maxResults,
501
+ types: options.types ?? ["all"],
502
+ mode
503
+ };
504
+ if (precisionLevel !== "high") body["precision_level"] = precisionLevel;
505
+ if (options.include_conversation_context === false) {
506
+ body["include_conversation_context"] = false;
507
+ }
508
+ switch (scope) {
509
+ case "user":
510
+ if (!userId) throw new InvalidInputError("user_id is required");
511
+ body["user_id"] = userId;
512
+ if (customerId !== void 0) body["customer_id"] = customerId;
513
+ break;
514
+ case "customer":
515
+ if (!customerId) throw new InvalidInputError("customer_id is required");
516
+ body["customer_id"] = customerId;
517
+ break;
518
+ case "conversation":
519
+ if (!conversationId) throw new InvalidInputError("conversation_id is required");
520
+ if (userId !== void 0) body["user_id"] = userId;
521
+ if (customerId !== void 0) body["customer_id"] = customerId;
522
+ break;
523
+ case "client":
524
+ break;
525
+ }
526
+ return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
527
+ }
528
+ async function fetchContext(transport, scope, options = {}, hooks = {}) {
529
+ const startedAt = Date.now();
530
+ const anticipated = hooks.beforeFetch?.(scope, options) ?? null;
531
+ if (anticipated !== null) {
532
+ hooks.onServedFromCache?.(scope, options, anticipated);
533
+ hooks.onAssembled?.(scope, options, anticipated.response, startedAt);
534
+ return anticipated.response;
535
+ }
536
+ const { endpoint, body } = buildFetchRequest(scope, options);
537
+ const result = await transport.request(endpoint, { body });
538
+ const response = result?.["context"] ?? {};
539
+ hooks.onAssembled?.(scope, options, response, startedAt);
540
+ return response;
541
+ }
542
+
543
+ // src/context/normalize.ts
544
+ function pickDefined(...values) {
545
+ for (const v of values) if (v !== void 0) return v;
546
+ return void 0;
547
+ }
548
+ function pick(...values) {
549
+ return pickDefined(...values);
550
+ }
551
+ var obj = (v) => v ?? {};
552
+ function normalizeTemporalFields(item = {}) {
553
+ return {
554
+ eventDate: pick(item["eventDate"], item.event_date, null),
555
+ validUntil: pick(item["validUntil"], item.valid_until, null),
556
+ temporalCategory: pick(item["temporalCategory"], item.temporal_category, null),
557
+ temporalConfidence: pick(item["temporalConfidence"], item.temporal_confidence, 0)
558
+ };
559
+ }
560
+ function normalizeFact(item = {}) {
561
+ return {
562
+ id: item.id || "",
563
+ content: item.content || "",
564
+ confidence: pick(item.confidence, 0),
565
+ source: item.source || "",
566
+ extractedAt: pick(item["extractedAt"], item.extracted_at, null),
567
+ metadata: obj(item.metadata),
568
+ ...normalizeTemporalFields(item)
569
+ };
570
+ }
571
+ function normalizePreference(item = {}) {
572
+ return {
573
+ id: item.id || "",
574
+ category: item["category"] || "",
575
+ content: item.content || "",
576
+ strength: pick(item["strength"], item.confidence, 0),
577
+ source: item.source || "",
578
+ extractedAt: pick(item["extractedAt"], item.extracted_at, null),
579
+ metadata: obj(item.metadata),
580
+ ...normalizeTemporalFields(item)
581
+ };
582
+ }
583
+ function normalizeEpisode(item = {}) {
584
+ return {
585
+ id: item.id || "",
586
+ summary: pick(item["summary"], item.content, ""),
587
+ occurredAt: pick(item["occurredAt"], item["occurred_at"], null),
588
+ significance: pick(item["significance"], item.confidence, 0),
589
+ participants: item["participants"] || [],
590
+ metadata: obj(item.metadata),
591
+ ...normalizeTemporalFields(item)
592
+ };
593
+ }
594
+ function normalizeEmotion(item = {}) {
595
+ return {
596
+ id: item.id || "",
597
+ emotionType: pick(item["emotionType"], item["emotion_type"], ""),
598
+ intensity: pick(item["intensity"], item.confidence, 0),
599
+ detectedAt: pick(item["detectedAt"], item["detected_at"], null),
600
+ context: item["context"] || "",
601
+ metadata: obj(item.metadata),
602
+ ...normalizeTemporalFields(item)
603
+ };
604
+ }
605
+ function normalizeTemporalEvent(item = {}) {
606
+ return {
607
+ id: item.id || "",
608
+ content: item.content || "",
609
+ eventDate: pick(item["eventDate"], item.event_date, null),
610
+ validUntil: pick(item["validUntil"], item.valid_until, null),
611
+ temporalCategory: pick(item["temporalCategory"], item.temporal_category, ""),
612
+ temporalConfidence: pick(item["temporalConfidence"], item.temporal_confidence, 0),
613
+ confidence: pick(item.confidence, 0),
614
+ source: item.source || "",
615
+ extractedAt: pick(item["extractedAt"], item.extracted_at, null),
616
+ metadata: obj(item.metadata)
617
+ };
618
+ }
619
+ function normalizeContextMetadata(metadata = {}) {
620
+ return {
621
+ correlationId: pick(metadata["correlationId"], metadata["correlation_id"], ""),
622
+ ttlSeconds: pick(metadata["ttlSeconds"], metadata["ttl_seconds"], 0),
623
+ source: metadata["source"] || "unknown",
624
+ retrievedAt: pick(metadata["retrievedAt"], metadata["retrieved_at"], null),
625
+ compactionApplied: pick(
626
+ metadata["compactionApplied"],
627
+ metadata["compaction_applied"],
628
+ null
629
+ )
630
+ };
631
+ }
632
+ function normalizeConversationContext(value) {
633
+ if (!value) return null;
634
+ return {
635
+ summary: value.summary || null,
636
+ currentState: pick(value["currentState"], value.current_state, {}),
637
+ keyExtractions: pick(value["keyExtractions"], value.key_extractions, {}),
638
+ recentTurns: pick(value["recentTurns"], value.recent_turns, []),
639
+ compactionId: pick(value["compactionId"], value.compaction_id, null),
640
+ compactedAt: pick(value["compactedAt"], value.compacted_at, null),
641
+ conversationId: pick(value["conversationId"], value.conversation_id, null)
642
+ };
643
+ }
644
+ function normalizeContextResponse(result = {}) {
645
+ const context = result["context"] ?? result;
646
+ return {
647
+ facts: (context.facts || []).map((i) => normalizeFact(i)),
648
+ preferences: (context.preferences || []).map((i) => normalizePreference(i)),
649
+ episodes: (context.episodes || []).map((i) => normalizeEpisode(i)),
650
+ emotions: (context.emotions || []).map((i) => normalizeEmotion(i)),
651
+ temporalEvents: (pick(context["temporalEvents"], context.temporal_events, []) || []).map((i) => normalizeTemporalEvent(i)),
652
+ conversationContext: normalizeConversationContext(
653
+ pick(
654
+ context["conversationContext"],
655
+ context.conversation_context,
656
+ null
657
+ )
658
+ ),
659
+ metadata: normalizeContextMetadata(obj(context.metadata)),
660
+ rawResponse: pick(context["rawResponse"], context["raw_response"], {})
661
+ };
662
+ }
663
+
664
+ // src/context/flatten.ts
665
+ var s = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
666
+ var n = (v, d = 0) => typeof v === "number" ? v : d;
667
+ var o = (v) => v ?? {};
668
+ var dt = (v) => v ? String(v) : null;
669
+ function temporal(item) {
670
+ return {
671
+ event_date: dt(item["event_date"]),
672
+ valid_until: dt(item["valid_until"]),
673
+ temporal_category: item["temporal_category"] ?? null,
674
+ temporal_confidence: n(item["temporal_confidence"], 0)
675
+ };
676
+ }
677
+ function flattenContextItems(context) {
678
+ const items = [];
679
+ for (const f of context.facts ?? []) {
680
+ items.push({
681
+ id: s(f["id"]),
682
+ memory: s(f["content"]),
683
+ score: n(f["confidence"]),
684
+ source: s(f["source"]),
685
+ metadata: o(f["metadata"]),
686
+ context_type: "fact",
687
+ ...temporal(f)
688
+ });
689
+ }
690
+ for (const p of context.preferences ?? []) {
691
+ items.push({
692
+ id: s(p["id"]),
693
+ memory: s(p["content"]),
694
+ score: n(p["strength"]),
695
+ source: s(p["source"]),
696
+ metadata: o(p["metadata"]),
697
+ context_type: "preference",
698
+ ...temporal(p)
699
+ });
700
+ }
701
+ for (const e of context.episodes ?? []) {
702
+ items.push({
703
+ id: s(e["id"]),
704
+ memory: s(e["summary"]),
705
+ score: n(e["significance"]),
706
+ metadata: o(e["metadata"]),
707
+ context_type: "episode",
708
+ ...temporal(e)
709
+ });
710
+ }
711
+ for (const m of context.emotions ?? []) {
712
+ items.push({
713
+ id: s(m["id"]),
714
+ memory: s(m["context"]),
715
+ score: n(m["intensity"]),
716
+ metadata: o(m["metadata"]),
717
+ context_type: "emotion",
718
+ ...temporal(m)
719
+ });
720
+ }
721
+ for (const t of context.temporal_events ?? []) {
722
+ items.push({
723
+ id: s(t["id"]),
724
+ memory: s(t["content"]),
725
+ score: n(t["temporal_confidence"]),
726
+ source: s(t["source"]),
727
+ metadata: o(t["metadata"]),
728
+ context_type: "temporal_event",
729
+ ...temporal(t)
730
+ });
731
+ }
732
+ return items;
733
+ }
734
+
735
+ // src/memories/interface.ts
736
+ var DOCUMENT_TYPES = [
737
+ "ai-chat-conversation",
738
+ "document",
739
+ "email",
740
+ "pdf",
741
+ "image",
742
+ "audio",
743
+ "meeting-transcript"
744
+ ];
745
+ var INGEST_MODES = ["fast", "long-range"];
746
+ var DEFAULT_DOCUMENT_TYPE = "ai-chat-conversation";
747
+ var DEFAULT_INGEST_MODE = "long-range";
748
+ function either(o2, snake, camel) {
749
+ const a = o2[snake];
750
+ if (a !== void 0) return a;
751
+ const b = o2[camel];
752
+ return b === void 0 ? void 0 : b;
753
+ }
754
+ function isoOrNull(v) {
755
+ if (v === void 0 || v === null) return null;
756
+ return v instanceof Date ? v.toISOString() : v;
757
+ }
758
+ function buildCreateBody(options) {
759
+ if (!options.document) throw new InvalidInputError("document is required");
760
+ const o2 = options;
761
+ const documentType = either(o2, "document_type", "documentType") ?? DEFAULT_DOCUMENT_TYPE;
762
+ const mode = options.mode ?? DEFAULT_INGEST_MODE;
763
+ if (!DOCUMENT_TYPES.includes(documentType)) {
764
+ throw new InvalidInputError(
765
+ `Invalid document_type '${documentType}'. Expected one of: ${DOCUMENT_TYPES.join(", ")}`
766
+ );
767
+ }
768
+ if (!INGEST_MODES.includes(mode)) {
769
+ throw new InvalidInputError(
770
+ `Invalid mode '${mode}'. Expected one of: ${INGEST_MODES.join(", ")}`
771
+ );
772
+ }
773
+ return {
774
+ document: options.document,
775
+ document_type: documentType,
776
+ document_id: either(o2, "document_id", "documentId") ?? null,
777
+ document_created_at: isoOrNull(
778
+ either(o2, "document_created_at", "documentCreatedAt")
779
+ ),
780
+ user_id: either(o2, "user_id", "userId") ?? null,
781
+ customer_id: either(o2, "customer_id", "customerId") ?? null,
782
+ mode,
783
+ metadata: options.metadata ?? {}
784
+ };
785
+ }
786
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "error", "cancelled"]);
787
+ function createMemoriesInterface(transport) {
788
+ return {
789
+ async create(options) {
790
+ return transport.request("memories_create", { body: buildCreateBody(options) });
791
+ },
792
+ async batch_create(options) {
793
+ const docs = options.documents;
794
+ if (!Array.isArray(docs) || docs.length === 0) {
795
+ throw new InvalidInputError("documents must be a non-empty array");
796
+ }
797
+ const failFast = options.fail_fast ?? options.failFast ?? false;
798
+ return transport.request("memories_batch", {
799
+ body: { documents: docs.map(buildCreateBody), fail_fast: failFast }
800
+ });
801
+ },
802
+ async create_from_file(options) {
803
+ const userId = options.user_id ?? options.userId;
804
+ const customerId = options.customer_id ?? options.customerId;
805
+ if (!userId) throw new InvalidInputError("user_id is required");
806
+ if (!customerId) throw new InvalidInputError("customer_id is required");
807
+ const filePath = options.file_path ?? options.filePath;
808
+ const sources = [filePath, options.file, options.text].filter((v) => v !== void 0);
809
+ if (sources.length === 0) {
810
+ throw new InvalidInputError("One of file_path, file, or text must be provided.");
811
+ }
812
+ if (sources.length > 1) {
813
+ throw new InvalidInputError(
814
+ "Provide exactly one of file_path, file, or text, not several."
815
+ );
816
+ }
817
+ const form = new FormData();
818
+ form.set("user_id", userId);
819
+ form.set("customer_id", customerId);
820
+ form.set("relationship_type", options.relationship_type ?? options.relationshipType ?? "b2c");
821
+ form.set("mode", String(options.mode ?? DEFAULT_INGEST_MODE));
822
+ const documentType = options.document_type ?? options.documentType;
823
+ if (documentType) form.set("document_type", String(documentType));
824
+ if (options.metadata !== void 0 && Object.keys(options.metadata).length > 0) {
825
+ form.set("metadata", JSON.stringify(options.metadata));
826
+ }
827
+ if (filePath !== void 0) {
828
+ const { readFile } = await import("fs/promises");
829
+ const { basename } = await import("path");
830
+ const bytes = await readFile(filePath);
831
+ form.set("file", new Blob([bytes]), basename(filePath));
832
+ } else if (options.file !== void 0) {
833
+ const blob = options.file instanceof Blob ? options.file : new Blob([options.file]);
834
+ form.set("file", blob, options.filename ?? "upload");
835
+ } else {
836
+ form.set("text", String(options.text));
837
+ }
838
+ return transport.request("memories_upload", {
839
+ body: form,
840
+ rawBody: true
841
+ });
842
+ },
843
+ async get(memoryId) {
844
+ if (!memoryId) throw new InvalidInputError("memory_id is required");
845
+ return transport.request("memories_get", { pathParams: { memory_id: memoryId } });
846
+ },
847
+ async update(options) {
848
+ const memoryId = options.memory_id ?? options.memoryId;
849
+ if (!memoryId) throw new InvalidInputError("memory_id is required");
850
+ if (!options.document) throw new InvalidInputError("document is required");
851
+ const body = {
852
+ document: options.document,
853
+ merge_strategy: options.merge_strategy ?? options.mergeStrategy ?? "smart-merge"
854
+ };
855
+ const dt2 = options.document_type ?? options.documentType;
856
+ if (dt2 !== void 0) body["document_type"] = dt2;
857
+ if (options.metadata !== void 0) body["metadata"] = options.metadata;
858
+ return transport.request("memories_update", {
859
+ pathParams: { memory_id: memoryId },
860
+ body
861
+ });
862
+ },
863
+ async delete(memoryId) {
864
+ if (!memoryId) {
865
+ throw new InvalidInputError(
866
+ "memory_id is required. Deleting by user is no longer supported: the previous implementation tracked ids in process memory, so it silently deleted nothing in any serverless or multi-process deployment while reporting success."
867
+ );
868
+ }
869
+ return transport.request("memories_delete", { pathParams: { memory_id: memoryId } });
870
+ },
871
+ async status(ingestionId) {
872
+ if (!ingestionId) throw new InvalidInputError("ingestion_id is required");
873
+ return transport.request("memories_status", {
874
+ pathParams: { ingestion_id: ingestionId }
875
+ });
876
+ },
877
+ async wait_for_completion(ingestionId, options = {}) {
878
+ if (!ingestionId) throw new InvalidInputError("ingestion_id is required");
879
+ const timeoutMs = (options.timeout_seconds ?? options.timeoutSeconds ?? 300) * 1e3;
880
+ const pollMs = (options.poll_interval_seconds ?? options.pollIntervalSeconds ?? 2) * 1e3;
881
+ const deadline = Date.now() + timeoutMs;
882
+ let last = {};
883
+ for (; ; ) {
884
+ last = await this.status(ingestionId);
885
+ const status = String(last["status"] ?? "").toLowerCase();
886
+ if (TERMINAL_STATUSES.has(status)) return last;
887
+ if (Date.now() >= deadline) {
888
+ return last;
889
+ }
890
+ await sleep2(Math.min(pollMs, Math.max(0, deadline - Date.now())), options.signal);
891
+ }
892
+ }
893
+ };
894
+ }
895
+ function sleep2(ms, signal) {
896
+ if (ms <= 0) return Promise.resolve();
897
+ return new Promise((resolve, reject) => {
898
+ const timer = setTimeout(() => {
899
+ signal?.removeEventListener("abort", onAbort);
900
+ resolve();
901
+ }, ms);
902
+ function onAbort() {
903
+ clearTimeout(timer);
904
+ reject(signal?.reason ?? new Error("Aborted"));
905
+ }
906
+ signal?.addEventListener("abort", onAbort, { once: true });
907
+ });
908
+ }
909
+
910
+ // src/util/validators.ts
911
+ var INSTANCE_ID_RE = /^inst_[0-9a-fA-F]{16}$/;
912
+ var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
913
+ function validateConversationId(conversationId) {
914
+ if (conversationId === void 0 || conversationId === null || conversationId === "") return;
915
+ if (typeof conversationId !== "string" || !UUID_RE.test(conversationId)) {
916
+ throw new InvalidConversationIdError(`Invalid conversation ID: ${String(conversationId)}`);
917
+ }
918
+ }
919
+ function validateInstanceId(instanceId) {
920
+ if (instanceId === void 0 || instanceId === null || instanceId === "") return;
921
+ if (typeof instanceId !== "string" || !INSTANCE_ID_RE.test(instanceId)) {
922
+ throw new InvalidInstanceIdError(`Invalid instance ID: ${String(instanceId)}`);
923
+ }
924
+ }
925
+
926
+ // src/conversation/interface.ts
927
+ function either2(o2, snake, camel) {
928
+ const a = o2[snake];
929
+ if (a !== void 0) return a;
930
+ const b = o2[camel];
931
+ return b === void 0 ? void 0 : b;
932
+ }
933
+ function iso(v) {
934
+ if (v === void 0) return void 0;
935
+ return v instanceof Date ? v.toISOString() : v;
936
+ }
937
+ function buildRecordMessageBody(options) {
938
+ const o2 = options;
939
+ const conversationId = either2(o2, "conversation_id", "conversationId");
940
+ const userId = either2(o2, "user_id", "userId");
941
+ const customerId = either2(o2, "customer_id", "customerId");
942
+ if (!conversationId) throw new InvalidInputError("conversation_id is required");
943
+ validateConversationId(conversationId);
944
+ if (!options.role) throw new InvalidInputError("role is required");
945
+ if (options.content === void 0 || options.content === null) {
946
+ throw new InvalidInputError("content is required");
947
+ }
948
+ if (!userId) throw new InvalidInputError("user_id is required");
949
+ if (!customerId) throw new InvalidInputError("customer_id is required");
950
+ const body = {
951
+ conversation_id: conversationId,
952
+ role: options.role,
953
+ content: options.content,
954
+ user_id: userId,
955
+ customer_id: customerId,
956
+ // Python always sends this, defaulting to {}.
957
+ metadata: options.metadata ?? {}
958
+ };
959
+ const sessionId = either2(o2, "session_id", "sessionId");
960
+ if (sessionId !== void 0) body["session_id"] = sessionId;
961
+ return body;
962
+ }
963
+ function buildIngestTranscriptBody(options) {
964
+ const o2 = options;
965
+ const conversationId = either2(o2, "conversation_id", "conversationId");
966
+ const userId = either2(o2, "user_id", "userId");
967
+ if (!conversationId) throw new InvalidInputError("conversation_id is required");
968
+ if (!userId) throw new InvalidInputError("user_id is required");
969
+ if (options.transcript === void 0 || options.transcript === null) {
970
+ throw new InvalidInputError("transcript is required");
971
+ }
972
+ const body = {
973
+ conversation_id: conversationId,
974
+ user_id: userId,
975
+ transcript: options.transcript
976
+ };
977
+ const customerId = either2(o2, "customer_id", "customerId");
978
+ const conversationType = either2(o2, "conversation_type", "conversationType");
979
+ const startedAt = iso(either2(o2, "started_at", "startedAt"));
980
+ const endedAt = iso(either2(o2, "ended_at", "endedAt"));
981
+ if (customerId !== void 0) body["customer_id"] = customerId;
982
+ if (conversationType !== void 0) body["conversation_type"] = conversationType;
983
+ if (options.analysis !== void 0) body["analysis"] = options.analysis;
984
+ if (options.metadata !== void 0) body["metadata"] = options.metadata;
985
+ if (startedAt !== void 0) body["started_at"] = startedAt;
986
+ if (endedAt !== void 0) body["ended_at"] = endedAt;
987
+ return body;
988
+ }
989
+ function buildCompactBody(options) {
990
+ const o2 = options;
991
+ const conversationId = either2(o2, "conversation_id", "conversationId");
992
+ if (!conversationId) throw new InvalidInputError("conversation_id is required");
993
+ validateConversationId(conversationId);
994
+ const body = { conversation_id: conversationId, force: options.force ?? false };
995
+ const strategy = options.strategy ?? either2(o2, "compaction_level", "compactionLevel");
996
+ const targetTokens = either2(o2, "target_tokens", "targetTokens");
997
+ if (strategy !== void 0) body["strategy"] = strategy;
998
+ if (targetTokens !== void 0) body["target_tokens"] = targetTokens;
999
+ return body;
1000
+ }
1001
+ var CompactionSubscribers = class {
1002
+ #byConversation = /* @__PURE__ */ new Map();
1003
+ add(conversationId, callback) {
1004
+ const list = this.#byConversation.get(conversationId);
1005
+ if (list === void 0) this.#byConversation.set(conversationId, [callback]);
1006
+ else list.push(callback);
1007
+ let unsubscribed = false;
1008
+ return () => {
1009
+ if (unsubscribed) return;
1010
+ unsubscribed = true;
1011
+ const current = this.#byConversation.get(conversationId);
1012
+ if (current === void 0) return;
1013
+ const at = current.indexOf(callback);
1014
+ if (at !== -1) current.splice(at, 1);
1015
+ if (current.length === 0) this.#byConversation.delete(conversationId);
1016
+ };
1017
+ }
1018
+ removeAll(conversationId) {
1019
+ if (conversationId === void 0) {
1020
+ let count = 0;
1021
+ for (const list2 of this.#byConversation.values()) count += list2.length;
1022
+ this.#byConversation.clear();
1023
+ return count;
1024
+ }
1025
+ const list = this.#byConversation.get(conversationId);
1026
+ if (list === void 0) return 0;
1027
+ this.#byConversation.delete(conversationId);
1028
+ return list.length;
1029
+ }
1030
+ /** Fires subscribers in registration order. A throwing listener must not break the stream reader. */
1031
+ dispatch(conversationId, bundle, onError) {
1032
+ for (const callback of [...this.#byConversation.get(conversationId) ?? []]) {
1033
+ try {
1034
+ const result = callback(bundle);
1035
+ if (result instanceof Promise) result.catch(onError);
1036
+ } catch (error) {
1037
+ onError(error);
1038
+ }
1039
+ }
1040
+ }
1041
+ };
1042
+ function createConversationNamespace(transport, subscribers = new CompactionSubscribers(), onWrite = () => {
1043
+ }, onTurn = () => {
1044
+ }, onFetched = () => {
1045
+ }) {
1046
+ const convId = (v) => {
1047
+ const id = typeof v === "string" ? v : v.conversation_id ?? v.conversationId;
1048
+ if (!id) throw new InvalidInputError("conversation_id is required");
1049
+ validateConversationId(id);
1050
+ return id;
1051
+ };
1052
+ return {
1053
+ async record_message(options) {
1054
+ const body = buildRecordMessageBody(options);
1055
+ const result = await transport.request("conversations_messages", { body });
1056
+ onTurn({
1057
+ conversationId: String(body["conversation_id"] ?? ""),
1058
+ role: String(body["role"] ?? "user"),
1059
+ content: String(body["content"] ?? ""),
1060
+ timestamp: body["timestamp"]
1061
+ });
1062
+ onWrite({
1063
+ userId: body["user_id"],
1064
+ customerId: body["customer_id"]
1065
+ });
1066
+ return result;
1067
+ },
1068
+ async record_messages_batch(messages) {
1069
+ if (!Array.isArray(messages) || messages.length === 0) {
1070
+ throw new InvalidInputError("messages must be a non-empty array");
1071
+ }
1072
+ const bodies = messages.map(buildRecordMessageBody);
1073
+ const result = await transport.request("conversations_messages_batch", {
1074
+ body: { messages: bodies }
1075
+ });
1076
+ for (const body of bodies) {
1077
+ onTurn({
1078
+ conversationId: String(body["conversation_id"] ?? ""),
1079
+ role: String(body["role"] ?? "user"),
1080
+ content: String(body["content"] ?? ""),
1081
+ timestamp: body["timestamp"]
1082
+ });
1083
+ }
1084
+ for (const body of bodies) {
1085
+ onWrite({
1086
+ userId: body["user_id"],
1087
+ customerId: body["customer_id"]
1088
+ });
1089
+ }
1090
+ return result;
1091
+ },
1092
+ async ingest_transcript(options) {
1093
+ const body = buildIngestTranscriptBody(options);
1094
+ const result = await transport.request("conversations_ingest", { body });
1095
+ onWrite({
1096
+ userId: body["user_id"],
1097
+ customerId: body["customer_id"]
1098
+ });
1099
+ return result;
1100
+ },
1101
+ context: {
1102
+ async fetch(options = {}) {
1103
+ const id = options.conversation_id ?? options.conversationId;
1104
+ validateConversationId(id);
1105
+ const response = await fetchContext(transport, "conversation", options);
1106
+ onFetched(
1107
+ response,
1108
+ id,
1109
+ options.user_id ?? options.userId
1110
+ );
1111
+ return response;
1112
+ },
1113
+ get_context_for_prompt: async (options = {}) => {
1114
+ const id = convId(options);
1115
+ const result = await transport.request("context_for_prompt", {
1116
+ pathParams: { conversation_id: id },
1117
+ query: { style: options.style ?? "structured" }
1118
+ });
1119
+ return result?.["context_for_prompt"] ?? result ?? {};
1120
+ },
1121
+ async compact(options) {
1122
+ return transport.request("conversations_compact", { body: buildCompactBody(options) });
1123
+ },
1124
+ get_compacted: async (options) => {
1125
+ const id = convId(options);
1126
+ const query = { format: options.format ?? "structured" };
1127
+ if (options.version !== void 0) query["version"] = options.version;
1128
+ try {
1129
+ return await transport.request("conversations_compacted", {
1130
+ pathParams: { conversation_id: id },
1131
+ query
1132
+ });
1133
+ } catch (error) {
1134
+ if (error.code === "context_not_found") return null;
1135
+ throw error;
1136
+ }
1137
+ },
1138
+ async get_compaction_status(options) {
1139
+ return transport.request("conversations_compaction_status", {
1140
+ pathParams: { conversation_id: convId(options) }
1141
+ });
1142
+ },
1143
+ /**
1144
+ * Fire `callback` whenever a `compaction_update` bundle arrives on the
1145
+ * Listen stream for this conversation.
1146
+ *
1147
+ * Requires an active `client.instance.listen()`; without it no bundles
1148
+ * arrive and the callback never fires. Registering before listening is
1149
+ * fine, which is why this does not throw when the stream is down.
1150
+ *
1151
+ * Returns an idempotent unsubscribe thunk, matching Python.
1152
+ */
1153
+ subscribe_to_compaction_updates(conversationId, callback) {
1154
+ if (!conversationId) throw new InvalidInputError("conversation_id is required");
1155
+ if (typeof callback !== "function") throw new InvalidInputError("callback is required");
1156
+ return subscribers.add(conversationId, callback);
1157
+ },
1158
+ /** Remove subscribers for one conversation, or all of them. Returns the count removed. */
1159
+ unsubscribe_all_compaction_updates(conversationId) {
1160
+ return subscribers.removeAll(conversationId);
1161
+ }
1162
+ }
1163
+ };
1164
+ }
1165
+
1166
+ // src/user/interface.ts
1167
+ function createUserNamespace(transport, hooks = {}) {
1168
+ return {
1169
+ async get_profile(options) {
1170
+ const userId = typeof options === "string" ? options : options.user_id ?? options.userId;
1171
+ if (!userId) throw new InvalidInputError("user_id is required");
1172
+ const customerId = typeof options === "string" ? void 0 : options.customer_id ?? options.customerId;
1173
+ return transport.request("user_profile", {
1174
+ pathParams: { user_id: userId },
1175
+ // Sent only when provided, matching Python. Note the server returns
1176
+ // 500 rather than a clean error if it needs one and does not get it.
1177
+ ...customerId !== void 0 ? { query: { customer_id: customerId } } : {}
1178
+ });
1179
+ },
1180
+ context: { async fetch(options = {}) {
1181
+ return fetchContext(transport, "user", options, hooks);
1182
+ } }
1183
+ };
1184
+ }
1185
+
1186
+ // src/credits/interface.ts
1187
+ function createCreditsNamespace(transport) {
1188
+ return {
1189
+ async get_balance() {
1190
+ return transport.request("credits_balance");
1191
+ },
1192
+ async get_ledger(options = {}) {
1193
+ return transport.request("credits_ledger", {
1194
+ query: { limit: options.limit ?? 50, offset: options.offset ?? 0 }
1195
+ });
1196
+ },
1197
+ async estimate(options) {
1198
+ return transport.request("credits_estimate", {
1199
+ body: {
1200
+ metric_type: options.metric_type ?? options.metricType ?? null,
1201
+ units: options.units ?? null,
1202
+ item_count: options.item_count ?? options.itemCount ?? null,
1203
+ endpoint: options.endpoint ?? null,
1204
+ mode: options.mode ?? null
1205
+ }
1206
+ });
1207
+ },
1208
+ async redeem(code) {
1209
+ if (!code) throw new InvalidInputError("code is required");
1210
+ return transport.request("credits_redeem", { body: { code } });
1211
+ }
1212
+ };
1213
+ }
1214
+
1215
+ // src/cache/interface.ts
1216
+ function createCacheNamespace(cache) {
1217
+ return {
1218
+ clear: () => cache.clear(),
1219
+ clear_user: (userId) => {
1220
+ if (!userId) throw new InvalidInputError("user_id is required");
1221
+ cache.dropEntity(userId);
1222
+ },
1223
+ clear_customer: (customerId) => {
1224
+ if (!customerId) throw new InvalidInputError("customer_id is required");
1225
+ cache.dropEntity(customerId);
1226
+ },
1227
+ stats: () => ({ bundles: cache.size, items: cache.itemCount })
1228
+ };
1229
+ }
1230
+
1231
+ // src/instance/interface.ts
1232
+ var DEFAULT_GRPC_HOST = "synap-cloud-prod.maximem.ai";
1233
+ var DEFAULT_GRPC_PORT = 443;
1234
+ function envPort() {
1235
+ const raw = getEnv("SYNAP_GRPC_PORT");
1236
+ if (raw === void 0 || raw === "") return void 0;
1237
+ const parsed = Number(raw);
1238
+ return Number.isFinite(parsed) ? parsed : void 0;
1239
+ }
1240
+ function envTls() {
1241
+ const raw = getEnv("SYNAP_GRPC_USE_TLS");
1242
+ if (raw === void 0 || raw === "") return void 0;
1243
+ return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase());
1244
+ }
1245
+ function createInstanceNamespace(credentials, cache, onTurn) {
1246
+ let client = null;
1247
+ let dispatchCompaction = null;
1248
+ let applyCompaction = null;
1249
+ const internals = {
1250
+ onCompactionUpdate(conversationId, bundle) {
1251
+ applyCompaction?.(bundle);
1252
+ dispatchCompaction?.(conversationId, bundle);
1253
+ },
1254
+ setCompactionApplier(apply) {
1255
+ applyCompaction = apply;
1256
+ },
1257
+ sendContextUsed(event) {
1258
+ client?.sendContextUsed(event);
1259
+ },
1260
+ sendContextAssembled(event) {
1261
+ client?.sendContextAssembled(event);
1262
+ },
1263
+ setCompactionDispatcher(dispatch) {
1264
+ dispatchCompaction = dispatch;
1265
+ },
1266
+ isListening() {
1267
+ return client !== null && client.isConnected;
1268
+ }
1269
+ };
1270
+ const namespace = {
1271
+ async listen(options = {}) {
1272
+ if (client !== null) {
1273
+ throw new ListeningAlreadyActiveError("Listening is already active on this instance");
1274
+ }
1275
+ const { GrpcStreamClient } = await import("./stream-client-ME5N5NK4.js");
1276
+ const onContext = options.on_context ?? options.onContext;
1277
+ const onDisconnect = options.on_disconnect ?? options.onDisconnect;
1278
+ const onReconnect = options.on_reconnect ?? options.onReconnect;
1279
+ let reconnectAttempts = 0;
1280
+ const created = new GrpcStreamClient(credentials(), cache, {
1281
+ host: options.host ?? getEnv("SYNAP_GRPC_HOST") ?? DEFAULT_GRPC_HOST,
1282
+ port: options.port ?? envPort() ?? DEFAULT_GRPC_PORT,
1283
+ useTls: options.use_tls ?? options.useTls ?? envTls() ?? true,
1284
+ ...onContext !== void 0 ? { onContext } : {},
1285
+ onCompactionUpdate: (conversationId, bundle) => {
1286
+ internals.onCompactionUpdate(conversationId, bundle);
1287
+ },
1288
+ onStateChange: (state) => {
1289
+ if (state === "reconnecting") {
1290
+ reconnectAttempts += 1;
1291
+ onReconnect?.(reconnectAttempts);
1292
+ } else if (state === "disconnected") {
1293
+ onDisconnect?.("stream disconnected");
1294
+ }
1295
+ }
1296
+ });
1297
+ try {
1298
+ await created.connect();
1299
+ } catch (error) {
1300
+ if (error instanceof SynapError) throw error;
1301
+ throw new SynapError(
1302
+ `Could not open the Synap anticipation stream: ${error?.message ?? String(error)}`,
1303
+ { cause: error }
1304
+ );
1305
+ }
1306
+ client = created;
1307
+ },
1308
+ async stop_listening() {
1309
+ if (client === null) return;
1310
+ const active = client;
1311
+ client = null;
1312
+ await active.disconnect();
1313
+ },
1314
+ async send_message(options) {
1315
+ if (client === null) throw new ListeningNotActiveError("Listening is not active");
1316
+ if (options.content === void 0) {
1317
+ throw new ListeningNotActiveError("content is required");
1318
+ }
1319
+ const conversationId = options.conversation_id ?? options.conversationId ?? "";
1320
+ onTurn?.({
1321
+ conversationId,
1322
+ role: options.role ?? "user",
1323
+ content: options.content
1324
+ });
1325
+ client.sendConversationEvent({
1326
+ event_type: options.event_type ?? options.eventType ?? "user_message",
1327
+ content: options.content,
1328
+ role: options.role ?? "user",
1329
+ conversation_id: options.conversation_id ?? options.conversationId ?? "",
1330
+ user_id: options.user_id ?? options.userId ?? "",
1331
+ customer_id: options.customer_id ?? options.customerId ?? "",
1332
+ session_id: options.session_id ?? options.sessionId ?? "",
1333
+ metadata: options.metadata ?? {},
1334
+ timestamp_ms: Date.now(),
1335
+ search_queries: options.search_queries ?? options.searchQueries ?? [],
1336
+ context_types: options.context_types ?? options.contextTypes ?? []
1337
+ });
1338
+ },
1339
+ async record_thinking(options) {
1340
+ if (client === null) throw new ListeningNotActiveError("Listening is not active");
1341
+ client.sendConversationEvent({
1342
+ // Python emits reasoning as an `agent_thinking` conversation event.
1343
+ event_type: "agent_thinking",
1344
+ content: options.content,
1345
+ role: "assistant",
1346
+ conversation_id: options.conversation_id ?? options.conversationId ?? "",
1347
+ user_id: options.user_id ?? options.userId ?? "",
1348
+ customer_id: options.customer_id ?? options.customerId ?? "",
1349
+ session_id: options.session_id ?? options.sessionId ?? "",
1350
+ metadata: options.metadata ?? {},
1351
+ timestamp_ms: Date.now()
1352
+ });
1353
+ },
1354
+ get is_listening() {
1355
+ return client !== null && client.isConnected;
1356
+ },
1357
+ _internal: internals
1358
+ };
1359
+ return namespace;
1360
+ }
1361
+
1362
+ // src/version.ts
1363
+ var SDK_VERSION = "1.0.0-beta.0";
1364
+
1365
+ // src/registry.ts
1366
+ var instances = /* @__PURE__ */ new Map();
1367
+ function buildRegistryKey(instanceId, apiKey) {
1368
+ if (instanceId) return instanceId;
1369
+ const key = apiKey ?? "";
1370
+ if (!key) return "";
1371
+ return `apikey:${digest(key)}`;
1372
+ }
1373
+ function digest(value) {
1374
+ const PRIME = 1099511628211n;
1375
+ const MASK = 0xffffffffffffffffn;
1376
+ let hash = 14695981039346656037n;
1377
+ for (let i = 0; i < value.length; i += 1) {
1378
+ hash ^= BigInt(value.charCodeAt(i));
1379
+ hash = hash * PRIME & MASK;
1380
+ }
1381
+ return hash.toString(16).padStart(16, "0");
1382
+ }
1383
+ function get(key) {
1384
+ return instances.get(key);
1385
+ }
1386
+ function registerIfAbsent(key, client) {
1387
+ const existing = instances.get(key);
1388
+ if (existing !== void 0) return existing;
1389
+ instances.set(key, client);
1390
+ return void 0;
1391
+ }
1392
+ function aliasIfAbsent(key, client) {
1393
+ if (!key || instances.has(key)) return false;
1394
+ instances.set(key, client);
1395
+ return true;
1396
+ }
1397
+ function unregisterIfOwner(key, client) {
1398
+ if (instances.get(key) === client) instances.delete(key);
1399
+ }
1400
+
1401
+ // src/cache/short-term-store.ts
1402
+ var DEFAULT_MAX_CONVERSATIONS = 100;
1403
+ var DEFAULT_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
1404
+ function parseIso(value) {
1405
+ if (value === null || value === void 0 || value === "") return null;
1406
+ if (value instanceof Date) return value.getTime();
1407
+ if (typeof value !== "string") return null;
1408
+ const parsed = Date.parse(value);
1409
+ return Number.isNaN(parsed) ? null : parsed;
1410
+ }
1411
+ function isAfter(turnTimestamp, cutoffMs) {
1412
+ const parsed = parseIso(turnTimestamp);
1413
+ if (parsed === null) return true;
1414
+ return parsed > cutoffMs;
1415
+ }
1416
+ var ShortTermStore = class {
1417
+ // Insertion order IS the LRU order: re-inserting on touch moves an entry to
1418
+ // the end, so the first key Map yields is the least recently used.
1419
+ #entries = /* @__PURE__ */ new Map();
1420
+ #maxConversations;
1421
+ #maxAgeMs;
1422
+ #now;
1423
+ constructor(options = {}) {
1424
+ this.#maxConversations = options.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
1425
+ this.#maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
1426
+ this.#now = options.now ?? (() => Date.now());
1427
+ }
1428
+ /** The entry, or null. Age-evicts on read, as Python does. */
1429
+ get(conversationId) {
1430
+ if (!conversationId) return null;
1431
+ const entry = this.#entries.get(conversationId);
1432
+ if (entry === void 0) return null;
1433
+ if (this.#now() - entry.lastActivityAt > this.#maxAgeMs) {
1434
+ this.#entries.delete(conversationId);
1435
+ return null;
1436
+ }
1437
+ this.#touch(conversationId, entry);
1438
+ return entry;
1439
+ }
1440
+ has(conversationId) {
1441
+ return this.get(conversationId) !== null;
1442
+ }
1443
+ get size() {
1444
+ return this.#entries.size;
1445
+ }
1446
+ /**
1447
+ * Append a raw turn.
1448
+ *
1449
+ * Called from `record_message` and the stream's `send_message`. When no entry
1450
+ * exists yet (the first turn, before any compaction has arrived) a fresh one
1451
+ * is created with an empty summary; the next `compaction_update` fills the
1452
+ * summary fields in and prunes the turns it covers.
1453
+ */
1454
+ appendTurn(conversationId, role, content, timestamp) {
1455
+ if (!conversationId) return;
1456
+ const stamp = timestamp ?? new Date(this.#now()).toISOString();
1457
+ const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
1458
+ entry.recentTurns.push({ role, content, timestamp: stamp });
1459
+ entry.lastActivityAt = parseIso(stamp) ?? this.#now();
1460
+ this.#touch(conversationId, entry);
1461
+ this.#evict();
1462
+ }
1463
+ /**
1464
+ * Apply a `compaction_update` bundle.
1465
+ *
1466
+ * Replaces the summary fields from the bundle's `conversation_context` and
1467
+ * prunes locally buffered turns at or before its `end_timestamp`. Turns that
1468
+ * arrived DURING the compaction window are kept by design: the server has not
1469
+ * seen them yet, which is the whole reason the local tail exists.
1470
+ */
1471
+ applyCompaction(bundle) {
1472
+ if (typeof bundle !== "object" || bundle === null) return;
1473
+ const cc = bundle["conversation_context"] ?? {};
1474
+ const conversationId = String(
1475
+ cc["conversation_id"] ?? bundle["anticipation_conversation_id"] ?? bundle["conversation_id"] ?? ""
1476
+ );
1477
+ if (!conversationId) return;
1478
+ const endTs = parseIso(cc["end_timestamp"] ?? cc["compacted_at"]);
1479
+ const entry = this.#entries.get(conversationId) ?? this.#blank(conversationId);
1480
+ entry.summary = cc["summary"] || entry.summary;
1481
+ entry.factualParagraph = cc["factual_paragraph"] || entry.factualParagraph;
1482
+ entry.conversationalParagraph = cc["conversational_paragraph"] || entry.conversationalParagraph;
1483
+ entry.currentState = { ...cc["current_state"] ?? {} };
1484
+ entry.keyExtractions = { ...cc["key_extractions"] ?? {} };
1485
+ entry.compactionId = cc["compaction_id"] || entry.compactionId;
1486
+ entry.compactedAt = cc["compacted_at"] || entry.compactedAt;
1487
+ entry.endTimestamp = cc["end_timestamp"] || entry.endTimestamp;
1488
+ if (endTs !== null && entry.recentTurns.length > 0) {
1489
+ entry.recentTurns = entry.recentTurns.filter((t) => isAfter(t.timestamp, endTs));
1490
+ }
1491
+ entry.lastActivityAt = this.#now();
1492
+ this.#touch(conversationId, entry);
1493
+ this.#evict();
1494
+ }
1495
+ invalidate(conversationId) {
1496
+ this.#entries.delete(conversationId);
1497
+ }
1498
+ clear() {
1499
+ this.#entries.clear();
1500
+ }
1501
+ #blank(conversationId) {
1502
+ return {
1503
+ conversationId,
1504
+ summary: null,
1505
+ factualParagraph: null,
1506
+ conversationalParagraph: null,
1507
+ currentState: {},
1508
+ keyExtractions: {},
1509
+ compactionId: null,
1510
+ compactedAt: null,
1511
+ endTimestamp: null,
1512
+ recentTurns: [],
1513
+ lastActivityAt: this.#now()
1514
+ };
1515
+ }
1516
+ /** Re-insert so this key becomes the most recently used. */
1517
+ #touch(conversationId, entry) {
1518
+ this.#entries.delete(conversationId);
1519
+ this.#entries.set(conversationId, entry);
1520
+ }
1521
+ #evict() {
1522
+ while (this.#entries.size > this.#maxConversations) {
1523
+ const oldest = this.#entries.keys().next();
1524
+ if (oldest.done === true) break;
1525
+ this.#entries.delete(oldest.value);
1526
+ }
1527
+ }
1528
+ };
1529
+
1530
+ // src/context/overlay.ts
1531
+ var MAX_TURNS = 20;
1532
+ var TOKEN_BUDGET = 2e3;
1533
+ function verbatimOverlayEnabled() {
1534
+ const raw = (getEnv("SYNAP_ST_VERBATIM_OVERLAY") ?? "").trim().toLowerCase();
1535
+ if (["0", "false", "no", "off"].includes(raw)) return false;
1536
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
1537
+ return true;
1538
+ }
1539
+ function estimateTokens(turn) {
1540
+ return Math.max(1, Math.floor(String(turn.content ?? "").length / 4));
1541
+ }
1542
+ function budgetRecentTurns(turns) {
1543
+ const windowed = turns.slice(-MAX_TURNS);
1544
+ let total = windowed.reduce((n2, t) => n2 + estimateTokens(t), 0);
1545
+ while (windowed.length > 0 && total > TOKEN_BUDGET) {
1546
+ const dropped = windowed.shift();
1547
+ total -= estimateTokens(dropped);
1548
+ }
1549
+ return windowed;
1550
+ }
1551
+ function overlayLocalRecentTurns(response, store, conversationId) {
1552
+ if (conversationId === void 0 || conversationId === "") return;
1553
+ try {
1554
+ const entry = store.get(conversationId);
1555
+ if (entry === null) return;
1556
+ const existing = response.conversation_context ?? null;
1557
+ if (existing !== null && !verbatimOverlayEnabled()) return;
1558
+ const localTurns = budgetRecentTurns(entry.recentTurns);
1559
+ const serverField = (name) => existing === null ? void 0 : existing[name];
1560
+ const orLocal = (name, local) => {
1561
+ const value = serverField(name);
1562
+ if (value === void 0 || value === null || value === "") return local;
1563
+ if (typeof value === "object" && Object.keys(value).length === 0) return local;
1564
+ return value;
1565
+ };
1566
+ response.conversation_context = {
1567
+ summary: orLocal("summary", entry.summary),
1568
+ current_state: orLocal("current_state", { ...entry.currentState }),
1569
+ key_extractions: orLocal("key_extractions", { ...entry.keyExtractions }),
1570
+ recent_turns: localTurns,
1571
+ compaction_id: orLocal("compaction_id", entry.compactionId),
1572
+ compacted_at: orLocal("compacted_at", entry.compactedAt),
1573
+ conversation_id: conversationId
1574
+ };
1575
+ } catch {
1576
+ }
1577
+ }
1578
+
1579
+ // src/context/user-summary.ts
1580
+ var USER_SUMMARY_INTERVAL = 5;
1581
+ var MAX_SUMMARY_ITEMS = 3;
1582
+ var COLLECTIONS = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
1583
+ var TurnCounter = class {
1584
+ #counts = /* @__PURE__ */ new Map();
1585
+ /** Increment and return the new count. Python buckets a missing id under `_global`. */
1586
+ increment(conversationId) {
1587
+ const key = conversationId !== void 0 && conversationId !== "" ? conversationId : "_global";
1588
+ const next = (this.#counts.get(key) ?? 0) + 1;
1589
+ this.#counts.set(key, next);
1590
+ return next;
1591
+ }
1592
+ /**
1593
+ * True on every Nth turn, and never on the first.
1594
+ *
1595
+ * Python: `count > 0 and count % interval == 0`. Turn 5 injects, turn 1 does
1596
+ * not, which matters because the first turn is usually the one that just
1597
+ * triggered a fresh retrieval anyway.
1598
+ */
1599
+ shouldInject(conversationId) {
1600
+ const key = conversationId !== void 0 && conversationId !== "" ? conversationId : "_global";
1601
+ const count = this.#counts.get(key) ?? 0;
1602
+ return count > 0 && count % USER_SUMMARY_INTERVAL === 0;
1603
+ }
1604
+ clear() {
1605
+ this.#counts.clear();
1606
+ }
1607
+ };
1608
+ function itemId(item) {
1609
+ return String(item["item_id"] ?? item["id"] ?? "");
1610
+ }
1611
+ function pick2(item, ...names) {
1612
+ for (const name of names) {
1613
+ const value = item[name];
1614
+ if (value !== void 0 && value !== null) return value;
1615
+ }
1616
+ return void 0;
1617
+ }
1618
+ function normalise(item) {
1619
+ const content = pick2(item, "content") ?? "";
1620
+ const confidence = pick2(item, "confidence") ?? 0;
1621
+ const extractedAt = pick2(item, "extracted_at", "created_at") ?? null;
1622
+ return {
1623
+ id: itemId(item),
1624
+ content: String(content),
1625
+ confidence: Number(confidence),
1626
+ source: String(pick2(item, "source") ?? ""),
1627
+ category: String(pick2(item, "context_type", "category") ?? ""),
1628
+ emotion_type: String(pick2(item, "context_type", "emotion_type") ?? ""),
1629
+ strength: Number(pick2(item, "strength", "confidence") ?? 0),
1630
+ summary: String(pick2(item, "summary", "content") ?? ""),
1631
+ significance: Number(pick2(item, "significance", "confidence") ?? 0),
1632
+ intensity: Number(pick2(item, "intensity", "confidence") ?? 0),
1633
+ context: String(pick2(item, "context", "content") ?? ""),
1634
+ participants: pick2(item, "participants") ?? [],
1635
+ extracted_at: extractedAt,
1636
+ occurred_at: pick2(item, "occurred_at", "extracted_at", "created_at") ?? null,
1637
+ detected_at: pick2(item, "detected_at", "extracted_at", "created_at") ?? null,
1638
+ metadata: pick2(item, "metadata") ?? {},
1639
+ event_date: pick2(item, "event_date") ?? null,
1640
+ valid_until: pick2(item, "valid_until") ?? null,
1641
+ temporal_category: pick2(item, "temporal_category") ?? null,
1642
+ temporal_confidence: Number(pick2(item, "temporal_confidence") ?? 0)
1643
+ };
1644
+ }
1645
+ function mergeUserSummary(response, bundle) {
1646
+ const itemsByType = bundle["items_by_type"] ?? {};
1647
+ for (const collection of COLLECTIONS) {
1648
+ const items = itemsByType[collection];
1649
+ if (!Array.isArray(items)) continue;
1650
+ const existing = response[collection] ?? [];
1651
+ const existingIds = new Set(existing.map((i) => String(i.id ?? i.item_id ?? "")));
1652
+ const fresh = items.filter((i) => typeof i === "object" && i !== null && !existingIds.has(itemId(i))).slice(0, MAX_SUMMARY_ITEMS).map(normalise);
1653
+ if (fresh.length === 0) continue;
1654
+ response[collection] = [...existing, ...fresh];
1655
+ }
1656
+ return response;
1657
+ }
1658
+
1659
+ // src/context/anticipated.ts
1660
+ function scopeLookupParams(scope, params) {
1661
+ const { searchQuery, entityId, customerId, clientId, conversationId } = params;
1662
+ switch (scope) {
1663
+ case "conversation":
1664
+ return { searchQuery, entityId, customerId, clientId, conversationId };
1665
+ case "user":
1666
+ return { searchQuery, entityId, customerId, clientId };
1667
+ case "customer":
1668
+ return { searchQuery, entityId: null, customerId, clientId };
1669
+ case "client":
1670
+ return { searchQuery, entityId: null, customerId: null, clientId };
1671
+ }
1672
+ }
1673
+ function buildAnticipationResponse(hit) {
1674
+ const response = {};
1675
+ for (const [type, items] of Object.entries(hit.itemsByType)) {
1676
+ response[type] = items;
1677
+ }
1678
+ response.metadata = {
1679
+ source: "anticipation",
1680
+ ttl_seconds: 0,
1681
+ cache_hit: true,
1682
+ retrieved_at: (/* @__PURE__ */ new Date()).toISOString()
1683
+ };
1684
+ return response;
1685
+ }
1686
+ function servedItemIds(hit) {
1687
+ const ids = [];
1688
+ for (const items of Object.values(hit.itemsByType)) {
1689
+ for (const item of items) {
1690
+ const id = item.item_id ?? item["id"];
1691
+ if (typeof id === "string" && id !== "") ids.push(id);
1692
+ }
1693
+ }
1694
+ return ids;
1695
+ }
1696
+ function tryServeFromCache(cache, scope, params, maxItems) {
1697
+ try {
1698
+ const scoped = scopeLookupParams(scope, params);
1699
+ const hit = cache.lookup({
1700
+ searchQuery: scoped.searchQuery ?? null,
1701
+ entityId: scoped.entityId ?? null,
1702
+ customerId: scoped.customerId ?? null,
1703
+ clientId: scoped.clientId ?? null,
1704
+ conversationId: scoped.conversationId ?? null,
1705
+ maxItems
1706
+ });
1707
+ if (hit === null) return { response: null, hit: null };
1708
+ return { response: buildAnticipationResponse(hit), hit };
1709
+ } catch {
1710
+ return { response: null, hit: null };
1711
+ }
1712
+ }
1713
+
1714
+ // src/context/assembled.ts
1715
+ var COLLECTIONS2 = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
1716
+ function finalItemIds(response) {
1717
+ const ids = [];
1718
+ for (const collection of COLLECTIONS2) {
1719
+ const items = response[collection];
1720
+ if (!Array.isArray(items)) continue;
1721
+ for (const item of items) {
1722
+ const id = item["id"] ?? item["item_id"];
1723
+ if (typeof id === "string" && id !== "") ids.push(id);
1724
+ }
1725
+ }
1726
+ return ids;
1727
+ }
1728
+ function resolveSource(response, override) {
1729
+ const metadata = response.metadata ?? {};
1730
+ let source = override ?? String(metadata["source"] ?? "") ?? "";
1731
+ if (source === "") source = "cloud";
1732
+ if (source === "cache") source = "http_cache";
1733
+ const impliesHit = ["anticipation", "anticipation_cache", "http_cache", "sdk_authoritative"];
1734
+ const cacheHit = impliesHit.includes(source) ? true : Boolean(metadata["cache_hit"]);
1735
+ return { source, cacheHit };
1736
+ }
1737
+ function buildAssembledEvent(params) {
1738
+ const { response } = params;
1739
+ const { source, cacheHit } = resolveSource(response, params.sourceOverride);
1740
+ const conversationContext = response.conversation_context ?? {};
1741
+ const recentTurns = conversationContext["recent_turns"];
1742
+ const metadata = response.metadata ?? {};
1743
+ return {
1744
+ correlation_id: params.correlationId,
1745
+ conversation_id: params.conversationId ?? "",
1746
+ user_id: params.userId ?? "",
1747
+ customer_id: params.customerId ?? "",
1748
+ final_item_ids: finalItemIds(response),
1749
+ final_total_tokens: Number(metadata["total_tokens"] ?? 0),
1750
+ compaction_id: String(conversationContext["compaction_id"] ?? ""),
1751
+ recent_turn_count: Array.isArray(recentTurns) ? recentTurns.length : 0,
1752
+ compaction_end_timestamp: String(
1753
+ conversationContext["end_timestamp"] ?? conversationContext["compacted_at"] ?? ""
1754
+ ),
1755
+ assembly_source: source,
1756
+ assembly_duration_ms: Math.max(0, Date.now() - params.startedAt),
1757
+ cache_hit: cacheHit,
1758
+ sdk_version: params.sdkVersion
1759
+ };
1760
+ }
1761
+
1762
+ // src/context/unified.ts
1763
+ var ITEM_COLLECTIONS = [
1764
+ "facts",
1765
+ "preferences",
1766
+ "episodes",
1767
+ "emotions",
1768
+ "temporal_events"
1769
+ ];
1770
+ function itemId2(item) {
1771
+ const id = item.id ?? item.item_id;
1772
+ return typeof id === "string" && id !== "" ? id : void 0;
1773
+ }
1774
+ function mergeScopeResults(scopeResults) {
1775
+ const seen = /* @__PURE__ */ new Set();
1776
+ const out = {
1777
+ facts: [],
1778
+ preferences: [],
1779
+ episodes: [],
1780
+ emotions: [],
1781
+ temporal_events: [],
1782
+ scope_map: {},
1783
+ conversation_context: null,
1784
+ profile: null,
1785
+ conversations: null,
1786
+ formatted_context: "",
1787
+ scopes_queried: [],
1788
+ total_items: 0,
1789
+ metadata: null
1790
+ };
1791
+ for (const [scopeName, response] of scopeResults) {
1792
+ out.scopes_queried.push(scopeName);
1793
+ if (out.metadata === null && response.metadata !== void 0) {
1794
+ out.metadata = response.metadata;
1795
+ }
1796
+ if (out.profile === null && response["profile"] !== void 0 && response["profile"] !== null) {
1797
+ out.profile = response["profile"];
1798
+ }
1799
+ if (out.conversations === null && response["conversations"] !== void 0 && response["conversations"] !== null) {
1800
+ out.conversations = response["conversations"];
1801
+ }
1802
+ for (const collection of ITEM_COLLECTIONS) {
1803
+ const items = response[collection];
1804
+ if (!Array.isArray(items)) continue;
1805
+ for (const item of items) {
1806
+ const id = itemId2(item);
1807
+ if (id !== void 0) {
1808
+ if (seen.has(id)) continue;
1809
+ seen.add(id);
1810
+ out.scope_map[id] = scopeName;
1811
+ }
1812
+ out[collection].push(item);
1813
+ }
1814
+ }
1815
+ }
1816
+ out.total_items = ITEM_COLLECTIONS.reduce((n2, c) => n2 + out[c].length, 0);
1817
+ return out;
1818
+ }
1819
+ function oneDecimal(value) {
1820
+ return value.toFixed(1);
1821
+ }
1822
+ function isoDate(value) {
1823
+ if (typeof value !== "string" || value === "") return null;
1824
+ const parsed = new Date(value);
1825
+ if (Number.isNaN(parsed.getTime())) return null;
1826
+ return parsed.toISOString().slice(0, 10);
1827
+ }
1828
+ function numberOf(value) {
1829
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1830
+ }
1831
+ function scopeOf(ctx, item) {
1832
+ const id = itemId2(item);
1833
+ return (id !== void 0 ? ctx.scope_map[id] : void 0) ?? "unknown";
1834
+ }
1835
+ function bulletWithExtras(text, extras) {
1836
+ return extras.length > 0 ? `- ${text} [${extras.join(", ")}]` : `- ${text}`;
1837
+ }
1838
+ function formatForPrompt(ctx, options = {}) {
1839
+ const includeScope = options.includeScope ?? false;
1840
+ const includeConversationContext = options.includeConversationContext ?? true;
1841
+ const sections = [];
1842
+ if (ctx.facts.length > 0) {
1843
+ const lines = ctx.facts.map((f) => {
1844
+ let line = `- ${String(f.content ?? "")}`;
1845
+ if (includeScope) line += ` [scope: ${scopeOf(ctx, f)}]`;
1846
+ return line;
1847
+ });
1848
+ sections.push(`### Facts
1849
+ ${lines.join("\n")}`);
1850
+ }
1851
+ if (ctx.preferences.length > 0) {
1852
+ const lines = ctx.preferences.map((p) => {
1853
+ const extras = [];
1854
+ if (includeScope) extras.push(`scope: ${scopeOf(ctx, p)}`);
1855
+ const strength = numberOf(p["strength"]);
1856
+ if (strength !== null && strength !== 0 && strength >= 0.7) {
1857
+ extras.push(`strength: ${oneDecimal(strength)}`);
1858
+ }
1859
+ return bulletWithExtras(String(p.content ?? ""), extras);
1860
+ });
1861
+ sections.push(`### Preferences
1862
+ ${lines.join("\n")}`);
1863
+ }
1864
+ if (ctx.episodes.length > 0) {
1865
+ const lines = ctx.episodes.map((e) => {
1866
+ let line = `- ${String(e["summary"] ?? "")}`;
1867
+ if (includeScope) line += ` [scope: ${scopeOf(ctx, e)}]`;
1868
+ return line;
1869
+ });
1870
+ sections.push(`### Episodes
1871
+ ${lines.join("\n")}`);
1872
+ }
1873
+ if (ctx.emotions.length > 0) {
1874
+ const lines = ctx.emotions.map((em) => {
1875
+ const extras = [];
1876
+ if (includeScope) extras.push(`scope: ${scopeOf(ctx, em)}`);
1877
+ const intensity = numberOf(em["intensity"]);
1878
+ if (intensity !== null && intensity !== 0 && intensity >= 0.5) {
1879
+ extras.push(`intensity: ${oneDecimal(intensity)}`);
1880
+ }
1881
+ const text = `${String(em["emotion_type"] ?? "")}: ${String(em["context"] ?? "")}`;
1882
+ return bulletWithExtras(text, extras);
1883
+ });
1884
+ sections.push(`### Emotions
1885
+ ${lines.join("\n")}`);
1886
+ }
1887
+ if (ctx.temporal_events.length > 0) {
1888
+ const lines = ctx.temporal_events.map((te) => {
1889
+ const extras = [];
1890
+ if (includeScope) extras.push(`scope: ${scopeOf(ctx, te)}`);
1891
+ const validUntil = isoDate(te["valid_until"]);
1892
+ if (validUntil !== null) extras.push(`valid until: ${validUntil}`);
1893
+ return bulletWithExtras(String(te.content ?? ""), extras);
1894
+ });
1895
+ sections.push(`### Temporal Events
1896
+ ${lines.join("\n")}`);
1897
+ }
1898
+ if (includeConversationContext && ctx.conversation_context !== null) {
1899
+ const formatted = ctx.conversation_context["formatted_context"];
1900
+ if (typeof formatted === "string" && formatted !== "") {
1901
+ sections.push(`### Conversation History
1902
+ ${formatted}`);
1903
+ }
1904
+ }
1905
+ const blocks = [];
1906
+ if (sections.length > 0) blocks.push(`## User Context
1907
+ ${sections.join("\n\n")}`);
1908
+ const profileBlock = formatProfileBlock(ctx.profile);
1909
+ if (profileBlock !== null) blocks.push(profileBlock);
1910
+ const conversationsBlock = formatConversationsBlock(ctx.conversations);
1911
+ if (conversationsBlock !== null) blocks.push(conversationsBlock);
1912
+ return blocks.length > 0 ? blocks.join("\n\n") : "";
1913
+ }
1914
+ function formatProfileBlock(profile) {
1915
+ if (profile === null) return null;
1916
+ const attributes = profile["attributes"];
1917
+ const lines = [];
1918
+ if (typeof attributes === "object" && attributes !== null) {
1919
+ for (const [name, attr] of Object.entries(attributes)) {
1920
+ const value = typeof attr === "object" && attr !== null ? attr["value"] : void 0;
1921
+ let valueStr;
1922
+ if (Array.isArray(value)) valueStr = value.map((v) => String(v)).join(", ");
1923
+ else if (value === null || value === void 0) valueStr = "";
1924
+ else valueStr = String(value);
1925
+ lines.push(`- ${name}: ${valueStr}`.replace(/\s+$/, ""));
1926
+ }
1927
+ }
1928
+ const bodyParts = [];
1929
+ if (lines.length > 0) bodyParts.push(lines.join("\n"));
1930
+ const overview = profile["overview"];
1931
+ if (typeof overview === "string" && overview !== "") bodyParts.push(overview);
1932
+ if (bodyParts.length === 0) return null;
1933
+ return `## Caller Profile
1934
+ ${bodyParts.join("\n\n")}`;
1935
+ }
1936
+ function formatConversationsBlock(conversations) {
1937
+ if (conversations === null || conversations.length === 0) return null;
1938
+ const callBlocks = conversations.map((conv) => {
1939
+ const when = isoDate(conv["started_at"]) ?? isoDate(conv["last_message_at"]) ?? isoDate(conv["ended_at"]);
1940
+ let header = `### Call on ${when ?? "unknown date"}`;
1941
+ const convType = conv["conversation_type"];
1942
+ if (typeof convType === "string" && convType !== "") header += ` (${convType})`;
1943
+ const callLines = [header];
1944
+ const overview = overviewText(conv);
1945
+ if (overview !== null) callLines.push(`Overview: ${overview}`);
1946
+ const outcome = outcomeText(conv);
1947
+ if (outcome !== null) callLines.push(`Outcome: ${outcome}`);
1948
+ if (overview === null && outcome === null) {
1949
+ callLines.push(`Status: ${String(conv["summary_status"] ?? "pending")}`);
1950
+ }
1951
+ return callLines.join("\n");
1952
+ });
1953
+ return `## Previous Conversations
1954
+ ${callBlocks.join("\n\n")}`;
1955
+ }
1956
+ function overviewText(conv) {
1957
+ const summary = conv["summary"];
1958
+ if (typeof summary !== "object" || summary === null || Array.isArray(summary)) return null;
1959
+ for (const key of ["narrative_summary", "overview", "narrative", "summary"]) {
1960
+ const val = summary[key];
1961
+ if (typeof val === "string" && val.trim() !== "") return val.trim();
1962
+ }
1963
+ return null;
1964
+ }
1965
+ function outcomeText(conv) {
1966
+ const classification = conv["classification"];
1967
+ if (typeof classification !== "object" || classification === null || Array.isArray(classification)) {
1968
+ return null;
1969
+ }
1970
+ const c = classification;
1971
+ const parts = [c["primary_category"], c["subcategory"], c["objective"]].filter((p) => typeof p === "string" && p.trim() !== "");
1972
+ return parts.length > 0 ? parts.join(" / ") : null;
1973
+ }
1974
+
1975
+ // src/tool/as-tool.ts
1976
+ var VALID_SCOPES = ["conversation", "user", "customer", "client", "unified"];
1977
+ var BASE_DESCRIPTION = "Retrieve stored context (facts, preferences, recent episodes, emotions, temporal events) from Synap memory.";
1978
+ function defaultToolDescription(scope) {
1979
+ switch (scope) {
1980
+ case "conversation":
1981
+ return BASE_DESCRIPTION + " Scoped to a specific conversation. Call this when you need facts already established in the current conversation, or to reload context after a topic shift.";
1982
+ case "user":
1983
+ return BASE_DESCRIPTION + " Scoped to the user's long-term memory. Call this when you need to recall who the user is, their preferences, or their history across conversations.";
1984
+ case "customer":
1985
+ return BASE_DESCRIPTION + " Scoped to the customer/organization. Call this when you need facts about the customer org rather than an individual.";
1986
+ case "client":
1987
+ return BASE_DESCRIPTION + " Scoped to the integrating client/product. Call this for product-level knowledge (e.g. policies, documentation).";
1988
+ default:
1989
+ return BASE_DESCRIPTION + " Cross-scope: merges conversation, user, customer, and client memory in one call. Call this at the start of a turn when you need general grounding before responding.";
1990
+ }
1991
+ }
1992
+ function toolInputSchema(scope, options) {
1993
+ const props = {
1994
+ search_query: {
1995
+ type: "array",
1996
+ items: { type: "string" },
1997
+ description: "Optional list of search queries describing what context you need. Free-form natural language is fine. Omit to retrieve the most relevant recent context."
1998
+ },
1999
+ max_results: {
2000
+ type: "integer",
2001
+ minimum: 1,
2002
+ maximum: 50,
2003
+ description: "Maximum items to return (default 10)."
2004
+ },
2005
+ types: {
2006
+ type: "array",
2007
+ items: {
2008
+ type: "string",
2009
+ enum: ["facts", "preferences", "episodes", "emotions", "temporal_events"]
2010
+ },
2011
+ description: "Filter to specific memory categories. Omit to retrieve all."
2012
+ },
2013
+ mode: {
2014
+ type: "string",
2015
+ enum: ["fast", "accurate"],
2016
+ description: "Retrieval mode. 'fast' = low-latency (~50ms); 'accurate' = LLM-decomposed multi-query (~200-500ms). Default 'fast'."
2017
+ },
2018
+ precision_level: {
2019
+ type: "string",
2020
+ enum: ["high", "medium"],
2021
+ description: "Result filtering precision. 'high' (default) applies an extra relevance-refinement pass; 'medium' skips it for faster, less precisely filtered results (recall unaffected)."
2022
+ }
2023
+ };
2024
+ let required = [];
2025
+ if (scope === "conversation" && !options.hasConversationId) {
2026
+ props["conversation_id"] = {
2027
+ type: "string",
2028
+ description: "The conversation id to fetch context for."
2029
+ };
2030
+ required = ["conversation_id"];
2031
+ }
2032
+ return { type: "object", properties: props, required, additionalProperties: false };
2033
+ }
2034
+ var RESULT_COLLECTIONS = ["facts", "preferences", "episodes", "emotions", "temporal_events"];
2035
+ async function invokeScopeFetch(params) {
2036
+ const { target, scope, userId, customerId, conversationId, callArgs } = params;
2037
+ const searchQuery = callArgs["search_query"];
2038
+ const rawMax = callArgs["max_results"];
2039
+ const maxResults = typeof rawMax === "number" && rawMax !== 0 ? Math.trunc(rawMax) : 10;
2040
+ const types = callArgs["types"];
2041
+ const mode = String(callArgs["mode"] ?? "") || "fast";
2042
+ const precisionLevel = String(callArgs["precision_level"] ?? "") || "high";
2043
+ const callConvId = (typeof callArgs["conversation_id"] === "string" && callArgs["conversation_id"] !== "" ? callArgs["conversation_id"] : void 0) ?? conversationId;
2044
+ const shared = {
2045
+ search_query: searchQuery,
2046
+ max_results: maxResults,
2047
+ types,
2048
+ mode,
2049
+ precision_level: precisionLevel
2050
+ };
2051
+ if (scope === "unified") {
2052
+ const response2 = await target.fetch({
2053
+ conversation_id: callConvId,
2054
+ user_id: userId,
2055
+ customer_id: customerId,
2056
+ ...shared
2057
+ });
2058
+ return {
2059
+ formatted_context: response2.formatted_context,
2060
+ scopes_queried: response2.scopes_queried,
2061
+ total_items: response2.total_items
2062
+ };
2063
+ }
2064
+ let response;
2065
+ if (scope === "conversation") {
2066
+ if (callConvId === void 0 || callConvId === "") {
2067
+ return { error: "conversation_id is required", items: [] };
2068
+ }
2069
+ response = await target.conversation.context.fetch({
2070
+ conversation_id: callConvId,
2071
+ ...shared,
2072
+ user_id: userId
2073
+ });
2074
+ } else if (scope === "user") {
2075
+ response = await target.user.context.fetch({
2076
+ user_id: userId,
2077
+ conversation_id: callConvId,
2078
+ ...shared,
2079
+ customer_id: customerId
2080
+ });
2081
+ } else if (scope === "customer") {
2082
+ response = await target.customer.context.fetch({
2083
+ customer_id: customerId,
2084
+ conversation_id: callConvId,
2085
+ ...shared
2086
+ });
2087
+ } else {
2088
+ response = await target.client.context.fetch({
2089
+ conversation_id: callConvId,
2090
+ ...shared
2091
+ });
2092
+ }
2093
+ const out = {};
2094
+ for (const collection of RESULT_COLLECTIONS) {
2095
+ out[collection] = Array.isArray(response[collection]) ? response[collection] : [];
2096
+ }
2097
+ return out;
2098
+ }
2099
+ function buildTool(target, options = {}, onWarning = () => {
2100
+ }) {
2101
+ const scope = String(options.scope ?? "user").toLowerCase();
2102
+ if (!VALID_SCOPES.includes(scope)) {
2103
+ const sorted = [...VALID_SCOPES].sort();
2104
+ throw new InvalidInputError(
2105
+ `scope must be one of ['${sorted.join("', '")}'], got '${String(options.scope)}'`
2106
+ );
2107
+ }
2108
+ const userId = options.user_id ?? options.userId;
2109
+ const customerId = options.customer_id ?? options.customerId;
2110
+ const conversationId = options.conversation_id ?? options.conversationId;
2111
+ if (scope === "user" && (userId === void 0 || userId === "")) {
2112
+ throw new InvalidInputError("scope='user' requires user_id");
2113
+ }
2114
+ if (scope === "customer" && (customerId === void 0 || customerId === "")) {
2115
+ throw new InvalidInputError("scope='customer' requires customer_id");
2116
+ }
2117
+ if (scope === "conversation" && (userId === void 0 || userId === "")) {
2118
+ onWarning(
2119
+ "as_tool(scope='conversation') without user_id: the SDK anticipation cache cannot apply per-user filtering. Pass user_id to enable the Section 15 privacy guarantee."
2120
+ );
2121
+ }
2122
+ const style = String(options.style ?? "openai");
2123
+ if (style !== "openai" && style !== "anthropic") {
2124
+ throw new InvalidInputError(`style must be 'openai' or 'anthropic', got '${style}'`);
2125
+ }
2126
+ const toolName = options.name ?? `synap_fetch_${scope}_context`;
2127
+ const toolDesc = options.description ?? defaultToolDescription(scope);
2128
+ const schema = toolInputSchema(scope, { hasConversationId: conversationId !== void 0 });
2129
+ const handler = (callArgs = {}) => invokeScopeFetch({ target, scope, userId, customerId, conversationId, callArgs });
2130
+ if (style === "openai") {
2131
+ return {
2132
+ type: "function",
2133
+ function: { name: toolName, description: toolDesc, parameters: schema },
2134
+ handler
2135
+ };
2136
+ }
2137
+ return { name: toolName, description: toolDesc, input_schema: schema, handler };
2138
+ }
2139
+
2140
+ // src/client.ts
2141
+ var SynapClient = class {
2142
+ // Native-private (`#`), not TypeScript `private`. TS `private` is erased at
2143
+ // compile time, so the field stays enumerable at runtime and a JS consumer
2144
+ // can still reach it. These two were public and made `client.transport
2145
+ // .credentials.apiKey` and the raw cache's `now`/`fire`/`invalidateEntity`
2146
+ // reachable, which is exactly what `client.cache` exists to prevent.
2147
+ // Definite-assignment assertions (`!`) because the constructor can RETURN an
2148
+ // existing client from the registry before reaching any of these. TypeScript
2149
+ // cannot follow a constructor return, so it sees a path where the fields are
2150
+ // unassigned. On that path the object is discarded and the incumbent is
2151
+ // handed back instead, so nothing observes them.
2152
+ #transport;
2153
+ #anticipationCache;
2154
+ user;
2155
+ customer;
2156
+ client;
2157
+ conversation;
2158
+ memories;
2159
+ credits;
2160
+ cache;
2161
+ instance;
2162
+ #closed = false;
2163
+ #initialized = false;
2164
+ #registryKeys;
2165
+ #compactionSubscribers;
2166
+ #shortTerm;
2167
+ #turns;
2168
+ #fetchHooks;
2169
+ constructor(options = {}) {
2170
+ const credentials = resolveCredentials(options);
2171
+ validateInstanceId(credentials.instanceId);
2172
+ const registryKey = buildRegistryKey(credentials.instanceId, credentials.apiKey);
2173
+ if (options._force_new !== true) {
2174
+ const existing = get(registryKey);
2175
+ if (existing !== void 0) {
2176
+ const incumbent = existing.#transport.currentCredentials().apiKey;
2177
+ if (credentials.apiKey && incumbent && credentials.apiKey !== incumbent) {
2178
+ existing.#warn(
2179
+ `Instance ${registryKey} is already running in this process on a different API key. The key passed here is ignored; this client will authenticate as the existing one. Rotating a key this way has no effect. Construct with apiKey and no instanceId to get a client on your own credential.`
2180
+ );
2181
+ }
2182
+ return existing;
2183
+ }
2184
+ }
2185
+ this.#registryKeys = options._force_new === true ? [] : [registryKey];
2186
+ this.#transport = new HttpTransport({
2187
+ credentials,
2188
+ ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
2189
+ ...options.timeouts !== void 0 ? { timeouts: options.timeouts } : {},
2190
+ ...options.retryPolicy !== void 0 ? { retryPolicy: options.retryPolicy } : {},
2191
+ ...options.keepAlive !== void 0 ? { keepAlive: options.keepAlive } : {},
2192
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
2193
+ userAgent: `@maximem/synap-js-sdk/${SDK_VERSION}`
2194
+ });
2195
+ if (options.heartbeat) this.#transport.startHeartbeat();
2196
+ this.#anticipationCache = new AnticipationCache();
2197
+ this.#fetchHooks = {
2198
+ beforeFetch: (scope, options2) => {
2199
+ const o2 = options2;
2200
+ const attempt = tryServeFromCache(
2201
+ this.#anticipationCache,
2202
+ scope,
2203
+ {
2204
+ searchQuery: o2["search_query"] ?? o2["searchQuery"],
2205
+ entityId: o2["user_id"] ?? o2["userId"],
2206
+ customerId: o2["customer_id"] ?? o2["customerId"],
2207
+ clientId: this.#transport.currentCredentials().clientId || null,
2208
+ conversationId: o2["conversation_id"] ?? o2["conversationId"]
2209
+ },
2210
+ Number(o2["max_results"] ?? o2["maxResults"] ?? 10)
2211
+ );
2212
+ if (attempt.response === null || attempt.hit === null) return null;
2213
+ return {
2214
+ response: attempt.response,
2215
+ servedItemIds: servedItemIds(attempt.hit),
2216
+ bundleId: attempt.hit.bundleIds[0] ?? ""
2217
+ };
2218
+ },
2219
+ onAssembled: (scope, options2, response, startedAt) => {
2220
+ this.#emitContextAssembled(scope, options2, response, startedAt);
2221
+ },
2222
+ onServedFromCache: (_scope, options2, served) => {
2223
+ this.#emitContextUsed(options2, _scope, served);
2224
+ }
2225
+ };
2226
+ this.user = createUserNamespace(this.#transport, this.#fetchHooks);
2227
+ this.customer = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "customer", o2, this.#fetchHooks) } };
2228
+ this.client = { context: { fetch: async (o2 = {}) => fetchContext(this.#transport, "client", o2, this.#fetchHooks) } };
2229
+ this.#compactionSubscribers = new CompactionSubscribers();
2230
+ this.#shortTerm = new ShortTermStore();
2231
+ this.#turns = new TurnCounter();
2232
+ this.conversation = createConversationNamespace(
2233
+ this.#transport,
2234
+ this.#compactionSubscribers,
2235
+ // Mirrors Python's `_invalidate_anticipation_on_write`, including its
2236
+ // gate: `invalidateEntity` is a no-op unless
2237
+ // SYNAP_SDK_CACHE_INVALIDATE_ON_WRITE is set, so default behaviour is
2238
+ // unchanged. Over-invalidating after a failed write is safe; it only
2239
+ // costs the next lookup a cold fetch.
2240
+ ({ userId, customerId }) => {
2241
+ for (const id of /* @__PURE__ */ new Set([userId, customerId])) {
2242
+ if (id) this.#anticipationCache.invalidateEntity(id);
2243
+ }
2244
+ },
2245
+ // Buffer the turn locally so the next fetch sees it, ahead of the server
2246
+ // having persisted and compacted it.
2247
+ ({ conversationId, role, content, timestamp }) => {
2248
+ this.#shortTerm.appendTurn(conversationId, role, content, timestamp);
2249
+ },
2250
+ (response, conversationId, userId) => {
2251
+ overlayLocalRecentTurns(response, this.#shortTerm, conversationId);
2252
+ this.#injectUserSummary(response, conversationId, userId);
2253
+ }
2254
+ );
2255
+ this.memories = createMemoriesInterface(this.#transport);
2256
+ this.credits = createCreditsNamespace(this.#transport);
2257
+ this.cache = createCacheNamespace(this.#anticipationCache);
2258
+ this.instance = createInstanceNamespace(
2259
+ () => this.#transport.currentCredentials(),
2260
+ this.#anticipationCache,
2261
+ ({ conversationId, role, content }) => {
2262
+ this.#shortTerm.appendTurn(conversationId, role, content);
2263
+ }
2264
+ );
2265
+ this.instance._internal.setCompactionApplier((bundle) => {
2266
+ this.#shortTerm.applyCompaction(bundle);
2267
+ });
2268
+ if (options._force_new !== true) {
2269
+ const incumbent = registerIfAbsent(registryKey, this);
2270
+ if (incumbent !== void 0) return incumbent;
2271
+ }
2272
+ this.instance._internal.setCompactionDispatcher((conversationId, bundle) => {
2273
+ this.#compactionSubscribers.dispatch(conversationId, bundle, (error) => {
2274
+ this.#warn(`compaction subscriber threw: ${String(error)}`);
2275
+ });
2276
+ });
2277
+ }
2278
+ // ── Deprecated camelCase surface, carried over from the 0.3.x wrapper ───────
2279
+ // These return the NORMALISED shape, unlike their namespaced equivalents.
2280
+ /** @deprecated Use `client.user.context.fetch()`. Note it returns raw snake_case. */
2281
+ async fetchUserContext(options = {}) {
2282
+ return normalizeContextResponse(await fetchContext(this.#transport, "user", options));
2283
+ }
2284
+ /** @deprecated Use `client.customer.context.fetch()`. */
2285
+ async fetchCustomerContext(options = {}) {
2286
+ return normalizeContextResponse(await fetchContext(this.#transport, "customer", options));
2287
+ }
2288
+ /** @deprecated Use `client.client.context.fetch()`. */
2289
+ async fetchClientContext(options = {}) {
2290
+ return normalizeContextResponse(await fetchContext(this.#transport, "client", options));
2291
+ }
2292
+ /** @deprecated Use `client.conversation.context.get_context_for_prompt()`. */
2293
+ async getContextForPrompt(options = {}) {
2294
+ return this.conversation.context.get_context_for_prompt(options);
2295
+ }
2296
+ /**
2297
+ * @deprecated Use `client.user.context.fetch()` and read the typed collections.
2298
+ *
2299
+ * Flattens every context type into one list, exactly as the 0.3.x bridge did.
2300
+ */
2301
+ async searchMemory(options) {
2302
+ const userId = options.user_id ?? options.userId;
2303
+ if (!userId) throw new InvalidInputError("user_id is required");
2304
+ if (options.query === void 0 || options.query === null) {
2305
+ throw new InvalidInputError("query is required");
2306
+ }
2307
+ const context = await fetchContext(this.#transport, "user", {
2308
+ user_id: userId,
2309
+ ...pick3(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick3(options, "customer_id", "customerId") } : {},
2310
+ ...pick3(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick3(options, "conversation_id", "conversationId") } : {},
2311
+ search_query: [options.query],
2312
+ max_results: pick3(options, "max_results", "maxResults") ?? 10,
2313
+ types: options.types ?? ["all"],
2314
+ ...options.mode !== void 0 ? { mode: options.mode } : {}
2315
+ });
2316
+ const results = flattenContextItems(context);
2317
+ return { results, count: results.length };
2318
+ }
2319
+ /**
2320
+ * @deprecated Use `client.user.context.fetch()` with an empty `search_query`.
2321
+ *
2322
+ * Note the default of 100, not 10: it matches the 0.3.x bridge.
2323
+ */
2324
+ async getMemories(options) {
2325
+ const userId = options.user_id ?? options.userId;
2326
+ if (!userId) throw new InvalidInputError("user_id is required");
2327
+ const context = await fetchContext(this.#transport, "user", {
2328
+ user_id: userId,
2329
+ ...pick3(options, "customer_id", "customerId") !== void 0 ? { customer_id: pick3(options, "customer_id", "customerId") } : {},
2330
+ ...pick3(options, "conversation_id", "conversationId") !== void 0 ? { conversation_id: pick3(options, "conversation_id", "conversationId") } : {},
2331
+ search_query: [],
2332
+ max_results: pick3(options, "max_results", "maxResults") ?? 100,
2333
+ types: options.types ?? ["all"],
2334
+ ...options.mode !== void 0 ? { mode: options.mode } : {}
2335
+ });
2336
+ const results = flattenContextItems(context);
2337
+ return { results, count: results.length };
2338
+ }
2339
+ /** @deprecated Use `client.memories.create()`. */
2340
+ async addMemory(options) {
2341
+ return this.memories.create(options);
2342
+ }
2343
+ /**
2344
+ * @deprecated Use `client.memories.delete(memoryId)`.
2345
+ *
2346
+ * The `{ userId }` form is gone: it relied on a process-local id list and so
2347
+ * deleted nothing in serverless while reporting success (gotcha G-F).
2348
+ */
2349
+ async deleteMemory(options) {
2350
+ return this.memories.delete(options.memory_id ?? options.memoryId ?? "");
2351
+ }
2352
+ /**
2353
+ * Resolve `client_id` and `instance_id` from the API key.
2354
+ *
2355
+ * Mirrors Python's `initialize()`. Calling it is optional here (unlike
2356
+ * Python, which raises "SDK not initialized" from `_ensure_initialized`)
2357
+ * because every HTTP route authenticates on the API key alone. It matters
2358
+ * for the anticipation cache: a bundle shared at client scope is filtered on
2359
+ * `client_id`, and an empty one fails that filter for every bundle. That was
2360
+ * a silent cache-scope bug in Python before whoami bootstrapping existed.
2361
+ *
2362
+ * Best-effort and idempotent. A whoami failure leaves whatever the
2363
+ * constructor resolved and is not fatal.
2364
+ */
2365
+ async initialize() {
2366
+ if (this.#initialized) return;
2367
+ this.#initialized = true;
2368
+ const credentials = this.#transport.currentCredentials();
2369
+ if (credentials.clientId !== "" && credentials.instanceId !== "") return;
2370
+ try {
2371
+ const whoami = await this.#transport.request("whoami", {});
2372
+ const resolvedClientId = String(whoami["client_id"] ?? "");
2373
+ const resolvedInstanceId = String(whoami["instance_id"] ?? "");
2374
+ this.#transport.updateCredentials({
2375
+ ...resolvedClientId !== "" && credentials.clientId === "" ? { clientId: resolvedClientId } : {},
2376
+ ...resolvedInstanceId !== "" && credentials.instanceId === "" ? { instanceId: resolvedInstanceId } : {}
2377
+ });
2378
+ if (resolvedInstanceId !== "" && this.#registryKeys.length > 0) {
2379
+ if (aliasIfAbsent(resolvedInstanceId, this)) {
2380
+ this.#registryKeys.push(resolvedInstanceId);
2381
+ }
2382
+ }
2383
+ } catch {
2384
+ }
2385
+ }
2386
+ /**
2387
+ * @deprecated Use `initialize()`, which is the Python name. `init()` was a
2388
+ * JS-only spelling with no Python counterpart; it now forwards, so a 0.3.x
2389
+ * startup script keeps working.
2390
+ */
2391
+ async init() {
2392
+ return this.initialize();
2393
+ }
2394
+ /** The resolved instance id. Empty until `initialize()` resolves it. */
2395
+ get instance_id() {
2396
+ return this.#transport.currentCredentials().instanceId;
2397
+ }
2398
+ /** The resolved client id. Empty until `initialize()` resolves it. */
2399
+ get client_id() {
2400
+ return this.#transport.currentCredentials().clientId;
2401
+ }
2402
+ /**
2403
+ * Update configuration before `initialize()`.
2404
+ *
2405
+ * Mirrors Python's `configure()`, including its refusal to reconfigure after
2406
+ * initialization: the transport and cache are already built by then and a
2407
+ * half-applied config is worse than a rejected one.
2408
+ *
2409
+ * The Python-only keys (`storage_path`, `cache_backend`,
2410
+ * `session_timeout_minutes`, `logger`) are accepted and ignored rather than
2411
+ * rejected, because this SDK has no SQLite cache backend and no global
2412
+ * logger to swap. Ignoring them keeps a shared config object portable
2413
+ * between the two SDKs.
2414
+ */
2415
+ configure(options = {}) {
2416
+ if (this.#initialized) {
2417
+ throw new InvalidInputError("Cannot reconfigure after initialization");
2418
+ }
2419
+ if (options.timeouts !== void 0) this.#transport.setTimeouts(options.timeouts);
2420
+ if (options.retryPolicy !== void 0 || options.retry_policy !== void 0) {
2421
+ this.#transport.setRetryPolicy(options.retryPolicy ?? options.retry_policy ?? null);
2422
+ }
2423
+ }
2424
+ /**
2425
+ * Fetch and merge context across every scope with an identifier, in one call.
2426
+ *
2427
+ * Mirrors Python's `sdk.fetch()`. The three behaviours worth knowing:
2428
+ *
2429
+ * - Scope fetches run in parallel and a FAILED scope is dropped with a
2430
+ * warning rather than failing the call, so partial context still gets
2431
+ * returned. The one exception is `InvalidInputError`: a bad request must
2432
+ * surface instead of degrading into a silently-empty context.
2433
+ * - `user_id` is threaded into the conversation-scope sub-fetch so the
2434
+ * anticipation cache can apply its per-user filter.
2435
+ * - The summary-mode parameters go ONLY to the user-scope sub-fetch. The
2436
+ * other three routes return 422 for `context_mode="conversation-summary"`,
2437
+ * and that rejection would be swallowed by the drop-failed-scope rule.
2438
+ */
2439
+ async fetch(options = {}) {
2440
+ const {
2441
+ conversation_id: conversationId,
2442
+ user_id: userId,
2443
+ customer_id: customerId,
2444
+ search_query: searchQuery,
2445
+ max_results: maxResults = 20,
2446
+ types,
2447
+ mode = "fast",
2448
+ precision_level: precisionLevel = "high",
2449
+ include_conversation_context: includeConversationContext = true,
2450
+ scopes,
2451
+ include_scope_labels: includeScopeLabels = false,
2452
+ context_mode: contextMode = "in-conversation",
2453
+ include_profile: includeProfile = true,
2454
+ last_n_conversations: lastNConversations = 1
2455
+ } = options;
2456
+ validateConversationId(conversationId);
2457
+ const wants = (scope) => scopes === void 0 || scopes.includes(scope);
2458
+ const shared = {
2459
+ search_query: searchQuery,
2460
+ max_results: maxResults,
2461
+ types,
2462
+ mode,
2463
+ precision_level: precisionLevel
2464
+ };
2465
+ const labels = [];
2466
+ const tasks = [];
2467
+ if (conversationId !== void 0 && conversationId !== "" && wants("conversation")) {
2468
+ labels.push("conversation");
2469
+ tasks.push(this.conversation.context.fetch({
2470
+ conversation_id: conversationId,
2471
+ ...shared,
2472
+ user_id: userId,
2473
+ customer_id: customerId
2474
+ }));
2475
+ }
2476
+ if (userId !== void 0 && userId !== "" && wants("user")) {
2477
+ labels.push("user");
2478
+ tasks.push(this.user.context.fetch({
2479
+ user_id: userId,
2480
+ conversation_id: conversationId,
2481
+ ...shared,
2482
+ customer_id: customerId,
2483
+ context_mode: contextMode,
2484
+ include_profile: includeProfile,
2485
+ last_n_conversations: lastNConversations
2486
+ }));
2487
+ }
2488
+ if (customerId !== void 0 && customerId !== "" && wants("customer")) {
2489
+ labels.push("customer");
2490
+ tasks.push(this.customer.context.fetch({
2491
+ customer_id: customerId,
2492
+ conversation_id: conversationId,
2493
+ ...shared
2494
+ }));
2495
+ }
2496
+ if (scopes !== void 0 && scopes.includes("client")) {
2497
+ labels.push("client");
2498
+ tasks.push(this.client.context.fetch({
2499
+ conversation_id: conversationId,
2500
+ ...shared
2501
+ }));
2502
+ }
2503
+ if (tasks.length === 0) {
2504
+ const empty = mergeScopeResults([]);
2505
+ return empty;
2506
+ }
2507
+ const settled = await Promise.allSettled(tasks);
2508
+ const successful = [];
2509
+ for (const [i, result] of settled.entries()) {
2510
+ const label = labels[i];
2511
+ if (result.status === "fulfilled") {
2512
+ successful.push([label, result.value]);
2513
+ continue;
2514
+ }
2515
+ if (result.reason instanceof InvalidInputError) throw result.reason;
2516
+ this.#warn(`Scope '${label}' fetch failed in unified fetch (non-fatal): ${String(result.reason)}`);
2517
+ }
2518
+ const merged = mergeScopeResults(successful);
2519
+ for (const [, response] of successful) {
2520
+ overlayLocalRecentTurns(response, this.#shortTerm, conversationId);
2521
+ }
2522
+ if (includeConversationContext && conversationId !== void 0 && conversationId !== "") {
2523
+ try {
2524
+ merged.conversation_context = await this.conversation.context.get_context_for_prompt({
2525
+ conversation_id: conversationId
2526
+ });
2527
+ } catch {
2528
+ }
2529
+ }
2530
+ merged.formatted_context = formatForPrompt(merged, {
2531
+ includeScope: includeScopeLabels,
2532
+ includeConversationContext
2533
+ });
2534
+ return merged;
2535
+ }
2536
+ /**
2537
+ * An LLM-ready tool definition that fetches Synap context.
2538
+ *
2539
+ * Mirrors Python's `as_tool()`. The scope identifiers are closed over, so
2540
+ * the model cannot choose whose memory to read.
2541
+ */
2542
+ as_tool(options = {}) {
2543
+ return buildTool(this, options, (message) => this.#warn(message));
2544
+ }
2545
+ /**
2546
+ * A read-only diagnostic view of the in-process anticipation cache.
2547
+ *
2548
+ * Mirrors Python's `anticipation_cache_snapshot()`. Returns the same
2549
+ * snake_case shape, since this output gets logged and compared across SDKs.
2550
+ */
2551
+ anticipation_cache_snapshot() {
2552
+ return this.#anticipationCache.snapshot();
2553
+ }
2554
+ /**
2555
+ * Report a locally served fetch back to the server.
2556
+ *
2557
+ * Only while listening: the event rides the Listen stream, and there is
2558
+ * nowhere to put it otherwise. Never throws, because a telemetry failure
2559
+ * must not fail a retrieval that already succeeded.
2560
+ */
2561
+ #emitContextUsed(options, scope, served) {
2562
+ try {
2563
+ if (!this.instance.is_listening) return;
2564
+ this.instance._internal.sendContextUsed({
2565
+ bundle_id: served.bundleId,
2566
+ conversation_id: String(options["conversation_id"] ?? options["conversationId"] ?? ""),
2567
+ user_id: String(options["user_id"] ?? options["userId"] ?? ""),
2568
+ customer_id: String(options["customer_id"] ?? options["customerId"] ?? ""),
2569
+ served_item_ids: served.servedItemIds,
2570
+ scope,
2571
+ source_bundle_ids: served.bundleId ? [served.bundleId] : [],
2572
+ timestamp_ms: Date.now()
2573
+ });
2574
+ } catch {
2575
+ }
2576
+ }
2577
+ /**
2578
+ * Report what was actually composed for this turn.
2579
+ *
2580
+ * Fires whether the fetch was served from the cache or the network, because
2581
+ * the audit needs to distinguish them. Like the other stream events it is
2582
+ * fire-and-forget and only goes anywhere while listening.
2583
+ */
2584
+ #emitContextAssembled(scope, options, response, startedAt) {
2585
+ try {
2586
+ if (!this.instance.is_listening) return;
2587
+ this.instance._internal.sendContextAssembled({
2588
+ ...buildAssembledEvent({
2589
+ correlationId: newCorrelationId(),
2590
+ response,
2591
+ conversationId: options["conversation_id"] ?? options["conversationId"],
2592
+ userId: options["user_id"] ?? options["userId"],
2593
+ customerId: options["customer_id"] ?? options["customerId"],
2594
+ startedAt,
2595
+ sdkVersion: SDK_VERSION
2596
+ }),
2597
+ scope
2598
+ });
2599
+ } catch {
2600
+ }
2601
+ }
2602
+ /**
2603
+ * Fold a cached user summary in, every Nth turn.
2604
+ *
2605
+ * Only when the caller supplied a user_id. Without one the summary lookup
2606
+ * cannot be scoped, and an unscoped lookup is how user A's summary ends up in
2607
+ * user B's response.
2608
+ */
2609
+ #injectUserSummary(response, conversationId, userId) {
2610
+ this.#turns.increment(conversationId);
2611
+ if (userId === void 0 || userId === "") return;
2612
+ if (!this.#turns.shouldInject(conversationId)) return;
2613
+ const summary = this.#anticipationCache.lookupUserSummary(userId);
2614
+ if (summary === null) return;
2615
+ mergeUserSummary(response, summary);
2616
+ }
2617
+ /** Python logs via `logger.warning`; here it is one overridable seam. */
2618
+ #warn(message) {
2619
+ console.warn(`[synap] ${message}`);
2620
+ }
2621
+ /** Release timers and connections. Safe to call more than once. */
2622
+ async shutdown() {
2623
+ if (this.#closed) return;
2624
+ this.#closed = true;
2625
+ this.#shortTerm.clear();
2626
+ this.#turns.clear();
2627
+ for (const key of this.#registryKeys) unregisterIfOwner(key, this);
2628
+ await this.instance.stop_listening();
2629
+ this.#compactionSubscribers.removeAll();
2630
+ this.#transport.close();
2631
+ this.#anticipationCache.clear();
2632
+ }
2633
+ };
2634
+ function pick3(o2, snake, camel) {
2635
+ const a = o2[snake];
2636
+ return a !== void 0 ? a : o2[camel];
2637
+ }
2638
+ function resolveCredentials(options) {
2639
+ const apiKey = options.apiKey ?? getEnv("SYNAP_API_KEY");
2640
+ if (!apiKey) {
2641
+ throw new InvalidInputError(
2642
+ "No Synap API key found. Set SYNAP_API_KEY in your environment, or pass apiKey to the SynapClient constructor."
2643
+ );
2644
+ }
2645
+ return {
2646
+ apiKey,
2647
+ clientId: options.clientId ?? getEnv("SYNAP_CLIENT_ID") ?? "",
2648
+ instanceId: options.instanceId ?? getEnv("SYNAP_INSTANCE_ID") ?? ""
2649
+ };
2650
+ }
2651
+ export {
2652
+ ANY_SCOPE,
2653
+ AgentUnavailableError,
2654
+ AnticipationCache,
2655
+ AuthenticationError,
2656
+ BM25,
2657
+ CONTRACT,
2658
+ ConflictError,
2659
+ ConnectionError,
2660
+ ContextNotFoundError,
2661
+ DEFAULT_BASE_URL,
2662
+ DEFAULT_DOCUMENT_TYPE,
2663
+ DEFAULT_INGEST_MODE,
2664
+ DEFAULT_RETRY_POLICY,
2665
+ DEFAULT_TIMEOUTS,
2666
+ DOCUMENT_TYPES,
2667
+ ENDPOINTS,
2668
+ HttpTransport,
2669
+ INGEST_MODES,
2670
+ ITEM_COLLECTIONS,
2671
+ InsufficientCreditsError,
2672
+ InvalidConversationIdError,
2673
+ InvalidInputError,
2674
+ InvalidInstanceIdError,
2675
+ ListeningAlreadyActiveError,
2676
+ ListeningNotActiveError,
2677
+ NetworkTimeoutError,
2678
+ PermanentError,
2679
+ RateLimitError,
2680
+ SDKError,
2681
+ SDK_VERSION,
2682
+ ServiceUnavailableError,
2683
+ SessionExpiredError,
2684
+ SynapClient,
2685
+ SynapError,
2686
+ SynapPermanentError,
2687
+ SynapTransientError,
2688
+ THRESHOLDS,
2689
+ TranscriptConflictError,
2690
+ TransientError,
2691
+ flattenContextItems,
2692
+ formatForPrompt,
2693
+ isOutcomeUnknown,
2694
+ isRecallQuery,
2695
+ isSynapError,
2696
+ mergeScopeResults,
2697
+ resolvePath,
2698
+ shouldRetry,
2699
+ stem,
2700
+ tokenize
2701
+ };
2702
+ //# sourceMappingURL=index.js.map