@maximem/synap-js-sdk 0.4.2 → 0.4.4

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.
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  TranscriptConflictError,
23
23
  TransientError,
24
24
  isSynapError
25
- } from "./chunk-T3NZBIV2.js";
25
+ } from "./chunk-OCMM2Z5P.js";
26
26
  import {
27
27
  ANY_SCOPE,
28
28
  AnticipationCache,
@@ -200,6 +200,16 @@ var Heartbeat = class {
200
200
  }
201
201
  };
202
202
 
203
+ // src/scoping.ts
204
+ var B2C_ISOLATION = "equals_customer";
205
+ function checkCustomerId(isolation, customerId, where, instanceId) {
206
+ if (customerId === void 0 || customerId === null || customerId === "") return;
207
+ if (isolation !== B2C_ISOLATION) return;
208
+ throw new InvalidInputError(
209
+ `${where}: customer_id is not accepted on this instance${instanceId ? ` (${instanceId})` : ""}, which is B2C (user_context_isolation='${B2C_ISOLATION}'). Send user_id only; it is the whole identity, and the server files and reads your data under it. Received customer_id=${JSON.stringify(customerId)}.`
210
+ );
211
+ }
212
+
203
213
  // src/transport/http.ts
204
214
  var DEFAULT_BASE_URL = "https://synap-cloud-prod.maximem.ai";
205
215
  var DEFAULT_TIMEOUTS = { connect: 5, read: 30, write: 10 };
