@granular-software/sdk 0.4.19 → 0.4.21

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
@@ -4660,10 +4660,15 @@ var Session = class {
4660
4660
  * ```typescript
4661
4661
  * import { Author, Book, global_search } from './sandbox-tools';
4662
4662
  *
4663
- * const authors = await Author.list({ limit: 10, saveAs: 'recent_authors' });
4663
+ * const totalAuthors = await Author.count();
4664
+ * const firstAuthorsPage = await Author.page({ page: 1, perPage: 10, saveAs: 'recent_authors' });
4665
+ * const authors = firstAuthorsPage.items;
4664
4666
  * const tolkien = await Author.get({ path: 'author_tolkien' });
4665
4667
  * const bio = await tolkien.get_bio({ detailed: true });
4666
4668
  * const books = await tolkien.get_books();
4669
+ * for await (const author of Author.iterate({ perPage: 100, maxItems: 500 })) {
4670
+ * console.log(author.id);
4671
+ * }
4667
4672
  * ```
4668
4673
  *
4669
4674
  * Effect calls (instance methods, static methods, global functions) trigger
@@ -11221,14 +11226,49 @@ var STANDARD_MODULES_OPERATIONS = [
11221
11226
  { create: "class", extends: "entity", has: {} },
11222
11227
  { create: "user", extends: "entity", has: { email: { value: void 0 }, firstName: { value: void 0 }, lastName: { value: void 0 } } },
11223
11228
  { create: "company", extends: "entity", has: { name: { value: void 0 }, website: { value: void 0 } } },
11224
- { create: "string", has: { value: { value: void 0 } } },
11225
- { create: "number", has: { value: { value: 0 } } },
11226
- { create: "boolean", has: { value: { value: false } } },
11229
+ { create: "string", has: {} },
11230
+ { create: "number", has: {} },
11231
+ { create: "boolean", has: {} },
11227
11232
  { create: "tool_parameter", has: { name: { value: void 0 }, type: { value: "string" }, description: { value: void 0 }, required: { value: false } } }
11228
11233
  ];
11229
11234
  var BUILTIN_MODULES = {
11230
11235
  "standard_modules": STANDARD_MODULES_OPERATIONS
11231
11236
  };
11237
+ var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
11238
+ var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
11239
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
11240
+ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
11241
+ var LOCAL_CONTROL_REQUEST_RETRY_COUNT = 4;
11242
+ var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
11243
+ function planRecordObjectsChunks(records, batchSize) {
11244
+ const total = records.length;
11245
+ const size = Math.max(1, Math.min(batchSize, total));
11246
+ const chunkCount = Math.ceil(total / size);
11247
+ const plans = [];
11248
+ for (let offset = 0, chunkIndex = 0; offset < total; offset += size, chunkIndex += 1) {
11249
+ const slice = records.slice(offset, offset + size);
11250
+ plans.push({ chunkIndex, chunkCount, offset, slice });
11251
+ }
11252
+ return plans;
11253
+ }
11254
+ function sleep(ms) {
11255
+ return new Promise((resolve) => setTimeout(resolve, ms));
11256
+ }
11257
+ function isLocalControlUrl(url) {
11258
+ try {
11259
+ const parsed = new URL(url);
11260
+ return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1";
11261
+ } catch {
11262
+ return false;
11263
+ }
11264
+ }
11265
+ function isRetryableLocalWorkerRestart(status, body, url) {
11266
+ return isLocalControlUrl(url) && (status === 503 && body.includes("Your worker restarted mid-request") || status === 500 && body.includes("Network connection lost"));
11267
+ }
11268
+ function isRetryableRecordObjectsError(error) {
11269
+ const message = error instanceof Error ? error.message : String(error);
11270
+ return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(message);
11271
+ }
11232
11272
  function computeEffectKey2(effect) {
11233
11273
  const attachedClass = effect.className?.trim();
11234
11274
  if (!attachedClass) {
@@ -12186,24 +12226,101 @@ var Environment = class extends Session {
12186
12226
  /**
12187
12227
  * Batch version of `recordObject()`.
12188
12228
  *
12189
- * Sends several upserts through the control-plane batch endpoint so the
12190
- * server can collapse the graph mutations into far fewer round trips.
12229
+ * Sends rows through the control-plane **`/records/batch`** endpoint in **chunks** (default
12230
+ * **100** records per HTTP request) so individual requests stay bounded and gateway timeouts are
12231
+ * unlikely. Each chunk is retried on transient network / worker errors.
12232
+ *
12233
+ * Use the optional second argument to:
12234
+ * - **`batchSize`** — rows per POST (smaller = more progress events; larger = fewer round trips).
12235
+ * - **`concurrency`** — run up to N chunk POSTs in parallel (capped at 16) when you want lower wall time.
12236
+ * - **`onChunkComplete`** — hook for UIs after each chunk succeeds (row order in the returned array
12237
+ * always matches `records`; chunk **completion** order may differ when `concurrency > 1`).
12238
+ *
12239
+ * For **asynchronous** ingestion with worker-side batching and aggregate counters (`queued`,
12240
+ * `completed`, …), use **`enqueueRecordImport`** and poll **`getRecordImport`** /
12241
+ * **`getRecordImportSummary`** — best for very large fire-and-forget loads when immediate
12242
+ * synchronous commit of every row is not required.
12191
12243
  */
12192
- async recordObjects(records) {
12244
+ async recordObjects(records, options) {
12193
12245
  if (!Array.isArray(records) || records.length === 0) {
12194
12246
  return [];
12195
12247
  }
12196
- const response = await this.controlPlaneRequest(
12197
- `/control/environments/${this.environmentId}/records/batch`,
12198
- {
12199
- method: "POST",
12200
- body: JSON.stringify({ records })
12201
- }
12248
+ const batchSize = Math.max(
12249
+ 1,
12250
+ Math.min(
12251
+ records.length,
12252
+ options?.batchSize ?? DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE
12253
+ )
12202
12254
  );
12203
- return Array.isArray(response.items) ? response.items : [];
12255
+ const concurrency = Math.min(
12256
+ MAX_RECORD_OBJECTS_CONCURRENCY,
12257
+ Math.max(1, options?.concurrency ?? 1)
12258
+ );
12259
+ const plans = planRecordObjectsChunks(records, batchSize);
12260
+ const total = records.length;
12261
+ const results = new Array(total);
12262
+ const onChunk = options?.onChunkComplete;
12263
+ for (let waveStart = 0; waveStart < plans.length; waveStart += concurrency) {
12264
+ const wave = plans.slice(waveStart, waveStart + concurrency);
12265
+ await Promise.all(
12266
+ wave.map(async (plan) => {
12267
+ const { items, durationMs } = await this.executeRecordObjectsChunk(plan.slice);
12268
+ if (items.length !== plan.slice.length) {
12269
+ throw new Error(
12270
+ `recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
12271
+ );
12272
+ }
12273
+ for (let index = 0; index < items.length; index += 1) {
12274
+ results[plan.offset + index] = items[index];
12275
+ }
12276
+ if (onChunk) {
12277
+ const info = {
12278
+ chunkIndex: plan.chunkIndex,
12279
+ totalChunks: plan.chunkCount,
12280
+ offset: plan.offset,
12281
+ recordCount: plan.slice.length,
12282
+ durationMs,
12283
+ results: items
12284
+ };
12285
+ await onChunk(info);
12286
+ }
12287
+ })
12288
+ );
12289
+ }
12290
+ return results;
12291
+ }
12292
+ async executeRecordObjectsChunk(chunk) {
12293
+ const wallStart = Date.now();
12294
+ let lastError;
12295
+ for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
12296
+ try {
12297
+ const response = await this.controlPlaneRequest(
12298
+ `/control/environments/${this.environmentId}/records/batch`,
12299
+ {
12300
+ method: "POST",
12301
+ body: JSON.stringify({ records: chunk })
12302
+ }
12303
+ );
12304
+ const items = Array.isArray(response.items) ? response.items : [];
12305
+ return { items, durationMs: Date.now() - wallStart };
12306
+ } catch (error) {
12307
+ lastError = error;
12308
+ if (!isRetryableRecordObjectsError(error) || attempt === DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT) {
12309
+ throw error;
12310
+ }
12311
+ await sleep(DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS * attempt);
12312
+ }
12313
+ }
12314
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
12204
12315
  }
12205
12316
  /**
12206
- * Queue a background record import for this environment.
12317
+ * Queue a background record import for this environment (async worker pipeline).
12318
+ *
12319
+ * **vs `recordObjects`:** this path accepts the full payload in one request, returns an
12320
+ * **`importId`**, and processes rows in the background — use **`getRecordImport`** /
12321
+ * **`getRecordImportSummary`** for progress. Choose it for large bulk loads where you do not
12322
+ * need every row committed before the HTTP call returns. Use **`recordObjects`** when you need
12323
+ * synchronous upserts and fine-grained chunk progress via **`onChunkComplete`**.
12207
12324
  */
12208
12325
  async enqueueRecordImport(records, options = {}) {
12209
12326
  return this.controlPlaneRequest(
@@ -13171,23 +13288,36 @@ var Granular = class _Granular {
13171
13288
  if (this.debugHttp) {
13172
13289
  console.log(`[SDK] Requesting: ${url}`);
13173
13290
  }
13174
- const response = await fetch(url, {
13175
- ...options,
13176
- headers: {
13177
- "Authorization": `Bearer ${this.apiKey}`,
13178
- "Content-Type": "application/json",
13179
- "Connection": "close",
13180
- ...options.headers
13291
+ for (let attempt = 1; attempt <= LOCAL_CONTROL_REQUEST_RETRY_COUNT; attempt += 1) {
13292
+ const response = await fetch(url, {
13293
+ ...options,
13294
+ headers: {
13295
+ "Authorization": `Bearer ${this.apiKey}`,
13296
+ "Content-Type": "application/json",
13297
+ "Connection": "close",
13298
+ ...options.headers
13299
+ }
13300
+ });
13301
+ if (response.ok) {
13302
+ if (response.status === 204) {
13303
+ return { deleted: true };
13304
+ }
13305
+ return response.json();
13181
13306
  }
13182
- });
13183
- if (!response.ok) {
13184
13307
  const errorText = await response.text();
13308
+ const retryable = isRetryableLocalWorkerRestart(response.status, errorText, url);
13309
+ if (retryable && attempt < LOCAL_CONTROL_REQUEST_RETRY_COUNT) {
13310
+ if (this.debugHttp) {
13311
+ console.warn(
13312
+ `[SDK] Retrying local control request after worker restart (${attempt}/${LOCAL_CONTROL_REQUEST_RETRY_COUNT - 1} retries used): ${url}`
13313
+ );
13314
+ }
13315
+ await sleep(LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS * attempt);
13316
+ continue;
13317
+ }
13185
13318
  throw new Error(`Granular API Error (${response.status}): ${errorText}`);
13186
13319
  }
13187
- if (response.status === 204) {
13188
- return { deleted: true };
13189
- }
13190
- return response.json();
13320
+ throw new Error(`Granular API Error: exhausted retries for ${url}`);
13191
13321
  }
13192
13322
  };
13193
13323
 
@@ -14113,7 +14243,10 @@ ${loopBlock}
14113
14243
  - Only use classes, methods, and parameter shapes that are explicitly declared in those typedefs.
14114
14244
  - Never invent helper methods such as \`find(...)\` or unsupported parameters such as \`id\` when the typedefs require \`path\`.
14115
14245
  - Use \`ClassName.get({ path })\` only when you already know an object's graph path.
14116
- - Use \`ClassName.list({ limit, saveAs })\` to load typed lists and persist reusable named lists in the heap.
14246
+ - Use \`ClassName.count()\` when you only need a total.
14247
+ - Use \`ClassName.page({ page, perPage, saveAs })\` when you need both records and pagination metadata like \`totalCount\` or \`hasMore\`.
14248
+ - Use \`ClassName.list({ page, perPage, saveAs })\` to load one typed page of records. \`limit\` is only a legacy alias for \`perPage\`.
14249
+ - Use \`for await (const item of ClassName.iterate({ perPage, maxItems }))\` for large batch jobs so you do not materialize the whole result set at once.
14117
14250
  - Instance methods: \`await instance.method_name(params)\`.
14118
14251
  - Static methods: \`await ClassName.static_method(params)\`.
14119
14252
  - Global effects: \`await effect_name(params)\`.
@@ -14125,7 +14258,7 @@ ${loopBlock}
14125
14258
  - Status fields are free-form operational strings, not strict enums. Normalize spelling mentally and do not rely on brittle hard-coded sets that miss variants like \`in-progress\`, \`in_progress\`, \`awaiting-part\`, or \`approval-submitted\`.
14126
14259
  - Do not discard a case, work order, part request, or shipment only because its status string does not match your preferred "open" spelling. If the record is otherwise the clear match, inspect it.
14127
14260
  - Reuse \`heap.getVar(name)\`, \`heap.setVar(name, value)\`, and \`heap.deleteVar(name)\` only when it clearly helps the next step. Do not mirror data into the heap just for completeness.
14128
- - Prefer \`heap.setVar(name, value)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for lists instead of \`heap.setVar(name, array)\`.
14261
+ - Prefer \`heap.setVar(name, value)\` for scalars or one selected instance. Prefer \`ClassName.list({ page, perPage, saveAs })\` for reusable list pages instead of \`heap.setVar(name, array)\`.
14129
14262
  - Never write an empty array into the heap. If a filtered list is empty, keep it local or clear the previous heap value with \`heap.deleteVar(name)\`.
14130
14263
  - Prefer heap-backed state that represents the current choice or recommendation. Avoid storing extra scalar bookkeeping unless it is needed for the next concrete step.
14131
14264
  - Only store true sandbox instances, typed lists of sandbox instances, or scalars in the heap. Results returned by static effects like availability/search helpers are often plain JSON, not sandbox instances.
@@ -14361,6 +14494,6 @@ function resolveJobPresentation({
14361
14494
  };
14362
14495
  }
14363
14496
 
14364
- export { Environment, Granular, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, normalizeEffectBehaviors, normalizePrompt, normalizePromptText, normalizePromptType, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch };
14497
+ export { Environment, Granular, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizePrompt, normalizePromptText, normalizePromptType, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch };
14365
14498
  //# sourceMappingURL=index.mjs.map
14366
14499
  //# sourceMappingURL=index.mjs.map