@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.
package/README.md CHANGED
@@ -456,6 +456,16 @@ Defines domain ontology: classes (with typed properties), and relationships (wit
456
456
  ### `environment.recordObject(options)`
457
457
  Creates or updates a class instance with fields and relationships. Returns `{ path, id, created }`.
458
458
 
459
+ ### `environment.recordObjects(records, options?)`
460
+ Batch upsert for many instances. The SDK sends **chunks** (default **100** rows per HTTP `POST` to `/records/batch`) with **retries** on transient failures, so large arrays do not time out as a single oversized request.
461
+
462
+ Optional **`options`**:
463
+ - **`batchSize`** — max rows per request (default 100).
464
+ - **`concurrency`** — how many chunk requests may run in parallel (default 1, max 16); can reduce wall time when the server can overlap work.
465
+ - **`onChunkComplete`** — async-friendly hook after each chunk for progress UIs; the returned array is always ordered like `records`.
466
+
467
+ **Sync batch vs queued import:** use **`recordObjects`** when you need **synchronous** commits and/or per-chunk feedback. Use **`enqueueRecordImport`** + **`getRecordImport` / `getRecordImportSummary`** for **background** ingestion with aggregate counters when admission latency matters more than immediate row-by-row completion.
468
+
459
469
  ### `environment.getRelationships(modelPath)`
460
470
  Returns relationship definitions for a given class.
461
471
 
@@ -1,4 +1,4 @@
1
- import { P as Prompt, c as Environment, b as SessionHeapSnapshot, aZ as ManifestContent, ax as RecordObjectOptions, T as ToolWithHandler, G as Granular, C as ConnectOptions, y as CreateEnvironmentData, i as GranularOptions } from './client-BQw_gUK3.mjs';
1
+ import { P as Prompt, c as Environment, b as SessionHeapSnapshot, a$ as ManifestContent, ax as RecordObjectOptions, T as ToolWithHandler, G as Granular, C as ConnectOptions, y as CreateEnvironmentData, i as GranularOptions } from './client-DWYdWpS-.mjs';
2
2
  import { GeneratedJobCodeIssue, HarnessControllerBudgets } from './agent-harness.mjs';
3
3
  import '@automerge/automerge';
4
4
  import '@automerge/automerge/slim';
@@ -1,4 +1,4 @@
1
- import { P as Prompt, c as Environment, b as SessionHeapSnapshot, aZ as ManifestContent, ax as RecordObjectOptions, T as ToolWithHandler, G as Granular, C as ConnectOptions, y as CreateEnvironmentData, i as GranularOptions } from './client-BQw_gUK3.js';
1
+ import { P as Prompt, c as Environment, b as SessionHeapSnapshot, a$ as ManifestContent, ax as RecordObjectOptions, T as ToolWithHandler, G as Granular, C as ConnectOptions, y as CreateEnvironmentData, i as GranularOptions } from './client-DWYdWpS-.js';
2
2
  import { GeneratedJobCodeIssue, HarnessControllerBudgets } from './agent-harness.js';
3
3
  import '@automerge/automerge';
4
4
  import '@automerge/automerge/slim';
@@ -11256,6 +11256,28 @@ var STANDARD_MODULES_OPERATIONS = [
11256
11256
  var BUILTIN_MODULES = {
11257
11257
  "standard_modules": STANDARD_MODULES_OPERATIONS
11258
11258
  };
11259
+ var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
11260
+ var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
11261
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
11262
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
11263
+ function planRecordObjectsChunks(records, batchSize) {
11264
+ const total = records.length;
11265
+ const size = Math.max(1, Math.min(batchSize, total));
11266
+ const chunkCount = Math.ceil(total / size);
11267
+ const plans = [];
11268
+ for (let offset = 0, chunkIndex = 0; offset < total; offset += size, chunkIndex += 1) {
11269
+ const slice = records.slice(offset, offset + size);
11270
+ plans.push({ chunkIndex, chunkCount, offset, slice });
11271
+ }
11272
+ return plans;
11273
+ }
11274
+ function sleep(ms) {
11275
+ return new Promise((resolve) => setTimeout(resolve, ms));
11276
+ }
11277
+ function isRetryableRecordObjectsError(error) {
11278
+ const message = error instanceof Error ? error.message : String(error);
11279
+ return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(message);
11280
+ }
11259
11281
  function computeEffectKey2(effect) {
11260
11282
  const attachedClass = effect.className?.trim();
11261
11283
  if (!attachedClass) {
@@ -12213,24 +12235,101 @@ var Environment = class extends Session {
12213
12235
  /**
12214
12236
  * Batch version of `recordObject()`.
12215
12237
  *
12216
- * Sends several upserts through the control-plane batch endpoint so the
12217
- * server can collapse the graph mutations into far fewer round trips.
12238
+ * Sends rows through the control-plane **`/records/batch`** endpoint in **chunks** (default
12239
+ * **100** records per HTTP request) so individual requests stay bounded and gateway timeouts are
12240
+ * unlikely. Each chunk is retried on transient network / worker errors.
12241
+ *
12242
+ * Use the optional second argument to:
12243
+ * - **`batchSize`** — rows per POST (smaller = more progress events; larger = fewer round trips).
12244
+ * - **`concurrency`** — run up to N chunk POSTs in parallel (capped at 16) when you want lower wall time.
12245
+ * - **`onChunkComplete`** — hook for UIs after each chunk succeeds (row order in the returned array
12246
+ * always matches `records`; chunk **completion** order may differ when `concurrency > 1`).
12247
+ *
12248
+ * For **asynchronous** ingestion with worker-side batching and aggregate counters (`queued`,
12249
+ * `completed`, …), use **`enqueueRecordImport`** and poll **`getRecordImport`** /
12250
+ * **`getRecordImportSummary`** — best for very large fire-and-forget loads when immediate
12251
+ * synchronous commit of every row is not required.
12218
12252
  */
12219
- async recordObjects(records) {
12253
+ async recordObjects(records, options) {
12220
12254
  if (!Array.isArray(records) || records.length === 0) {
12221
12255
  return [];
12222
12256
  }
12223
- const response = await this.controlPlaneRequest(
12224
- `/control/environments/${this.environmentId}/records/batch`,
12225
- {
12226
- method: "POST",
12227
- body: JSON.stringify({ records })
12228
- }
12257
+ const batchSize = Math.max(
12258
+ 1,
12259
+ Math.min(
12260
+ records.length,
12261
+ options?.batchSize ?? DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE
12262
+ )
12229
12263
  );
12230
- return Array.isArray(response.items) ? response.items : [];
12264
+ const concurrency = Math.min(
12265
+ MAX_RECORD_OBJECTS_CONCURRENCY,
12266
+ Math.max(1, options?.concurrency ?? 1)
12267
+ );
12268
+ const plans = planRecordObjectsChunks(records, batchSize);
12269
+ const total = records.length;
12270
+ const results = new Array(total);
12271
+ const onChunk = options?.onChunkComplete;
12272
+ for (let waveStart = 0; waveStart < plans.length; waveStart += concurrency) {
12273
+ const wave = plans.slice(waveStart, waveStart + concurrency);
12274
+ await Promise.all(
12275
+ wave.map(async (plan) => {
12276
+ const { items, durationMs } = await this.executeRecordObjectsChunk(plan.slice);
12277
+ if (items.length !== plan.slice.length) {
12278
+ throw new Error(
12279
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
12280
+ );
12281
+ }
12282
+ for (let index = 0; index < items.length; index += 1) {
12283
+ results[plan.offset + index] = items[index];
12284
+ }
12285
+ if (onChunk) {
12286
+ const info = {
12287
+ chunkIndex: plan.chunkIndex,
12288
+ totalChunks: plan.chunkCount,
12289
+ offset: plan.offset,
12290
+ recordCount: plan.slice.length,
12291
+ durationMs,
12292
+ results: items
12293
+ };
12294
+ await onChunk(info);
12295
+ }
12296
+ })
12297
+ );
12298
+ }
12299
+ return results;
12300
+ }
12301
+ async executeRecordObjectsChunk(chunk) {
12302
+ const wallStart = Date.now();
12303
+ let lastError;
12304
+ for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
12305
+ try {
12306
+ const response = await this.controlPlaneRequest(
12307
+ `/control/environments/${this.environmentId}/records/batch`,
12308
+ {
12309
+ method: "POST",
12310
+ body: JSON.stringify({ records: chunk })
12311
+ }
12312
+ );
12313
+ const items = Array.isArray(response.items) ? response.items : [];
12314
+ return { items, durationMs: Date.now() - wallStart };
12315
+ } catch (error) {
12316
+ lastError = error;
12317
+ if (!isRetryableRecordObjectsError(error) || attempt === DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT) {
12318
+ throw error;
12319
+ }
12320
+ await sleep(DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS * attempt);
12321
+ }
12322
+ }
12323
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
12231
12324
  }
12232
12325
  /**
12233
- * Queue a background record import for this environment.
12326
+ * Queue a background record import for this environment (async worker pipeline).
12327
+ *
12328
+ * **vs `recordObjects`:** this path accepts the full payload in one request, returns an
12329
+ * **`importId`**, and processes rows in the background — use **`getRecordImport`** /
12330
+ * **`getRecordImportSummary`** for progress. Choose it for large bulk loads where you do not
12331
+ * need every row committed before the HTTP call returns. Use **`recordObjects`** when you need
12332
+ * synchronous upserts and fine-grained chunk progress via **`onChunkComplete`**.
12234
12333
  */
12235
12334
  async enqueueRecordImport(records, options = {}) {
12236
12335
  return this.controlPlaneRequest(
@@ -14589,7 +14688,7 @@ ${modelOutputInstruction()}` },
14589
14688
  if (!response.ok) {
14590
14689
  const errorText = await response.text();
14591
14690
  if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
14592
- await sleep(500 * attempt);
14691
+ await sleep2(500 * attempt);
14593
14692
  continue;
14594
14693
  }
14595
14694
  throw new Error(`OpenAI chat generation failed: ${response.status} ${errorText}`);
@@ -14600,7 +14699,7 @@ ${modelOutputInstruction()}` },
14600
14699
  const parsed = extractJsonObject(text);
14601
14700
  if (!parsed) {
14602
14701
  if (attempt < 3) {
14603
- await sleep(300 * attempt);
14702
+ await sleep2(300 * attempt);
14604
14703
  continue;
14605
14704
  }
14606
14705
  throw new Error(`Model output was not valid JSON:
@@ -14614,7 +14713,7 @@ ${text}`);
14614
14713
  } catch (error) {
14615
14714
  lastError = error instanceof Error ? error : new Error(String(error));
14616
14715
  if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(lastError.message)) {
14617
- await sleep(500 * attempt);
14716
+ await sleep2(500 * attempt);
14618
14717
  continue;
14619
14718
  }
14620
14719
  throw lastError;
@@ -14626,7 +14725,7 @@ ${text}`);
14626
14725
  async function ensureDir(dir) {
14627
14726
  await promises.mkdir(dir, { recursive: true });
14628
14727
  }
14629
- async function sleep(ms) {
14728
+ async function sleep2(ms) {
14630
14729
  await new Promise((resolve) => setTimeout(resolve, ms));
14631
14730
  }
14632
14731
  async function withTimeout(promise, ms, label) {
@@ -15069,7 +15168,7 @@ function createAgentEvalHarness(options) {
15069
15168
  stderr: [...pending.stderr, ...resumed.stderr]
15070
15169
  };
15071
15170
  }
15072
- await sleep(350);
15171
+ await sleep2(350);
15073
15172
  const liveDoc = cloneJson(pending.conversation.environment.document);
15074
15173
  const presentation = resolveJobPresentation({
15075
15174
  jobId: pending.job.id,
@@ -15238,7 +15337,7 @@ function createAgentEvalHarness(options) {
15238
15337
  if (outcome.kind !== "completed") {
15239
15338
  throw new Error("Unexpected non-completed outcome after prompt handling");
15240
15339
  }
15241
- await sleep(350);
15340
+ await sleep2(350);
15242
15341
  const settledLiveDoc = cloneJson(conversation.environment.document);
15243
15342
  const sessionHeap = normalizeHeapSnapshot2(asRecord4(settledLiveDoc?.heap));
15244
15343
  const presentation = resolveJobPresentation({