@granular-software/sdk 0.4.20 → 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/README.md CHANGED
@@ -353,6 +353,14 @@ After applying the manifest and publishing tools, the sandbox gets **auto-genera
353
353
 
354
354
  ```typescript
355
355
  // What the sandbox sees (auto-generated):
356
+ export interface SandboxPageResult<T> {
357
+ items: T[];
358
+ page: number;
359
+ perPage: number;
360
+ totalCount: number;
361
+ hasMore: boolean;
362
+ }
363
+
356
364
  export declare class Author {
357
365
  readonly id: string;
358
366
  readonly name: string;
@@ -363,8 +371,17 @@ export declare class Author {
363
371
  /** Get a cached Author by graph path, hydrating from the graph when needed */
364
372
  static get(query: { path: string; refresh?: boolean }): Promise<Author | null>;
365
373
 
366
- /** List known Author instances */
367
- static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Author[]>;
374
+ /** Count Author instances without loading them into the heap */
375
+ static count(): Promise<number>;
376
+
377
+ /** Return one page of Author instances together with pagination metadata */
378
+ static page(query?: { page?: number; perPage?: number; limit?: number; saveAs?: string; refresh?: boolean }): Promise<SandboxPageResult<Author>>;
379
+
380
+ /** List one page of Author instances */
381
+ static list(query?: { page?: number; perPage?: number; limit?: number; saveAs?: string; refresh?: boolean }): Promise<Author[]>;
382
+
383
+ /** Stream Author instances page by page */
384
+ static iterate(query?: { page?: number; perPage?: number; limit?: number; maxItems?: number; refresh?: boolean }): AsyncIterable<Author>;
368
385
 
369
386
  /** Get biography of an author */
370
387
  get_bio(input?: { detailed?: boolean }): Promise<{ bio: string; source?: string }>;
@@ -384,7 +401,13 @@ export declare class Book {
384
401
 
385
402
  static get(query: { path: string; refresh?: boolean }): Promise<Book | null>;
386
403
 
387
- static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Book[]>;
404
+ static count(): Promise<number>;
405
+
406
+ static page(query?: { page?: number; perPage?: number; limit?: number; saveAs?: string; refresh?: boolean }): Promise<SandboxPageResult<Book>>;
407
+
408
+ static list(query?: { page?: number; perPage?: number; limit?: number; saveAs?: string; refresh?: boolean }): Promise<Book[]>;
409
+
410
+ static iterate(query?: { page?: number; perPage?: number; limit?: number; maxItems?: number; refresh?: boolean }): AsyncIterable<Book>;
388
411
 
389
412
  /** Navigate to author (many_to_one) */
390
413
  get_author(): Promise<Author | null>;
@@ -399,11 +422,18 @@ The LLM or user writes code against these typed classes:
399
422
  ```typescript
400
423
  import { Author, Book, global_search } from './sandbox-tools';
401
424
 
402
- const authors = await Author.list();
425
+ const totalAuthors = await Author.count();
426
+ const firstPage = await Author.page({ page: 1, perPage: 25 });
427
+ const authors = firstPage.items;
403
428
  const tolkien = authors.find((author) => author.name === 'J.R.R. Tolkien');
404
429
  if (!tolkien) throw new Error('Author not found');
430
+ console.log(totalAuthors, firstPage.hasMore);
405
431
  console.log(tolkien.name); // "J.R.R. Tolkien"
406
432
 
433
+ for await (const author of Author.iterate({ perPage: 100, maxItems: 200 })) {
434
+ console.log(author.id);
435
+ }
436
+
407
437
  const bio = await tolkien.get_bio({ detailed: true });
408
438
  console.log(bio.bio); // typed as string
409
439
 