@@ -219,7 +229,7 @@ var HttpTransport = class {
219
229
  this.#credentials = options.credentials;
220
230
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
221
231
  this.timeouts = { ...DEFAULT_TIMEOUTS, ...options.timeouts };
222
- this.retryPolicy = { ...DEFAULT_RETRY_POLICY, ...options.retryPolicy };
232
+ this.retryPolicy = options.retryPolicy === null ? { ...DEFAULT_RETRY_POLICY, maxAttempts: 1 } : { ...DEFAULT_RETRY_POLICY, ...options.retryPolicy };
223
233
  this.userAgent = options.userAgent ?? "@maximem/synap-js-sdk";
224
234
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
225
235
  this.keepAliveOptions = options.keepAlive ?? {};
@@ -253,12 +263,38 @@ var HttpTransport = class {
253
263
  updateCredentials(patch) {
254
264
  this.#credentials = { ...this.#credentials, ...patch };
255
265
  }
266
+ /**
267
+ * The instance's scoping mode, from whoami. Lives here rather than on the
268
+ * client because every interface factory already receives the transport,
269
+ * and it is per-client, so a module-level global would be wrong the moment a
270
+ * process holds two clients.
271
+ *
272
+ * undefined means the server did not report it, which is true of any
273
+ * deployment older than the field, and must mean "enforce nothing".
274
+ */
275
+ userContextIsolation = void 0;
276
+ /** Refuse a customer_id on a B2C instance, at the call site. */
277
+ checkCustomerId(customerId, where) {
278
+ checkCustomerId(
279
+ this.userContextIsolation,
280
+ customerId,
281
+ where,
282
+ this.#credentials.instanceId
283
+ );
284
+ }
256
285
  setTimeouts(timeouts) {
257
286
  this.timeouts = { ...this.timeouts, ...timeouts };
258
287
  }
259
- /** `null` restores the default policy, matching Python's `retry_policy=None`. */
288
+ /**
289
+ * `null` DISABLES retries, matching Python.
290
+ *
291
+ * Python's `SDKConfig(retry_policy=None)` leaves `self.retry_policy` unset and
292
+ * `max_attempts` falls to 1 (http_client.py:180). We restored the default
293
+ * policy instead, so a caller following the "Disabling Retries" documentation
294
+ * got three attempts and no way to turn them off.
295
+ */
260
296
  setRetryPolicy(policy) {
261
- this.retryPolicy = policy === null ? { ...DEFAULT_RETRY_POLICY } : { ...this.retryPolicy, ...policy };
297
+ this.retryPolicy = policy === null ? { ...DEFAULT_RETRY_POLICY, maxAttempts: 1 } : { ...this.retryPolicy, ...policy };
262
298
  }
263
299
  close() {
264
300
  this.closed = true;
@@ -406,7 +442,9 @@ async function toHttpError(response, correlationId) {
406
442
  return new InsufficientCreditsError(message, {
407
443
  ...opts,
408
444
  balanceCredits: numberOrNull(parsed["balance_credits"]),
409
- requiredCredits: numberOrNull(parsed["required_credits"])
445
+ requiredCredits: numberOrNull(parsed["minimum_required_credits"]) ?? numberOrNull(parsed["required_credits"]),
446
+ recoveryUrl: stringOrNull(parsed["recovery_url"]),
447
+ redeemUrl: stringOrNull(parsed["redeem_url"])
410
448
  });
411
449
  case 409:
412
450
  if (detailObject !== null && detailObject["code"] === "transcript_conflict") {
@@ -433,6 +471,9 @@ async function toHttpError(response, correlationId) {
433
471
  function numberOrNull(v) {
434
472
  return typeof v === "number" && Number.isFinite(v) ? v : null;
435
473
  }
474
+ function stringOrNull(v) {
475
+ return typeof v === "string" && v !== "" ? v : null;
476
+ }
436
477
  function withTimeout(ms, external) {
437
478
  const controller = new AbortController();
438
479
  const onExternalAbort = () => controller.abort(external?.reason);
@@ -507,9 +548,32 @@ function buildFetchRequest(scope, options) {
507
548
  }
508
549
  switch (scope) {
509
550
  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;
551
+ {
552
+ if (!userId) throw new InvalidInputError("user_id is required");
553
+ body["user_id"] = userId;
554
+ const scopePath = opt(options, "scope_path", "scopePath");
555
+ if (scopePath !== void 0) body["scope"] = scopePath;
556
+ const contextMode = opt(options, "context_mode", "contextMode") ?? "in-conversation";
557
+ if (!VALID_CONTEXT_MODES.includes(contextMode)) {
558
+ throw new InvalidInputError(
559
+ `Invalid context_mode '${contextMode}'. Must be one of: ${VALID_CONTEXT_MODES.join(", ")}`
560
+ );
561
+ }
562
+ const lastN = opt(options, "last_n_conversations", "lastNConversations") ?? 1;
563
+ if (!Number.isInteger(lastN)) {
564
+ throw new InvalidInputError("last_n_conversations must be an integer");
565
+ }
566
+ if (lastN < 0 || lastN > 20) {
567
+ throw new InvalidInputError("last_n_conversations must be between 0 and 20");
568
+ }
569
+ if (contextMode === "conversation-summary") {
570
+ body["context_mode"] = contextMode;
571
+ body["include_profile"] = opt(options, "include_profile", "includeProfile") ?? true;
572
+ body["last_n_conversations"] = lastN;
573
+ }
574
+ if (customerId !== void 0) body["customer_id"] = customerId;
575
+ break;
576
+ }
513
577
  break;
514
578
  case "customer":
515
579
  if (!customerId) throw new InvalidInputError("customer_id is required");
@@ -525,8 +589,13 @@ function buildFetchRequest(scope, options) {
525
589
  }
526
590
  return { endpoint: ENDPOINT_BY_SCOPE[scope], body };
527
591
  }
592
+ var VALID_CONTEXT_MODES = ["in-conversation", "conversation-summary"];
528
593
  async function fetchContext(transport, scope, options = {}, hooks = {}) {
529
594
  const startedAt = Date.now();
595
+ transport.checkCustomerId(
596
+ opt(options, "customer_id", "customerId"),
597
+ `${scope}.context.fetch`
598
+ );
530
599
  const anticipated = hooks.beforeFetch?.(scope, options) ?? null;
531
600
  if (anticipated !== null) {
532
601
  hooks.onServedFromCache?.(scope, options, anticipated);
@@ -787,6 +856,8 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "error",
787
856
  function createMemoriesInterface(transport) {
788
857
  return {
789
858
  async create(options) {
859
+ const o2 = options;
860
+ transport.checkCustomerId(o2["customer_id"] ?? o2["customerId"], "memories.create");
790
861
  return transport.request("memories_create", { body: buildCreateBody(options) });
791
862
  },
792
863
  async batch_create(options) {
@@ -803,7 +874,7 @@ function createMemoriesInterface(transport) {
803
874
  const userId = options.user_id ?? options.userId;
804
875
  const customerId = options.customer_id ?? options.customerId;
805
876
  if (!userId) throw new InvalidInputError("user_id is required");
806
- if (!customerId) throw new InvalidInputError("customer_id is required");
877
+ transport.checkCustomerId(customerId, "memories.create_from_file");
807
878
  const filePath = options.file_path ?? options.filePath;
808
879
  const sources = [filePath, options.file, options.text].filter((v) => v !== void 0);
809
880
  if (sources.length === 0) {
@@ -924,6 +995,15 @@ function validateInstanceId(instanceId) {
924
995
  }
925
996
 
926
997
  // src/conversation/interface.ts
998
+ var COMPACTION_LEVELS = [
999
+ "low",
1000
+ "medium",
1001
+ "high",
1002
+ "adaptive",
1003
+ "aggressive",
1004
+ "balanced",
1005
+ "conservative"
1006
+ ];
927
1007
  function either2(o2, snake, camel) {
928
1008
  const a = o2[snake];
929
1009
  if (a !== void 0) return a;
@@ -946,7 +1026,6 @@ function buildRecordMessageBody(options) {
946
1026
  throw new InvalidInputError("content is required");
947
1027
  }
948
1028
  if (!userId) throw new InvalidInputError("user_id is required");
949
- if (!customerId) throw new InvalidInputError("customer_id is required");
950
1029
  const body = {
951
1030
  conversation_id: conversationId,
952
1031
  role: options.role,
@@ -1051,6 +1130,10 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1051
1130
  };
1052
1131
  return {
1053
1132
  async record_message(options) {
1133
+ transport.checkCustomerId(
1134
+ options["customer_id"] ?? options["customerId"],
1135
+ "conversation.record_message"
1136
+ );
1054
1137
  const body = buildRecordMessageBody(options);
1055
1138
  const result = await transport.request("conversations_messages", { body });
1056
1139
  onTurn({
@@ -1069,6 +1152,13 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1069
1152
  if (!Array.isArray(messages) || messages.length === 0) {
1070
1153
  throw new InvalidInputError("messages must be a non-empty array");
1071
1154
  }
1155
+ for (const m of messages) {
1156
+ const mo = m;
1157
+ transport.checkCustomerId(
1158
+ mo["customer_id"] ?? mo["customerId"],
1159
+ "conversation.record_messages_batch"
1160
+ );
1161
+ }
1072
1162
  const bodies = messages.map(buildRecordMessageBody);
1073
1163
  const result = await transport.request("conversations_messages_batch", {
1074
1164
  body: { messages: bodies }
@@ -1184,15 +1274,26 @@ function createUserNamespace(transport, hooks = {}) {
1184
1274
  }
1185
1275
 
1186
1276
  // src/credits/interface.ts
1277
+ function iso2(v) {
1278
+ return v instanceof Date ? v.toISOString() : v;
1279
+ }
1187
1280
  function createCreditsNamespace(transport) {
1188
1281
  return {
1189
1282
  async get_balance() {
1190
1283
  return transport.request("credits_balance");
1191
1284
  },
1192
1285
  async get_ledger(options = {}) {
1193
- return transport.request("credits_ledger", {
1194
- query: { limit: options.limit ?? 50, offset: options.offset ?? 0 }
1195
- });
1286
+ const query = {
1287
+ limit: options.limit ?? 100,
1288
+ offset: options.offset ?? 0
1289
+ };
1290
+ const entryType = options.entry_type ?? options.entryType;
1291
+ if (entryType !== void 0) query["entry_type"] = entryType;
1292
+ const from = options.from_time ?? options.fromTime;
1293
+ if (from !== void 0) query["from"] = iso2(from);
1294
+ const to = options.to_time ?? options.toTime;
1295
+ if (to !== void 0) query["to"] = iso2(to);
1296
+ return transport.request("credits_ledger", { query });
1196
1297
  },
1197
1298
  async estimate(options) {
1198
1299
  return transport.request("credits_estimate", {
@@ -1272,7 +1373,7 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1272
1373
  if (client !== null) {
1273
1374
  throw new ListeningAlreadyActiveError("Listening is already active on this instance");
1274
1375
  }
1275
- const { GrpcStreamClient } = await import("./stream-client-ME5N5NK4.js");
1376
+ const { GrpcStreamClient } = await import("./stream-client-6V4FGKZ3.js");
1276
1377
  const onContext = options.on_context ?? options.onContext;
1277
1378
  const onDisconnect = options.on_disconnect ?? options.onDisconnect;
1278
1379
  const onReconnect = options.on_reconnect ?? options.onReconnect;
@@ -1332,6 +1433,12 @@ function createInstanceNamespace(credentials, cache, onTurn) {
1332
1433
  session_id: options.session_id ?? options.sessionId ?? "",
1333
1434
  metadata: options.metadata ?? {},
1334
1435
  timestamp_ms: Date.now(),
1436
+ tool_name: options.tool_name ?? options.toolName ?? "",
1437
+ // Python serialises tool_args with json.dumps into tool_args_json.
1438
+ tool_args_json: (() => {
1439
+ const a = options.tool_args ?? options.toolArgs;
1440
+ return a === void 0 ? "" : JSON.stringify(a);
1441
+ })(),
1335
1442
  search_queries: options.search_queries ?? options.searchQueries ?? [],
1336
1443
  context_types: options.context_types ?? options.contextTypes ?? []
1337
1444
  });
@@ -2377,6 +2484,8 @@ var SynapClient = class {
2377
2484
  const whoami = await this.#transport.request("whoami", {});
2378
2485
  const resolvedClientId = String(whoami["client_id"] ?? "");
2379
2486
  const resolvedInstanceId = String(whoami["instance_id"] ?? "");
2487
+ const iso3 = whoami["user_context_isolation"];
2488
+ this.#transport.userContextIsolation = typeof iso3 === "string" ? iso3 : void 0;
2380
2489
  this.#transport.updateCredentials({
2381
2490
  ...resolvedClientId !== "" && credentials.clientId === "" ? { clientId: resolvedClientId } : {},
2382
2491
  ...resolvedInstanceId !== "" && credentials.instanceId === "" ? { instanceId: resolvedInstanceId } : {}
@@ -2665,6 +2774,7 @@ export {
2665
2774
  AnticipationCache,
2666
2775
  AuthenticationError,
2667
2776
  BM25,
2777
+ COMPACTION_LEVELS,
2668
2778
  CONTRACT,
2669
2779
  ConflictError,
2670
2780
  ConnectionError,