@superatomai/sdk-node 0.0.93-dsp → 0.0.95-dsp

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.mjs CHANGED
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
6
6
  });
7
7
 
8
8
  // src/index.ts
9
- import { randomUUID as randomUUID3 } from "crypto";
9
+ import { randomUUID as randomUUID4 } from "crypto";
10
10
 
11
11
  // src/websocket.ts
12
12
  import WebSocket from "ws";
@@ -2909,6 +2909,7 @@ async function handleDataRequestInner(data, collections, sendMessage, tools) {
2909
2909
  let requestId;
2910
2910
  let collection;
2911
2911
  let op;
2912
+ const fromId = typeof data?.from?.id === "string" ? data.from.id : void 0;
2912
2913
  try {
2913
2914
  const dataRequest = DataRequestMessageSchema.parse(data);
2914
2915
  const { id, payload } = dataRequest;
@@ -2919,13 +2920,13 @@ async function handleDataRequestInner(data, collections, sendMessage, tools) {
2919
2920
  if (!collections[collection]) {
2920
2921
  sendDataResponse(id, collection, op, null, {
2921
2922
  error: `Collection '${collection}' not found`
2922
- }, sendMessage);
2923
+ }, sendMessage, fromId);
2923
2924
  return;
2924
2925
  }
2925
2926
  if (!collections[collection][op]) {
2926
2927
  sendDataResponse(id, collection, op, null, {
2927
2928
  error: `Operation '${op}' not found for collection '${collection}'`
2928
- }, sendMessage);
2929
+ }, sendMessage, fromId);
2929
2930
  return;
2930
2931
  }
2931
2932
  const startTime = performance.now();
@@ -2997,21 +2998,22 @@ async function handleDataRequestInner(data, collections, sendMessage, tools) {
2997
2998
  logger.warn(`UIBlock ${uiBlockId} not found in threads`);
2998
2999
  }
2999
3000
  }
3000
- sendDataResponse(id, collection, op, result, { executionMs }, sendMessage);
3001
+ sendDataResponse(id, collection, op, result, { executionMs }, sendMessage, fromId);
3001
3002
  } catch (error) {
3002
3003
  const errorMsg = error instanceof Error ? error.message : String(error);
3003
3004
  logger.error("Failed to handle data request:", error);
3004
3005
  if (requestId) {
3005
3006
  sendDataResponse(requestId, collection || "unknown", op || "unknown", null, {
3006
3007
  error: errorMsg
3007
- }, sendMessage);
3008
+ }, sendMessage, fromId);
3008
3009
  }
3009
3010
  }
3010
3011
  }