@@ -1,4 +1,4 @@
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';
1
+ import { P as Prompt, d as Environment, c 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-DLGC0mJk.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, 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';
1
+ import { P as Prompt, d as Environment, c 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-DLGC0mJk.js';
2
2
  import { GeneratedJobCodeIssue, HarnessControllerBudgets } from './agent-harness.js';
3
3
  import '@automerge/automerge';
4
4
  import '@automerge/automerge/slim';
@@ -4687,10 +4687,15 @@ var Session = class {
4687
4687
  * ```typescript
4688
4688
  * import { Author, Book, global_search } from './sandbox-tools';
4689
4689
  *
4690
- * const authors = await Author.list({ limit: 10, saveAs: 'recent_authors' });
4690
+ * const totalAuthors = await Author.count();
4691
+ * const firstAuthorsPage = await Author.page({ page: 1, perPage: 10, saveAs: 'recent_authors' });
4692
+ * const authors = firstAuthorsPage.items;
4691
4693
  * const tolkien = await Author.get({ path: 'author_tolkien' });
4692
4694
  * const bio = await tolkien.get_bio({ detailed: true });
4693
4695
  * const books = await tolkien.get_books();
4696
+ * for await (const author of Author.iterate({ perPage: 100, maxItems: 500 })) {
4697
+ * console.log(author.id);
4698
+ * }
4694
4699
  * ```
4695
4700
  *
4696
4701
  * Effect calls (instance methods, static methods, global functions) trigger
@@ -11248,9 +11253,9 @@ var STANDARD_MODULES_OPERATIONS = [
11248
11253
  { create: "class", extends: "entity", has: {} },
11249
11254
  { create: "user", extends: "entity", has: { email: { value: void 0 }, firstName: { value: void 0 }, lastName: { value: void 0 } } },
11250
11255
  { create: "company", extends: "entity", has: { name: { value: void 0 }, website: { value: void 0 } } },
11251
- { create: "string", has: { value: { value: void 0 } } },
11252
- { create: "number", has: { value: { value: 0 } } },
11253
- { create: "boolean", has: { value: { value: false } } },
11256
+ { create: "string", has: {} },
11257
+ { create: "number", has: {} },
11258
+ { create: "boolean", has: {} },
11254
11259
  { create: "tool_parameter", has: { name: { value: void 0 }, type: { value: "string" }, description: { value: void 0 }, required: { value: false } } }
11255
11260
  ];
11256
11261
  var BUILTIN_MODULES = {
@@ -11260,6 +11265,8 @@ var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
11260
11265
  var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
11261
11266
  var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
11262
11267
  var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
11268
+ var LOCAL_CONTROL_REQUEST_RETRY_COUNT = 4;
11269
+ var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
11263
11270
  function planRecordObjectsChunks(records, batchSize) {
11264
11271
  const total = records.length;
11265
11272
  const size = Math.max(1, Math.min(batchSize, total));
@@ -11274,6 +11281,17 @@ function planRecordObjectsChunks(records, batchSize) {
11274
11281
  function sleep(ms) {
11275
11282
  return new Promise((resolve) => setTimeout(resolve, ms));
11276
11283
  }
11284
+ function isLocalControlUrl(url) {
11285
+ try {
11286
+ const parsed = new URL(url);
11287
+ return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1";
11288
+ } catch {
11289
+ return false;
11290
+ }
11291
+ }
11292
+ function isRetryableLocalWorkerRestart(status, body, url) {
11293
+ return isLocalControlUrl(url) && (status === 503 && body.includes("Your worker restarted mid-request") || status === 500 && body.includes("Network connection lost"));
11294
+ }
11277
11295
  function isRetryableRecordObjectsError(error) {
11278
11296
  const message = error instanceof Error ? error.message : String(error);
11279
11297
  return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(message);
@@ -13297,23 +13315,36 @@ var Granular = class _Granular {
13297
13315
  if (this.debugHttp) {
13298
13316
  console.log(`[SDK] Requesting: ${url}`);
13299
13317
  }
13300
- const response = await fetch(url, {
13301
- ...options,
13302
- headers: {
13303
- "Authorization": `Bearer ${this.apiKey}`,
13304
- "Content-Type": "application/json",
13305
- "Connection": "close",
13306
- ...options.headers
13318
+ for (let attempt = 1; attempt <= LOCAL_CONTROL_REQUEST_RETRY_COUNT; attempt += 1) {
13319
+ const response = await fetch(url, {
13320
+ ...options,
13321
+ headers: {
13322
+ "Authorization": `Bearer ${this.apiKey}`,
13323
+ "Content-Type": "application/json",
13324
+ "Connection": "close",
13325
+ ...options.headers
13326
+ }
13327
+ });
13328
+ if (response.ok) {
13329
+ if (response.status === 204) {
13330
+ return { deleted: true };
13331
+ }
13332
+ return response.json();
13307
13333
  }
13308
- });
13309
- if (!response.ok) {
13310
13334
  const errorText = await response.text();
13335
+ const retryable = isRetryableLocalWorkerRestart(response.status, errorText, url);
13336
+ if (retryable && attempt < LOCAL_CONTROL_REQUEST_RETRY_COUNT) {
13337
+ if (this.debugHttp) {
13338
+ console.warn(
13339
+ `[SDK] Retrying local control request after worker restart (${attempt}/${LOCAL_CONTROL_REQUEST_RETRY_COUNT - 1} retries used): ${url}`
13340
+ );
13341
+ }
13342
+ await sleep(LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS * attempt);
13343
+ continue;
13344
+ }
13311
13345
  throw new Error(`Granular API Error (${response.status}): ${errorText}`);
13312
13346
  }
13313
- if (response.status === 204) {
13314
- return { deleted: true };
13315
- }
13316
- return response.json();
13347
+ throw new Error(`Granular API Error: exhausted retries for ${url}`);
13317
13348
  }
13318
13349
  };
13319
13350
 
@@ -14234,7 +14265,10 @@ ${loopBlock}
14234
14265
  - Only use classes, methods, and parameter shapes that are explicitly declared in those typedefs.
14235
14266
  - Never invent helper methods such as \`find(...)\` or unsupported parameters such as \`id\` when the typedefs require \`path\`.
14236
14267
  - Use \`ClassName.get({ path })\` only when you already know an object's graph path.
14237
- - Use \`ClassName.list({ limit, saveAs })\` to load typed lists and persist reusable named lists in the heap.
14268
+ - Use \`ClassName.count()\` when you only need a total.
14269
+ - Use \`ClassName.page({ page, perPage, saveAs })\` when you need both records and pagination metadata like \`totalCount\` or \`hasMore\`.
14270
+ - Use \`ClassName.list({ page, perPage, saveAs })\` to load one typed page of records. \`limit\` is only a legacy alias for \`perPage\`.
14271
+ - 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.
14238
14272
  - Instance methods: \`await instance.method_name(params)\`.
14239
14273
  - Static methods: \`await ClassName.static_method(params)\`.
14240
14274
  - Global effects: \`await effect_name(params)\`.
@@ -14246,7 +14280,7 @@ ${loopBlock}
14246
14280
  - 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\`.
14247
14281
  - 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.
14248
14282
  - 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.
14249
- - Prefer \`heap.setVar(name, value)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for lists instead of \`heap.setVar(name, array)\`.
14283
+ - 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)\`.
14250
14284
  - 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)\`.
14251
14285
  - 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.
14252
14286
  - 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.