@granular-software/sdk 0.4.20 → 0.4.22

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
@@ -61,95 +61,107 @@ Projects created with `granular init` can include Markdown for coding agents (op
61
61
  ## Quick Start
62
62
 
63
63
  ```typescript
64
- import { Granular, type ManifestContent } from '@granular-software/sdk';
64
+ import { Granular, type ManifestContent } from "@granular-software/sdk";
65
65
 
66
66
  const granular = new Granular({ apiKey: process.env.GRANULAR_API_KEY });
67
67
 
68
68
  // 1. Connect to the default dev environment for one of your app users
69
69
  const env = await granular.connect({
70
- ontology: 'my-ontology',
71
- environment: 'dev',
72
- userId: 'user_123',
73
- permissions: ['agent'],
74
- name: 'Jane Doe', // optional
75
- email: 'jane@example.com', // optional
70
+ ontology: "my-ontology",
71
+ environment: "dev",
72
+ userId: "user_123",
73
+ permissions: ["agent"],
74
+ name: "Jane Doe", // optional
75
+ email: "jane@example.com", // optional
76
76
  });
77
77
 
78
78
  // 2. Define your domain ontology
79
79
  const manifest: ManifestContent = {
80
80
  schemaVersion: 2,
81
- name: 'my-app',
82
- volumes: [{
83
- name: 'schema',
84
- scope: 'sandbox',
85
- imports: [{ alias: '@std', name: 'standard_modules', label: 'prod' }],
86
- operations: [
87
- // Define classes with typed properties
88
- {
89
- create: 'customer',
90
- extends: '@std/class',
91
- has: {
92
- name: { type: 'string', description: 'Customer name' },
93
- email: { type: 'string', description: 'Email address' },
94
- tier: { type: 'string', description: 'Subscription tier' },
81
+ name: "my-app",
82
+ volumes: [
83
+ {
84
+ name: "schema",
85
+ scope: "sandbox",
86
+ imports: [{ alias: "@std", name: "standard_modules", label: "prod" }],
87
+ operations: [
88
+ // Define classes with typed properties
89
+ {
90
+ create: "customer",
91
+ extends: "@std/class",
92
+ has: {
93
+ name: { type: "string", description: "Customer name" },
94
+ email: { type: "string", description: "Email address" },
95
+ tier: { type: "string", description: "Subscription tier" },
96
+ },
95
97
  },
96
- },
97
- {
98
- create: 'order',
99
- extends: '@std/class',
100
- has: {
101
- total: { type: 'number', description: 'Order total' },
102
- status: { type: 'string', description: 'Order status' },
98
+ {
99
+ create: "order",
100
+ extends: "@std/class",
101
+ has: {
102
+ total: { type: "number", description: "Order total" },
103
+ status: { type: "string", description: "Order status" },
104
+ },
103
105
  },
104
- },
105
- // Define relationships
106
- {
107
- defineRelationship: {
108
- left: 'customer', right: 'order',
109
- leftSubmodel: 'orders', rightSubmodel: 'customer',
110
- leftIsMany: true, rightIsMany: false,
106
+ // Define relationships
107
+ {
108
+ defineRelationship: {
109
+ left: "customer",
110
+ right: "order",
111
+ leftSubmodel: "orders",
112
+ rightSubmodel: "customer",
113
+ leftIsMany: true,
114
+ rightIsMany: false,
115
+ },
111
116
  },
112
- },
113
- ],
114
- }],
117
+ ],
118
+ },
119
+ ],
115
120
  };
116
121
 
117
122
  await env.applyManifest(manifest);
118
123
 
119
124
  // 3. Record object instances
120
125
  await env.recordObject({
121
- className: 'customer',
122
- id: 'cust_42',
123
- label: 'Acme Corp',
124
- fields: { name: 'Acme Corp', email: 'billing@acme.com', tier: 'enterprise' },
126
+ className: "customer",
127
+ id: "cust_42",
128
+ label: "Acme Corp",
129
+ fields: { name: "Acme Corp", email: "billing@acme.com", tier: "enterprise" },
125
130
  });
126
131
 
127
132
  // 4. Register live effect handlers for effects already declared in the ontology manifest
128
133
  await granular.registerEffects(env.sandboxId, [
129
134
  {
130
- name: 'get_billing_summary',
131
- description: 'Get billing summary for a customer',
132
- className: 'customer', // Attached to Customer class
135
+ name: "get_billing_summary",
136
+ description: "Get billing summary for a customer",
137
+ className: "customer", // Attached to Customer class
133
138
  // static omitted → instance method (receives object ID automatically)
134
139
  inputSchema: {
135
- type: 'object',
140
+ type: "object",
136
141
  properties: {
137
- period: { type: 'string', description: 'Billing period (e.g. "2024-Q1")' },
142
+ period: {
143
+ type: "string",
144
+ description: 'Billing period (e.g. "2024-Q1")',
145
+ },
138
146
  },
139
147
  },
140
148
  outputSchema: {
141
- type: 'object',
149
+ type: "object",
142
150
  properties: {
143
- total: { type: 'number', description: 'Total billed amount' },
144
- invoices: { type: 'number', description: 'Number of invoices' },
145
- period: { type: 'string', description: 'The billing period' },
151
+ total: { type: "number", description: "Total billed amount" },
152
+ invoices: { type: "number", description: "Number of invoices" },
153
+ period: { type: "string", description: "The billing period" },
146
154
  },
147
- required: ['total', 'invoices'],
155
+ required: ["total", "invoices"],
148
156
  },
149
157
  handler: async (customerId: string, params: any, ctx: any) => {
150
158
  // customerId comes from `this.id` in sandbox code
151
- console.log('Running for subject:', ctx.user.subjectId);
152
- return { total: 4250.00, invoices: 3, period: params?.period || 'current' };
159
+ console.log("Running for subject:", ctx.user.subjectId);
160
+ return {
161
+ total: 4250.0,
162
+ invoices: 3,
163
+ period: params?.period || "current",
164
+ };
153
165
  },
154
166
  },
155
167
  ]);
@@ -183,13 +195,13 @@ Effects must be declared ahead of time in the ontology version manifest with `wi
183
195
  ## Core Flow
184
196
 
185
197
  ```
186
- declare effects in the manifest → `granular build` creates or reuses a version → `connect({ ontology, environment, userId })` opens a named environment → `recordObject()` → `registerEffects()` → `submitJob()`
198
+ declare effects in the manifest → `granular build` creates or reuses a version → `connect({ ontology, environment, userId })` opens a named environment → `recordObject()` or `recordObjects()` → `registerEffects()` → `submitJob()`
187
199
  ```
188
200
 
189
201
  1. **`connect()`** — Connect to an ontology environment for a given `userId`, returning an `Environment`
190
202
  2. **`recordUser()`** — Optional explicit user upsert when you want the returned `granularId`
191
203
  3. **`applyManifest()`** — Define your domain ontology (classes, properties, relationships)
192
- 4. **`recordObject()`** — Create/update instances of your classes with fields and relationships
204
+ 4. **`recordObject()` / `recordObjects()`** — Use `recordObject()` for one targeted upsert. Use `recordObjects()` for immediate multi-record writes with chunk progress.
193
205
  5. **`granular.registerEffects()`** — Register sandbox-scoped live handlers for effects declared in the ontology manifest
194
206
  6. **`submitJob()`** — Execute code in the sandbox that uses the auto-generated typed classes
195
207
 
@@ -200,42 +212,47 @@ Use `applyManifest()` to declare classes, typed properties, and relationships:
200
212
  ```typescript
201
213
  const manifest: ManifestContent = {
202
214
  schemaVersion: 2,
203
- name: 'library-app',
204
- volumes: [{
205
- name: 'schema',
206
- scope: 'sandbox',
207
- imports: [{ alias: '@std', name: 'standard_modules', label: 'prod' }],
208
- operations: [
209
- // Classes with typed properties
210
- {
211
- create: 'author',
212
- extends: '@std/class',
213
- has: {
214
- name: { type: 'string', description: 'Author full name' },
215
- birth_year: { type: 'number', description: 'Year of birth' },
215
+ name: "library-app",
216
+ volumes: [
217
+ {
218
+ name: "schema",
219
+ scope: "sandbox",
220
+ imports: [{ alias: "@std", name: "standard_modules", label: "prod" }],
221
+ operations: [
222
+ // Classes with typed properties
223
+ {
224
+ create: "author",
225
+ extends: "@std/class",
226
+ has: {
227
+ name: { type: "string", description: "Author full name" },
228
+ birth_year: { type: "number", description: "Year of birth" },
229
+ },
216
230
  },
217
- },
218
- {
219
- create: 'book',
220
- extends: '@std/class',
221
- has: {
222
- title: { type: 'string', description: 'Book title' },
223
- isbn: { type: 'string', description: 'ISBN number' },
224
- published_year: { type: 'number', description: 'Year published' },
231
+ {
232
+ create: "book",
233
+ extends: "@std/class",
234
+ has: {
235
+ title: { type: "string", description: "Book title" },
236
+ isbn: { type: "string", description: "ISBN number" },
237
+ published_year: { type: "number", description: "Year published" },
238
+ },
225
239
  },
226
- },
227
240
 
228
- // Relationships (bidirectional, with cardinality)
229
- {
230
- defineRelationship: {
231
- left: 'author', right: 'book',
232
- leftSubmodel: 'books', rightSubmodel: 'author',
233
- leftIsMany: true, rightIsMany: false,
234
- // author.books (one-to-many), book.author (many-to-one)
241
+ // Relationships (bidirectional, with cardinality)
242
+ {
243
+ defineRelationship: {
244
+ left: "author",
245
+ right: "book",
246
+ leftSubmodel: "books",
247
+ rightSubmodel: "author",
248
+ leftIsMany: true,
249
+ rightIsMany: false,
250
+ // → author.books (one-to-many), book.author (many-to-one)
251
+ },
235
252
  },
236
- },
237
- ],
238
- }],
253
+ ],
254
+ },
255
+ ],
239
256
  };
240
257
 
241
258
  await env.applyManifest(manifest);
@@ -247,32 +264,47 @@ After defining the ontology, connect as a user and populate it with data:
247
264
 
248
265
  ```typescript
249
266
  const env = await granular.connect({
250
- ontology: 'library-app',
251
- environment: 'dev',
252
- userId: 'user_123',
253
- permissions: ['agent'],
267
+ ontology: "library-app",
268
+ environment: "dev",
269
+ userId: "user_123",
270
+ permissions: ["agent"],
254
271
  });
255
272
 
256
- const tolkien = await env.recordObject({
257
- className: 'author',
258
- id: 'tolkien', // Real-world ID (unique per class)
259
- label: 'J.R.R. Tolkien',
260
- fields: { name: 'J.R.R. Tolkien', birth_year: 1892 },
261
- });
273
+ const [tolkien, lotr] = await env.recordObjects(
274
+ [
275
+ {
276
+ className: "author",
277
+ id: "tolkien", // Real-world ID (unique per class)
278
+ label: "J.R.R. Tolkien",
279
+ fields: { name: "J.R.R. Tolkien", birth_year: 1892 },
280
+ },
281
+ {
282
+ className: "book",
283
+ id: "lotr",
284
+ label: "The Lord of the Rings",
285
+ fields: {
286
+ title: "The Lord of the Rings",
287
+ isbn: "978-0-618-64015-7",
288
+ published_year: 1954,
289
+ },
290
+ relationships: { author: "tolkien" }, // Real-world ID — SDK resolves it automatically
291
+ },
292
+ ],
293
+ {
294
+ onChunkComplete(info) {
295
+ console.log(`Committed chunk ${info.chunkIndex + 1}/${info.totalChunks}`);
296
+ },
297
+ },
298
+ );
262
299
  // tolkien.path → 'author_tolkien' (internal graph path, unique globally)
263
300
  // tolkien.id → 'tolkien' (real-world ID)
264
-
265
- const lotr = await env.recordObject({
266
- className: 'book',
267
- id: 'lotr',
268
- label: 'The Lord of the Rings',
269
- fields: { title: 'The Lord of the Rings', isbn: '978-0-618-64015-7', published_year: 1954 },
270
- relationships: { author: 'tolkien' }, // Real-world ID — SDK resolves it automatically
271
- });
301
+ // lotr.created → true (false means the record already existed and was updated)
272
302
  ```
273
303
 
274
304
  > **Cross-class ID uniqueness**: Two objects of different classes can share the same real-world ID (e.g., an `author` "tolkien" and a `publisher` "tolkien"). The SDK derives unique graph paths internally (`author_tolkien`, `publisher_tolkien`) so they never collide.
275
305
 
306
+ For one-off writes, `recordObject(...)` is still the clearest choice. For large fire-and-forget imports, use `enqueueRecordImport(...)` and poll `getRecordImport(...)` / `getRecordImportSummary()` for aggregate progress.
307
+
276
308
  ## Effect Definitions
277
309
 
278
310
  Effects are declared in the manifest with `withEffect`, then their live handlers are registered at sandbox scope. Effects can be **instance methods**, **static methods**, or **global functions**. Both `inputSchema` and `outputSchema` use JSON Schema:
@@ -282,44 +314,44 @@ await granular.registerEffects(env.sandboxId, [
282
314
  // Instance method: called as `tolkien.get_bio({ detailed: true })`
283
315
  // Handler receives (objectId, params)
284
316
  {
285
- name: 'get_bio',
286
- description: 'Get biography of an author',
287
- className: 'author', // Attached to Author class
317
+ name: "get_bio",
318
+ description: "Get biography of an author",
319
+ className: "author", // Attached to Author class
288
320
  // static: false (default) // Instance method
289
321
  inputSchema: {
290
- type: 'object',
322
+ type: "object",
291
323
  properties: {
292
- detailed: { type: 'boolean', description: 'Include full details' },
324
+ detailed: { type: "boolean", description: "Include full details" },
293
325
  },
294
326
  },
295
327
  outputSchema: {
296
- type: 'object',
328
+ type: "object",
297
329
  properties: {
298
- bio: { type: 'string', description: 'The biography text' },
299
- source: { type: 'string', description: 'Source of the bio' },
330
+ bio: { type: "string", description: "The biography text" },
331
+ source: { type: "string", description: "Source of the bio" },
300
332
  },
301
- required: ['bio'],
333
+ required: ["bio"],
302
334
  },
303
335
  handler: async (id: string, params: any, ctx: any) => {
304
- return { bio: `Biography of ${id}`, source: 'database' };
336
+ return { bio: `Biography of ${id}`, source: "database" };
305
337
  },
306
338
  },
307
339
 
308
340
  // Static method: called as `Author.search({ query: 'tolkien' })`
309
341
  // Handler receives (params) — no object ID
310
342
  {
311
- name: 'search',
312
- description: 'Search for authors',
313
- className: 'author',
343
+ name: "search",
344
+ description: "Search for authors",
345
+ className: "author",
314
346
  static: true,
315
347
  inputSchema: {
316
- type: 'object',
317
- properties: { query: { type: 'string' } },
318
- required: ['query'],
348
+ type: "object",
349
+ properties: { query: { type: "string" } },
350
+ required: ["query"],
319
351
  },
320
352
  outputSchema: {
321
- type: 'object',
322
- properties: { results: { type: 'array' } },
353
+ type: "object",
354
+ properties: { results: { type: "array" } },
323
355
  },
324
356
  handler: async (params: any, ctx: any) => {
325
357
  return { results: [`Found: ${params.query}`] };
@@ -329,16 +361,16 @@ await granular.registerEffects(env.sandboxId, [
329
361
  // Global tool: called as `global_search({ query: 'rings' })`
330
362
  // No className → standalone exported function
331
363
  {
332
- name: 'global_search',
333
- description: 'Search across everything',
364
+ name: "global_search",
365
+ description: "Search across everything",
334
366
  inputSchema: {
335
- type: 'object',
336
- properties: { query: { type: 'string' } },
337
- required: ['query'],
367
+ type: "object",
368
+ properties: { query: { type: "string" } },
369
+ required: ["query"],
338
370
  },
339
371
  outputSchema: {
340
- type: 'object',
341
- properties: { results: { type: 'array' } },
372
+ type: "object",
373
+ properties: { results: { type: "array" } },
342
374
  },
343
375
  handler: async (params: any, ctx: any) => {
344
376
  return { results: [`Result for: ${params.query}`] };
@@ -353,6 +385,14 @@ After applying the manifest and publishing tools, the sandbox gets **auto-genera
353
385
 
354
386
  ```typescript
355
387
  // What the sandbox sees (auto-generated):
388
+ export interface SandboxPageResult<T> {
389
+ items: T[];
390
+ page: number;
391
+ perPage: number;
392
+ totalCount: number;
393
+ hasMore: boolean;
394
+ }
395
+
356
396
  export declare class Author {
357
397
  readonly id: string;
358
398
  readonly name: string;
@@ -361,13 +401,45 @@ export declare class Author {
361
401
  constructor(id: string, fields?: Record<string, any>);
362
402
 
363
403
  /** Get a cached Author by graph path, hydrating from the graph when needed */
364
- static get(query: { path: string; refresh?: boolean }): Promise<Author | null>;
365
-
366
- /** List known Author instances */
367
- static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Author[]>;
404
+ static get(query: {
405
+ path: string;
406
+ refresh?: boolean;
407
+ }): Promise<Author | null>;
408
+
409
+ /** Count Author instances without loading them into the heap */
410
+ static count(): Promise<number>;
411
+
412
+ /** Return one page of Author instances together with pagination metadata */
413
+ static page(query?: {
414
+ page?: number;
415
+ perPage?: number;
416
+ limit?: number;
417
+ saveAs?: string;
418
+ refresh?: boolean;
419
+ }): Promise<SandboxPageResult<Author>>;
420
+
421
+ /** List one page of Author instances */
422
+ static list(query?: {
423
+ page?: number;
424
+ perPage?: number;
425
+ limit?: number;
426
+ saveAs?: string;
427
+ refresh?: boolean;
428
+ }): Promise<Author[]>;
429
+
430
+ /** Stream Author instances page by page */
431
+ static iterate(query?: {
432
+ page?: number;
433
+ perPage?: number;
434
+ limit?: number;
435
+ maxItems?: number;
436
+ refresh?: boolean;
437
+ }): AsyncIterable<Author>;
368
438
 
369
439
  /** Get biography of an author */
370
- get_bio(input?: { detailed?: boolean }): Promise<{ bio: string; source?: string }>;
440
+ get_bio(input?: {
441
+ detailed?: boolean;
442
+ }): Promise<{ bio: string; source?: string }>;
371
443
 
372
444
  /** Search for authors (static) */
373
445
  static search(input: { query: string }): Promise<{ results?: any[] }>;
@@ -384,36 +456,69 @@ export declare class Book {
384
456
 
385
457
  static get(query: { path: string; refresh?: boolean }): Promise<Book | null>;
386
458
 
387
- static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Book[]>;
459
+ static count(): Promise<number>;
460
+
461
+ static page(query?: {
462
+ page?: number;
463
+ perPage?: number;
464
+ limit?: number;
465
+ saveAs?: string;
466
+ refresh?: boolean;
467
+ }): Promise<SandboxPageResult<Book>>;
468
+
469
+ static list(query?: {
470
+ page?: number;
471
+ perPage?: number;
472
+ limit?: number;
473
+ saveAs?: string;
474
+ refresh?: boolean;
475
+ }): Promise<Book[]>;
476
+
477
+ static iterate(query?: {
478
+ page?: number;
479
+ perPage?: number;
480
+ limit?: number;
481
+ maxItems?: number;
482
+ refresh?: boolean;
483
+ }): AsyncIterable<Book>;
388
484
 
389
485
  /** Navigate to author (many_to_one) */
390
486
  get_author(): Promise<Author | null>;
391
487
  }
392
488
 
393
489
  /** Search across everything */
394
- export declare function global_search(input: { query: string }): Promise<{ results?: any[] }>;
490
+ export declare function global_search(input: {
491
+ query: string;
492
+ }): Promise<{ results?: any[] }>;
395
493
  ```
396
494
 
397
495
  The LLM or user writes code against these typed classes:
398
496
 
399
497
  ```typescript
400
- import { Author, Book, global_search } from './sandbox-tools';
401
-
402
- const authors = await Author.list();
403
- const tolkien = authors.find((author) => author.name === 'J.R.R. Tolkien');
404
- if (!tolkien) throw new Error('Author not found');
405
- console.log(tolkien.name); // "J.R.R. Tolkien"
498
+ import { Author, Book, global_search } from "./sandbox-tools";
499
+
500
+ const totalAuthors = await Author.count();
501
+ const firstPage = await Author.page({ page: 1, perPage: 25 });
502
+ const authors = firstPage.items;
503
+ const tolkien = authors.find((author) => author.name === "J.R.R. Tolkien");
504
+ if (!tolkien) throw new Error("Author not found");
505
+ console.log(totalAuthors, firstPage.hasMore);
506
+ console.log(tolkien.name); // "J.R.R. Tolkien"
507
+
508
+ for await (const author of Author.iterate({ perPage: 100, maxItems: 200 })) {
509
+ console.log(author.id);
510
+ }
406
511
 
407
512
  const bio = await tolkien.get_bio({ detailed: true });
408
- console.log(bio.bio); // typed as string
513
+ console.log(bio.bio); // typed as string
409
514
 
410
515
  const books = await tolkien.get_books();
411
516
  for (const book of books) {
412
- console.log(book.title, book.isbn); // typed properties
517
+ console.log(book.title, book.isbn); // typed properties
413
518
  }
414
519
 
415
- const results = await Author.search({ query: 'tolkien' });
416
- const globalResults = await global_search({ query: 'rings' });
520
+ const results = await Author.search({ query: "tolkien" });
521
+ const globalResults = await global_search({ query: "rings" });
417
522
  ```
418
523
 
419
524
  ## GraphQL API
@@ -423,7 +528,7 @@ Every environment has a built-in GraphQL API for direct graph queries:
423
528
  ```typescript
424
529
  // Authenticated automatically with your API key
425
530
  const result = await env.graphql(
426
- `query { model(path: "author") { path label submodels { path label } } }`
531
+ `query { model(path: "author") { path label submodels { path label } } }`,
427
532
  );
428
533
 
429
534
  // The endpoint is also available as a URL
@@ -445,21 +550,27 @@ See [examples/](./examples/) for runnable code:
445
550
  ## API Reference
446
551
 
447
552
  ### `granular.recordUser(options)`
553
+
448
554
  Registers a user identity and their assigned permission profiles.
449
555
 
450
556
  ### `granular.connect(options)`
557
+
451
558
  Connects to an environment session for the specified user. Pass `ontology` and `environment` explicitly, for example `environment: 'dev'` or `environment: 'prod'`. Only pass `tagName` for advanced overrides.
452
559
 
453
560
  ### `environment.applyManifest(manifest)`
561
+
454
562
  Defines domain ontology: classes (with typed properties), and relationships (with cardinality).
455
563
 
456
564
  ### `environment.recordObject(options)`
565
+
457
566
  Creates or updates a class instance with fields and relationships. Returns `{ path, id, created }`.
458
567
 
459
568
  ### `environment.recordObjects(records, options?)`
569
+
460
570
  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
571
 
462
572
  Optional **`options`**:
573
+
463
574
  - **`batchSize`** — max rows per request (default 100).
464
575
  - **`concurrency`** — how many chunk requests may run in parallel (default 1, max 16); can reduce wall time when the server can overlap work.
465
576
  - **`onChunkComplete`** — async-friendly hook after each chunk for progress UIs; the returned array is always ordered like `records`.
@@ -467,30 +578,39 @@ Optional **`options`**:
467
578
  **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
579
 
469
580
  ### `environment.getRelationships(modelPath)`
581
+
470
582
  Returns relationship definitions for a given class.
471
583
 
472
584
  ### `environment.listRelated(modelPath, submodelPath)`
585
+
473
586
  Lists related instances through a relationship.
474
587
 
475
588
  ### `granular.registerEffects(sandboxId, effects)`
589
+
476
590
  Registers sandbox-scoped live handlers for effects declared in the ontology manifest. Effects can be instance methods (`className` set, `static` omitted), static methods (`static: true`), or global functions (no `className`).
477
591
 
478
592
  ### `environment.submitJob(code)`
593
+
479
594
  Submits code to be executed in the sandbox. The code imports typed classes from `./sandbox-tools`.
480
595
 
481
596
  ### `environment.getDomainDocumentation()`
597
+
482
598
  Get auto-generated TypeScript class declarations. Pass this to LLMs to help them write correct code.
483
599
 
484
600
  ### `environment.graphql(query, variables?)`
601
+
485
602
  Execute a GraphQL query against the environment's graph. Authenticated automatically.
486
603
 
487
604
  ### `environment.on(event, handler)`
605
+
488
606
  Listen for events: `'effect:invoke'`, `'effect:result'`, `'job:status'`, `'stdout'`, etc. Legacy `'tool:*'` aliases still exist internally but are no longer the primary model.
489
607
 
490
608
  ### `Environment.toGraphPath(className, id)`
609
+
491
610
  Convert a class name + real-world ID to a unique graph path (`{className}_{id}`).
492
611
 
493
612
  ### `Environment.extractIdFromGraphPath(graphPath, className)`
613
+
494
614
  Extract the real-world ID from a graph path by stripping the class prefix.
495
615
 
496
616
  ## License
@@ -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';