@granular-software/sdk 0.4.19 → 0.4.20

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.
@@ -11231,6 +11231,28 @@ var STANDARD_MODULES_OPERATIONS = [
11231
11231
  var BUILTIN_MODULES = {
11232
11232
  "standard_modules": STANDARD_MODULES_OPERATIONS
11233
11233
  };
11234
+ var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
11235
+ var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
11236
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
11237
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
11238
+ function planRecordObjectsChunks(records, batchSize) {
11239
+ const total = records.length;
11240
+ const size = Math.max(1, Math.min(batchSize, total));
11241
+ const chunkCount = Math.ceil(total / size);
11242
+ const plans = [];
11243
+ for (let offset = 0, chunkIndex = 0; offset < total; offset += size, chunkIndex += 1) {
11244
+ const slice = records.slice(offset, offset + size);
11245
+ plans.push({ chunkIndex, chunkCount, offset, slice });
11246
+ }
11247
+ return plans;
11248
+ }
11249
+ function sleep(ms) {
11250
+ return new Promise((resolve) => setTimeout(resolve, ms));
11251
+ }
11252
+ function isRetryableRecordObjectsError(error) {
11253
+ const message = error instanceof Error ? error.message : String(error);
11254
+ return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(message);
11255
+ }
11234
11256
  function computeEffectKey2(effect) {
11235
11257
  const attachedClass = effect.className?.trim();
11236
11258
  if (!attachedClass) {
@@ -12188,24 +12210,101 @@ var Environment = class extends Session {
12188
12210
  /**
12189
12211
  * Batch version of `recordObject()`.
12190
12212
  *
12191
- * Sends several upserts through the control-plane batch endpoint so the
12192
- * server can collapse the graph mutations into far fewer round trips.
12213
+ * Sends rows through the control-plane **`/records/batch`** endpoint in **chunks** (default
12214
+ * **100** records per HTTP request) so individual requests stay bounded and gateway timeouts are
12215
+ * unlikely. Each chunk is retried on transient network / worker errors.
12216
+ *
12217
+ * Use the optional second argument to:
12218
+ * - **`batchSize`** — rows per POST (smaller = more progress events; larger = fewer round trips).
12219
+ * - **`concurrency`** — run up to N chunk POSTs in parallel (capped at 16) when you want lower wall time.
12220
+ * - **`onChunkComplete`** — hook for UIs after each chunk succeeds (row order in the returned array
12221
+ * always matches `records`; chunk **completion** order may differ when `concurrency > 1`).
12222
+ *
12223
+ * For **asynchronous** ingestion with worker-side batching and aggregate counters (`queued`,
12224
+ * `completed`, …), use **`enqueueRecordImport`** and poll **`getRecordImport`** /
12225
+ * **`getRecordImportSummary`** — best for very large fire-and-forget loads when immediate
12226
+ * synchronous commit of every row is not required.
12193
12227
  */
12194
- async recordObjects(records) {
12228
+ async recordObjects(records, options) {
12195
12229
  if (!Array.isArray(records) || records.length === 0) {
12196
12230
  return [];
12197
12231
  }
12198
- const response = await this.controlPlaneRequest(
12199
- `/control/environments/${this.environmentId}/records/batch`,
12200
- {
12201
- method: "POST",
12202
- body: JSON.stringify({ records })
12203
- }
12232
+ const batchSize = Math.max(
12233
+ 1,
12234
+ Math.min(
12235
+ records.length,
12236
+ options?.batchSize ?? DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE
12237
+ )
12204
12238
  );
12205
- return Array.isArray(response.items) ? response.items : [];
12239
+ const concurrency = Math.min(
12240
+ MAX_RECORD_OBJECTS_CONCURRENCY,
12241
+ Math.max(1, options?.concurrency ?? 1)
12242
+ );
12243
+ const plans = planRecordObjectsChunks(records, batchSize);
12244
+ const total = records.length;
12245
+ const results = new Array(total);
12246
+ const onChunk = options?.onChunkComplete;
12247
+ for (let waveStart = 0; waveStart < plans.length; waveStart += concurrency) {
12248
+ const wave = plans.slice(waveStart, waveStart + concurrency);
12249
+ await Promise.all(
12250
+ wave.map(async (plan) => {
12251
+ const { items, durationMs } = await this.executeRecordObjectsChunk(plan.slice);
12252
+ if (items.length !== plan.slice.length) {
12253
+ throw new Error(
12254
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
12255
+ );
12256
+ }
12257
+ for (let index = 0; index < items.length; index += 1) {
12258
+ results[plan.offset + index] = items[index];
12259
+ }
12260
+ if (onChunk) {
12261
+ const info = {
12262
+ chunkIndex: plan.chunkIndex,
12263
+ totalChunks: plan.chunkCount,
12264
+ offset: plan.offset,
12265
+ recordCount: plan.slice.length,
12266
+ durationMs,
12267
+ results: items
12268
+ };
12269
+ await onChunk(info);
12270
+ }
12271
+ })
12272
+ );
12273
+ }
12274
+ return results;
12275
+ }
12276
+ async executeRecordObjectsChunk(chunk) {
12277
+ const wallStart = Date.now();
12278
+ let lastError;
12279
+ for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
12280
+ try {
12281
+ const response = await this.controlPlaneRequest(
12282
+ `/control/environments/${this.environmentId}/records/batch`,
12283
+ {
12284
+ method: "POST",
12285
+ body: JSON.stringify({ records: chunk })
12286
+ }
12287
+ );
12288
+ const items = Array.isArray(response.items) ? response.items : [];
12289
+ return { items, durationMs: Date.now() - wallStart };
12290
+ } catch (error) {
12291
+ lastError = error;
12292
+ if (!isRetryableRecordObjectsError(error) || attempt === DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT) {
12293
+ throw error;
12294
+ }
12295
+ await sleep(DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS * attempt);
12296
+ }
12297
+ }
12298
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
12206
12299
  }
12207
12300
  /**
12208
- * Queue a background record import for this environment.
12301
+ * Queue a background record import for this environment (async worker pipeline).
12302
+ *
12303
+ * **vs `recordObjects`:** this path accepts the full payload in one request, returns an
12304
+ * **`importId`**, and processes rows in the background — use **`getRecordImport`** /
12305
+ * **`getRecordImportSummary`** for progress. Choose it for large bulk loads where you do not
12306
+ * need every row committed before the HTTP call returns. Use **`recordObjects`** when you need
12307
+ * synchronous upserts and fine-grained chunk progress via **`onChunkComplete`**.
12209
12308
  */
12210
12309
  async enqueueRecordImport(records, options = {}) {
12211
12310
  return this.controlPlaneRequest(
@@ -14564,7 +14663,7 @@ ${modelOutputInstruction()}` },
14564
14663
  if (!response.ok) {
14565
14664
  const errorText = await response.text();
14566
14665
  if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
14567
- await sleep(500 * attempt);
14666
+ await sleep2(500 * attempt);
14568
14667
  continue;
14569
14668
  }
14570
14669
  throw new Error(`OpenAI chat generation failed: ${response.status} ${errorText}`);
@@ -14575,7 +14674,7 @@ ${modelOutputInstruction()}` },
14575
14674
  const parsed = extractJsonObject(text);
14576
14675
  if (!parsed) {
14577
14676
  if (attempt < 3) {
14578
- await sleep(300 * attempt);
14677
+ await sleep2(300 * attempt);
14579
14678
  continue;
14580
14679
  }
14581
14680
  throw new Error(`Model output was not valid JSON:
@@ -14589,7 +14688,7 @@ ${text}`);
14589
14688
  } catch (error) {
14590
14689
  lastError = error instanceof Error ? error : new Error(String(error));
14591
14690
  if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(lastError.message)) {
14592
- await sleep(500 * attempt);
14691
+ await sleep2(500 * attempt);
14593
14692
  continue;
14594
14693
  }
14595
14694
  throw lastError;
@@ -14601,7 +14700,7 @@ ${text}`);
14601
14700
  async function ensureDir(dir) {
14602
14701
  await mkdir(dir, { recursive: true });
14603
14702
  }
14604
- async function sleep(ms) {
14703
+ async function sleep2(ms) {
14605
14704
  await new Promise((resolve) => setTimeout(resolve, ms));
14606
14705
  }
14607
14706
  async function withTimeout(promise, ms, label) {
@@ -15044,7 +15143,7 @@ function createAgentEvalHarness(options) {
15044
15143
  stderr: [...pending.stderr, ...resumed.stderr]
15045
15144
  };
15046
15145
  }
15047
- await sleep(350);
15146
+ await sleep2(350);
15048
15147
  const liveDoc = cloneJson(pending.conversation.environment.document);
15049
15148
  const presentation = resolveJobPresentation({
15050
15149
  jobId: pending.job.id,
@@ -15213,7 +15312,7 @@ function createAgentEvalHarness(options) {
15213
15312
  if (outcome.kind !== "completed") {
15214
15313
  throw new Error("Unexpected non-completed outcome after prompt handling");
15215
15314
  }
15216
- await sleep(350);
15315
+ await sleep2(350);
15217
15316
  const settledLiveDoc = cloneJson(conversation.environment.document);
15218
15317
  const sessionHeap = normalizeHeapSnapshot2(asRecord4(settledLiveDoc?.heap));
15219
15318
  const presentation = resolveJobPresentation({