3011
- function sendDataResponse(id, collection, op, data, meta, sendMessage) {
3012
+ function sendDataResponse(id, collection, op, data, meta, sendMessage, fromId) {
3012
3013
  const response = {
3013
3014
  id,
3014
3015
  from: { type: "data-agent" },
3016
+ to: fromId ? { id: fromId } : void 0,
3015
3017
  type: "DATA_RES",
3016
3018
  payload: {
3017
3019
  collection,
@@ -28243,6 +28245,459 @@ var ReportManager = class {
28243
28245
  }
28244
28246
  };
28245
28247
 
28248
+ // src/parcels/index.ts
28249
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
28250
+ var PARCEL_THRESHOLD_BYTES = 256 * 1024;
28251
+ var MAX_INLINE_BYTES = 900 * 1024;
28252
+ var MAX_PARCEL_BYTES = 95 * 1024 * 1024;
28253
+ var PART_BYTES = 600 * 1024;
28254
+ var PART_WAIT_MS = 6e4;
28255
+ var NEVER_PARCEL = /* @__PURE__ */ new Set(["PING", "PONG", "BUNDLE_CHUNK", "BUNDLE_CSS_CHUNK"]);
28256
+ var RETRY_DELAYS_MS = [500, 1500];
28257
+ var REQUEST_TIMEOUT_MS = 6e4;
28258
+ var MAX_DEPTH = 32;
28259
+ var PARCEL_FIELD = "__parcel";
28260
+ var FILE_FIELD = "__parcelFile";
28261
+ var PART_FIELD = "__parcelPart";
28262
+ var partOf = (payload) => {
28263
+ const p = payload?.[PART_FIELD];
28264
+ return p && typeof p.id === "string" && typeof p.data === "string" && Number.isInteger(p.total) && Number.isInteger(p.index) && p.total > 0 && p.index >= 0 && p.index < p.total ? p : null;
28265
+ };
28266
+ var isRef = (ref) => !!ref && typeof ref === "object" && typeof ref.projectId === "string" && typeof ref.hash === "string" && typeof ref.ext === "string" && typeof ref.size === "number";
28267
+ function parcelRefOf(payload) {
28268
+ const ref = payload?.[PARCEL_FIELD];
28269
+ return isRef(ref) ? ref : null;
28270
+ }
28271
+ var fileRefOf = (value) => {
28272
+ const ref = value?.[FILE_FIELD];
28273
+ return typeof ref?.contentType === "string" && isRef(ref) ? ref : null;
28274
+ };
28275
+ var sha256Hex = (bytes) => createHash3("sha256").update(bytes).digest("hex");
28276
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
28277
+ var ParcelHttpError = class extends Error {
28278
+ constructor(message, status) {
28279
+ super(message);
28280
+ this.status = status;
28281
+ }
28282
+ };
28283
+ var retryable = (err) => !(err instanceof ParcelHttpError) || err.status >= 500 || err.status === 408 || err.status === 429;
28284
+ async function withRetries(what, run) {
28285
+ let lastError;
28286
+ for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
28287
+ try {
28288
+ return await run();
28289
+ } catch (err) {
28290
+ lastError = err;
28291
+ if (!retryable(err) || attempt === RETRY_DELAYS_MS.length) break;
28292
+ logger.warn(`[parcels] ${what} failed (attempt ${attempt + 1}), retrying: ${err?.message ?? err}`);
28293
+ await sleep(RETRY_DELAYS_MS[attempt] ?? 0);
28294
+ }
28295
+ }
28296
+ throw lastError;
28297
+ }
28298
+ var ParcelClient = class {
28299
+ constructor(opts) {
28300
+ this.opts = opts;
28301
+ this.base = opts.apiUrl.replace(/\/+$/, "");
28302
+ this.fetchImpl = opts.fetchImpl ?? fetch;
28303
+ this.timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
28304
+ }
28305
+ url(ref) {
28306
+ return `${this.base}/parcels/${encodeURIComponent(ref.projectId)}/${ref.hash}.${ref.ext}`;
28307
+ }
28308
+ async call(url, init) {
28309
+ const res = await this.fetchImpl(url, {
28310
+ ...init,
28311
+ headers: { ...init.headers, Authorization: `Bearer ${this.opts.apiKey}` },
28312
+ signal: AbortSignal.timeout(this.timeoutMs)
28313
+ });
28314
+ if (!res.ok) {
28315
+ const text = await res.text().catch(() => "");
28316
+ throw new ParcelHttpError(`HTTP ${res.status}${text ? ` ${text.slice(0, 200)}` : ""}`, res.status);
28317
+ }
28318
+ return res;
28319
+ }
28320
+ /** The receipt the bytes would get, without storing them. */
28321
+ refFor(bytes, ext) {
28322
+ return { projectId: this.opts.projectId, hash: sha256Hex(bytes), ext, size: bytes.byteLength };
28323
+ }
28324
+ /** Stores the bytes and returns the receipt for them. */
28325
+ async upload(bytes, ext = "json", contentType = "application/json") {
28326
+ if (bytes.byteLength > MAX_PARCEL_BYTES) {
28327
+ throw new Error(`Parcel too large (${bytes.byteLength} bytes, limit ${MAX_PARCEL_BYTES})`);
28328
+ }
28329
+ const ref = this.refFor(bytes, ext);
28330
+ await withRetries(
28331
+ "upload",
28332
+ () => this.call(this.url(ref), { method: "PUT", headers: { "Content-Type": contentType }, body: bytes })
28333
+ );
28334
+ return ref;
28335
+ }
28336
+ /** The bytes behind a receipt, checked against its hash. */
28337
+ async download(ref) {
28338
+ return withRetries("download", async () => {
28339
+ const res = await this.call(this.url(ref), { method: "GET" });
28340
+ const bytes = new Uint8Array(await res.arrayBuffer());
28341
+ if (sha256Hex(bytes) !== ref.hash) {
28342
+ throw new ParcelHttpError("Parcel content does not match its hash", 502);
28343
+ }
28344
+ return bytes;
28345
+ });
28346
+ }
28347
+ };
28348
+ var ParcelFile = class {
28349
+ constructor(ref, client) {
28350
+ this.ref = ref;
28351
+ this.client = client;
28352
+ this.name = ref.name;
28353
+ this.size = ref.size;
28354
+ this.contentType = ref.contentType;
28355
+ }
28356
+ /**
28357
+ * The file's bytes, checked against their hash — from the message itself when
28358
+ * it carried them (storage was down), otherwise from storage. Throws if they
28359
+ * cannot be fetched.
28360
+ */
28361
+ async download() {
28362
+ if (typeof this.ref.data === "string") {
28363
+ const bytes = new Uint8Array(Buffer.from(this.ref.data, "base64"));
28364
+ if (sha256Hex(bytes) !== this.ref.hash) throw new Error("File content does not match its hash");
28365
+ return bytes;
28366
+ }
28367
+ if (!this.client) throw new Error("Cannot download files: SA_API_URL or the API key is not set");
28368
+ return this.client.download(this.ref);
28369
+ }
28370
+ toJSON() {
28371
+ return { [FILE_FIELD]: this.ref };
28372
+ }
28373
+ };
28374
+ var isBinary = (v) => v instanceof Uint8Array || v instanceof ArrayBuffer;
28375
+ var isPlain = (v) => {
28376
+ if (!v || typeof v !== "object") return false;
28377
+ const proto = Object.getPrototypeOf(v);
28378
+ return proto === Object.prototype || proto === null;
28379
+ };
28380
+ function hasBinary(value, depth = 0) {
28381
+ if (isBinary(value)) return true;
28382
+ if (depth >= MAX_DEPTH) return false;
28383
+ if (Array.isArray(value)) return value.some((v) => hasBinary(v, depth + 1));
28384
+ if (isPlain(value)) return Object.values(value).some((v) => hasBinary(v, depth + 1));
28385
+ return false;
28386
+ }
28387
+ function hasFileRef(value, depth = 0) {
28388
+ if (fileRefOf(value)) return true;
28389
+ if (depth >= MAX_DEPTH) return false;
28390
+ if (Array.isArray(value)) return value.some((v) => hasFileRef(v, depth + 1));
28391
+ if (isPlain(value)) return Object.values(value).some((v) => hasFileRef(v, depth + 1));
28392
+ return false;
28393
+ }
28394
+ var extOf = (name) => typeof name === "string" && /\.([a-z0-9]{1,10})$/i.exec(name)?.[1]?.toLowerCase() || "bin";
28395
+ async function uploadFiles(value, client, storage, parent, depth = 0) {
28396
+ if (isBinary(value)) {
28397
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
28398
+ const name = parent?.name ?? parent?.fileName;
28399
+ const contentType = String(parent?.contentType ?? parent?.mimeType ?? "application/octet-stream");
28400
+ const named = typeof name === "string" ? { name } : {};
28401
+ if (!storage.down) {
28402
+ try {
28403
+ const ref = await client.upload(bytes, extOf(name), contentType);
28404
+ const fileRef2 = { ...ref, contentType, ...named };
28405
+ return { [FILE_FIELD]: fileRef2 };
28406
+ } catch (err) {
28407
+ if (bytes.byteLength > MAX_PARCEL_BYTES) throw err;
28408
+ storage.down = true;
28409
+ logger.warn(`[parcels] file upload failed, sending it in the message instead: ${err?.message ?? err}`);
28410
+ }
28411
+ }
28412
+ const fileRef = {
28413
+ ...client.refFor(bytes, extOf(name)),
28414
+ contentType,
28415
+ ...named,
28416
+ data: Buffer.from(bytes).toString("base64")
28417
+ };
28418
+ return { [FILE_FIELD]: fileRef };
28419
+ }
28420
+ if (depth >= MAX_DEPTH) return value;
28421
+ if (Array.isArray(value)) {
28422
+ const out = [];
28423
+ for (const v of value) out.push(await uploadFiles(v, client, storage, void 0, depth + 1));
28424
+ return out;
28425
+ }
28426
+ if (isPlain(value)) {
28427
+ const out = {};
28428
+ for (const [k, v] of Object.entries(value)) out[k] = await uploadFiles(v, client, storage, value, depth + 1);
28429
+ return out;
28430
+ }
28431
+ return value;
28432
+ }
28433
+ function splitPayload(payload) {
28434
+ const bytes = Buffer.from(JSON.stringify(payload ?? null), "utf8");
28435
+ const total = Math.max(1, Math.ceil(bytes.byteLength / PART_BYTES));
28436
+ const id = randomUUID3();
28437
+ return Array.from({ length: total }, (_, index) => ({
28438
+ id,
28439
+ index,
28440
+ total,
28441
+ data: bytes.subarray(index * PART_BYTES, (index + 1) * PART_BYTES).toString("base64")
28442
+ }));
28443
+ }
28444
+ function toFileHandles(value, client, depth = 0) {
28445
+ const ref = fileRefOf(value);
28446
+ if (ref) return new ParcelFile(ref, client);
28447
+ if (depth >= MAX_DEPTH) return value;
28448
+ if (Array.isArray(value)) return value.map((v) => toFileHandles(v, client, depth + 1));
28449
+ if (isPlain(value)) {
28450
+ const out = {};
28451
+ for (const [k, v] of Object.entries(value)) out[k] = toFileHandles(v, client, depth + 1);
28452
+ return out;
28453
+ }
28454
+ return value;
28455
+ }
28456
+ var asError = (message, reason) => ({
28457
+ ...message,
28458
+ payload: { error: reason, parcelError: true }
28459
+ });
28460
+ var recipientOf = (message) => {
28461
+ const id = message.to?.id;
28462
+ return typeof id === "string" ? id : "";
28463
+ };
28464
+ var InOrder = class {
28465
+ /** `onIdle` runs each time the last waiting task finishes. */
28466
+ constructor(onIdle) {
28467
+ this.onIdle = onIdle;
28468
+ this.pending = 0;
28469
+ this.chain = Promise.resolve();
28470
+ }
28471
+ get busy() {
28472
+ return this.pending > 0;
28473
+ }
28474
+ run(task) {
28475
+ this.pending++;
28476
+ this.chain = this.chain.then(task).catch((err) => logger.error("[parcels] queue fault:", err)).finally(() => {
28477
+ if (--this.pending === 0) this.onIdle?.();
28478
+ });
28479
+ }
28480
+ idle() {
28481
+ return this.chain;
28482
+ }
28483
+ };
28484
+ var OutgoingParcels = class {
28485
+ constructor(client, jsonParcels, deliver, threshold = PARCEL_THRESHOLD_BYTES) {
28486
+ this.client = client;
28487
+ this.jsonParcels = jsonParcels;
28488
+ this.deliver = deliver;
28489
+ this.threshold = threshold;
28490
+ /** Only recipients with something waiting; a queue is dropped once it empties. */
28491
+ this.queues = /* @__PURE__ */ new Map();
28492
+ this.warnedNoClient = false;
28493
+ }
28494
+ /**
28495
+ * Same contract as the SDK's `send`: never throws, returns whether the message
28496
+ * went out (or, for a queued one, was accepted).
28497
+ */
28498
+ send(message) {
28499
+ if (NEVER_PARCEL.has(message.type)) return this.now(message);
28500
+ let files = false;
28501
+ try {
28502
+ files = hasBinary(message.payload);
28503
+ } catch {
28504
+ }
28505
+ if (files && !this.client) {
28506
+ if (!this.warnedNoClient) {
28507
+ this.warnedNoClient = true;
28508
+ logger.warn("[parcels] a message carries a file but SA_API_URL or the API key is missing \u2014 sent as-is");
28509
+ }
28510
+ files = false;
28511
+ }
28512
+ if (!files) {
28513
+ if (!this.jsonParcels || !this.client) return this.now(message);
28514
+ let wire;
28515
+ try {
28516
+ wire = JSON.stringify(message);
28517
+ } catch {
28518
+ return this.now(message);
28519
+ }
28520
+ if (Buffer.byteLength(wire, "utf8") < this.threshold) return this.now(message, wire);
28521
+ }
28522
+ this.queueFor(message).run(async () => {
28523
+ for (const out of await this.prepare(message, files)) this.deliver(out.message, out.wire);
28524
+ });
28525
+ return true;
28526
+ }
28527
+ /** Delivers now, unless something for the same recipient is already waiting — then behind it. */
28528
+ now(message, wire) {
28529
+ if (!this.queues.get(recipientOf(message))?.busy) return this.deliver(message, wire);
28530
+ this.queueFor(message).run(async () => {
28531
+ this.deliver(message, wire);
28532
+ });
28533
+ return true;
28534
+ }
28535
+ queueFor(message) {
28536
+ const key = recipientOf(message);
28537
+ let queue = this.queues.get(key);
28538
+ if (!queue) {
28539
+ queue = new InOrder(() => this.queues.delete(key));
28540
+ this.queues.set(key, queue);
28541
+ }
28542
+ return queue;
28543
+ }
28544
+ /** Resolves when everything queued so far has been handed to the socket. For tests and shutdown. */
28545
+ async idle() {
28546
+ await Promise.all(Array.from(this.queues.values(), (queue) => queue.idle()));
28547
+ }
28548
+ /**
28549
+ * What actually goes on the socket for this message: usually one message (as
28550
+ * is, or as a parcel receipt); several parts when storage is down and the
28551
+ * message is too large for the relay.
28552
+ */
28553
+ async prepare(message, files) {
28554
+ let current = message;
28555
+ const storage = { down: false };
28556
+ if (files) {
28557
+ const started = Date.now();
28558
+ try {
28559
+ current = { ...message, payload: await uploadFiles(message.payload, this.client, storage) };
28560
+ if (!storage.down) logger.info(`[parcels] ${message.type} ${message.id ?? ""}: file(s) uploaded (${Date.now() - started}ms)`);
28561
+ } catch (err) {
28562
+ const reason2 = err?.message ?? String(err);
28563
+ logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${reason2}`);
28564
+ return [{ message: asError(message, `Could not send a file: ${reason2}`) }];
28565
+ }
28566
+ }
28567
+ let wire;
28568
+ try {
28569
+ wire = JSON.stringify(current);
28570
+ } catch {
28571
+ return [{ message: current }];
28572
+ }
28573
+ const size = Buffer.byteLength(wire, "utf8");
28574
+ if (size < this.threshold) return [{ message: current, wire }];
28575
+ let reason = "large-JSON parcels are off";
28576
+ if (this.jsonParcels && !storage.down) {
28577
+ const started = Date.now();
28578
+ try {
28579
+ const bytes = Buffer.from(JSON.stringify(current.payload ?? null), "utf8");
28580
+ const ref = await this.client.upload(bytes);
28581
+ logger.info(
28582
+ `[parcels] ${message.type} ${message.id ?? ""} \u2192 parcel ${ref.hash.slice(0, 12)} (${ref.size} bytes, ${Date.now() - started}ms)`
28583
+ );
28584
+ return [{ message: { ...current, payload: { [PARCEL_FIELD]: ref } } }];
28585
+ } catch (err) {
28586
+ reason = err?.message ?? String(err);
28587
+ }
28588
+ } else if (storage.down) {
28589
+ reason = "storage is down";
28590
+ }
28591
+ if (size <= MAX_INLINE_BYTES) {
28592
+ if (this.jsonParcels) logger.warn(`[parcels] ${message.type} not stored (${reason}), sending inline (${size} bytes)`);
28593
+ return [{ message: current, wire }];
28594
+ }
28595
+ if (size > MAX_PARCEL_BYTES) {
28596
+ logger.error(`[parcels] ${message.type} (${size} bytes) is too large to send`);
28597
+ return [{ message: asError(message, `The message was too large to deliver (${size} bytes): ${reason}`) }];
28598
+ }
28599
+ const parts = splitPayload(current.payload);
28600
+ logger.warn(`[parcels] ${message.type} not stored (${reason}), sending in ${parts.length} parts (${size} bytes)`);
28601
+ return parts.map((part) => ({ message: { ...current, payload: { [PART_FIELD]: part } } }));
28602
+ }
28603
+ };
28604
+ var IncomingParcels = class {
28605
+ constructor(client, dispatch) {
28606
+ this.client = client;
28607
+ this.dispatch = dispatch;
28608
+ this.queue = new InOrder();
28609
+ /** Split messages still waiting for parts, by part id. */
28610
+ this.partial = /* @__PURE__ */ new Map();
28611
+ /** Split messages already rebuilt, so a second copy of their parts is ignored. */
28612
+ this.rebuilt = /* @__PURE__ */ new Set();
28613
+ }
28614
+ /**
28615
+ * `raw` is the message's text when it is at hand: a message that never
28616
+ * mentions a file placeholder is not searched for one.
28617
+ */
28618
+ receive(message, raw) {
28619
+ const part = partOf(message?.payload);
28620
+ if (part) {
28621
+ const whole = this.collect(message, part);
28622
+ if (whole) this.receive(whole);
28623
+ return;
28624
+ }
28625
+ const ref = parcelRefOf(message?.payload);
28626
+ if (!ref) {
28627
+ const withFiles = raw === void 0 || raw.includes(FILE_FIELD) ? this.withFiles(message) : message;
28628
+ if (this.queue.busy) {
28629
+ this.queue.run(async () => this.dispatch(withFiles));
28630
+ } else {
28631
+ this.dispatch(withFiles);
28632
+ }
28633
+ return;
28634
+ }
28635
+ this.queue.run(async () => this.dispatch(await this.restore(message, ref)));
28636
+ }
28637
+ idle() {
28638
+ return this.queue.idle();
28639
+ }
28640
+ /**
28641
+ * Holds one part; once all of them are here, the original message (envelope
28642
+ * from the last part, payload rebuilt). Null while parts are still missing.
28643
+ */
28644
+ collect(message, part) {
28645
+ if (this.rebuilt.has(part.id)) return null;
28646
+ let entry = this.partial.get(part.id);
28647
+ if (!entry) {
28648
+ const timer = setTimeout(() => {
28649
+ this.partial.delete(part.id);
28650
+ logger.warn(`[parcels] ${message.type}: gave up waiting for the rest of a split message (${part.id})`);
28651
+ }, PART_WAIT_MS).unref();
28652
+ entry = { parts: new Array(part.total), got: 0, timer };
28653
+ this.partial.set(part.id, entry);
28654
+ }
28655
+ if (entry.parts.length !== part.total || entry.parts[part.index] !== void 0) return null;
28656
+ entry.parts[part.index] = part.data;
28657
+ if (++entry.got < part.total) return null;
28658
+ clearTimeout(entry.timer);
28659
+ this.partial.delete(part.id);
28660
+ this.rebuilt.add(part.id);
28661
+ setTimeout(() => this.rebuilt.delete(part.id), PART_WAIT_MS).unref();
28662
+ try {
28663
+ const bytes = Buffer.concat(entry.parts.map((data) => Buffer.from(data, "base64")));
28664
+ logger.info(`[parcels] ${message.type} ${message.id ?? ""} rebuilt from ${part.total} parts (${bytes.byteLength} bytes)`);
28665
+ return { ...message, payload: JSON.parse(bytes.toString("utf8")) };
28666
+ } catch (err) {
28667
+ const reason = err?.message ?? String(err);
28668
+ logger.error(`[parcels] could not rebuild a split ${message.type}: ${reason}`);
28669
+ return asError(message, `Could not rebuild the message: ${reason}`);
28670
+ }
28671
+ }
28672
+ withFiles(message) {
28673
+ try {
28674
+ return hasFileRef(message.payload) ? { ...message, payload: toFileHandles(message.payload, this.client) } : message;
28675
+ } catch {
28676
+ return message;
28677
+ }
28678
+ }
28679
+ async restore(message, ref) {
28680
+ if (!this.client) {
28681
+ logger.error(`[parcels] received a parcel for ${message.type} but SA_API_URL or the API key is not set`);
28682
+ return asError(message, "This engine cannot download parcels: SA_API_URL or the API key is not set.");
28683
+ }
28684
+ const started = Date.now();
28685
+ try {
28686
+ const bytes = await this.client.download(ref);
28687
+ const payload = JSON.parse(Buffer.from(bytes).toString("utf8"));
28688
+ logger.info(
28689
+ `[parcels] ${message.type} ${message.id ?? ""} \u2190 parcel ${ref.hash.slice(0, 12)} (${ref.size} bytes, ${Date.now() - started}ms)`
28690
+ );
28691
+ return this.withFiles({ ...message, payload });
28692
+ } catch (err) {
28693
+ const reason = err?.message ?? String(err);
28694
+ logger.error(`[parcels] download failed for ${message.type} (${ref.hash.slice(0, 12)}): ${reason}`);
28695
+ return asError(message, `Could not download the message content: ${reason}`);
28696
+ }
28697
+ }
28698
+ };
28699
+ var parcelsFlagFromEnv = () => !/^(off|false|0|no)$/i.test((process.env.SA_PARCELS ?? "").trim());
28700
+
28246
28701
  // src/userResponse/scripts/fmt.ts
28247
28702
  var DASH = "\u2014";
28248
28703
  var DEFAULT_FMT_CONFIG = { locale: "en-IN", currency: "INR" };
@@ -29594,6 +30049,18 @@ var SuperatomSDK = class {
29594
30049
  this.type = config2.type || "data-agent";
29595
30050
  this.bundleDir = config2.bundleDir;
29596
30051
  this.url = config2.url || process.env.SA_WEBSOCKET_URL || DEFAULT_WS_URL;
30052
+ const apiUrl = config2.apiUrl || process.env.SA_API_URL;
30053
+ this.parcelClient = apiUrl && this.apiKey ? new ParcelClient({ apiUrl, projectId: this.projectId, apiKey: this.apiKey }) : null;
30054
+ const jsonParcels = config2.parcels ?? parcelsFlagFromEnv();
30055
+ if (jsonParcels && !this.parcelClient) {
30056
+ logger.warn("[parcels] parcels are on but SA_API_URL or the API key is missing \u2014 large messages go inline");
30057
+ }
30058
+ this.outgoing = new OutgoingParcels(
30059
+ this.parcelClient,
30060
+ jsonParcels,
30061
+ (message, wire) => this.deliver(message, wire)
30062
+ );
30063
+ this.incoming = new IncomingParcels(this.parcelClient, (parsed) => this.dispatchMessage(parsed));
29597
30064
  this.anthropicApiKey = config2.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || "";
29598
30065
  this.groqApiKey = config2.GROQ_API_KEY || process.env.GROQ_API_KEY || "";
29599
30066
  this.geminiApiKey = config2.GEMINI_API_KEY || process.env.GEMINI_API_KEY || "";
@@ -29745,11 +30212,24 @@ var SuperatomSDK = class {
29745
30212
  });
29746
30213
  }
29747
30214
  /**
29748
- * Handle incoming WebSocket messages
30215
+ * Handle incoming WebSocket messages.
30216
+ *
30217
+ * Parsed here, then handed to the parcel layer, which restores a parcel's
30218
+ * payload (and holds later messages back until it has) before routing.
29749
30219
  */
29750
30220
  handleMessage(data) {
30221
+ let parsed;
30222
+ try {
30223
+ parsed = JSON.parse(data);
30224
+ } catch (error) {
30225
+ logger.error("Failed to parse incoming message:", error);
30226
+ return;
30227
+ }
30228
+ this.incoming.receive(parsed, data);
30229
+ }
30230
+ /** Routes one parsed message — everything `handleMessage` did after parsing. */
30231
+ dispatchMessage(parsed) {
29751
30232
  try {
29752
- const parsed = JSON.parse(data);
29753
30233
  const message = IncomingMessageSchema.parse(parsed);
29754
30234
  logger.debug("Received message:", message.type);
29755
30235
  switch (message.type) {
@@ -29939,8 +30419,19 @@ var SuperatomSDK = class {
29939
30419
  * Send a message to the Superatom service.
29940
30420
  * Returns true if the message was sent, false if the WebSocket is not connected.
29941
30421
  * Does NOT throw on closed connections — callers can check the return value if needed.
30422
+ *
30423
+ * Large payloads and files go as parcels (see ./parcels): a file is any
30424
+ * Buffer / Uint8Array / ArrayBuffer inside the payload, with an optional
30425
+ * `name` and `contentType` beside it — e.g. `payload: { file: { name, bytes } }`.
30426
+ * Such a message goes out once its upload is done (true = accepted), and later
30427
+ * messages keep their order. A received file arrives as a `ParcelFile` handle
30428
+ * whose `download()` fetches the bytes.
29942
30429
  */
29943
30430
  send(message) {
30431
+ return this.outgoing.send(message);
30432
+ }
30433
+ /** Puts one message on the socket, or in the reconnect outbox. `wire` is its JSON, when already made. */
30434
+ deliver(message, wire) {
29944
30435
  if (!this.ws || !this.connected) {
29945
30436
  logger.warn("WebSocket is not connected, queueing message for reconnect:", message.type);
29946
30437
  this.queuePendingMessage(message);
@@ -29952,7 +30443,7 @@ var SuperatomSDK = class {
29952
30443
  return false;
29953
30444
  }
29954
30445
  try {
29955
- const payload = JSON.stringify(message);
30446
+ const payload = wire ?? JSON.stringify(message);
29956
30447
  this.ws.send(payload);
29957
30448
  return true;
29958
30449
  } catch (error) {
@@ -30205,7 +30696,7 @@ var SuperatomSDK = class {
30205
30696
  waiters.push(finish);
30206
30697
  this.registryWaiters.set(resType, waiters);
30207
30698
  const sent = this.send({
30208
- id: `${reqType.toLowerCase()}_${randomUUID3()}`,
30699
+ id: `${reqType.toLowerCase()}_${randomUUID4()}`,
30209
30700
  type: reqType,
30210
30701
  from: { type: this.type },
30211
30702
  to: { type: "runtime" },
@@ -30295,6 +30786,8 @@ export {
30295
30786
  MainAgent,
30296
30787
  MissingRequestContextError,
30297
30788
  NUMERIC_KEYS,
30789
+ PARCEL_THRESHOLD_BYTES,
30790
+ ParcelFile,
30298
30791
  STORAGE_CONFIG,
30299
30792
  ScriptMatcher,
30300
30793
  ScriptStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superatomai/sdk-node",
3
- "version": "0.0.93-dsp",
3
+ "version": "0.0.95-dsp",
4
4
  "description": "Node.js TypeScript SDK for Superatom",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -16,14 +16,6 @@
16
16
  "dist",
17
17
  "!dist/**/*.map"
18
18
  ],
19
- "scripts": {
20
- "build": "tsc --noEmit && tsup",
21
- "dev": "tsup --watch",
22
- "test": "tsx test-sdk.ts",
23
- "test:unit": "node --import tsx --test tests/unit/*.test.ts",
24
- "prepublishOnly": "pnpm run build",
25
- "type-check": "tsc --noEmit"
26
- },
27
19
  "keywords": [
28
20
  "superatom",
29
21
  "sdk",
@@ -53,5 +45,12 @@
53
45
  },
54
46
  "optionalDependencies": {
55
47
  "@openai/codex-sdk": "0.156.1"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc --noEmit && tsup",
51
+ "dev": "tsup --watch",
52
+ "test": "tsx test-sdk.ts",
53
+ "test:unit": "node --import tsx --test tests/unit/*.test.ts",
54
+ "type-check": "tsc --noEmit"
56
55
  }
57
- }
56
+ }