@granular-software/sdk 0.4.21 → 0.4.23

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}`] };
@@ -369,22 +401,45 @@ export declare class Author {
369
401
  constructor(id: string, fields?: Record<string, any>);
370
402
 
371
403
  /** Get a cached Author by graph path, hydrating from the graph when needed */
372
- static get(query: { path: string; refresh?: boolean }): Promise<Author | null>;
404
+ static get(query: {
405
+ path: string;
406
+ refresh?: boolean;
407
+ }): Promise<Author | null>;
373
408
 
374
409
  /** Count Author instances without loading them into the heap */
375
410
  static count(): Promise<number>;
376
411
 
377
412
  /** 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>>;
413
+ static page(query?: {
414
+ page?: number;
415
+ perPage?: number;
416
+ limit?: number;
417
+ saveAs?: string;
418
+ refresh?: boolean;
419
+ }): Promise<SandboxPageResult<Author>>;
379
420
 
380
421
  /** List one page of Author instances */
381
- static list(query?: { page?: number; perPage?: number; limit?: number; saveAs?: string; refresh?: boolean }): Promise<Author[]>;
422
+ static list(query?: {
423
+ page?: number;
424
+ perPage?: number;
425
+ limit?: number;
426
+ saveAs?: string;
427
+ refresh?: boolean;
428
+ }): Promise<Author[]>;
382
429
 
383
430
  /** Stream Author instances page by page */
384
- static iterate(query?: { page?: number; perPage?: number; limit?: number; maxItems?: number; refresh?: boolean }): AsyncIterable<Author>;
431
+ static iterate(query?: {
432
+ page?: number;
433
+ perPage?: number;
434
+ limit?: number;
435
+ maxItems?: number;
436
+ refresh?: boolean;
437
+ }): AsyncIterable<Author>;
385
438
 
386
439
  /** Get biography of an author */
387
- get_bio(input?: { detailed?: boolean }): Promise<{ bio: string; source?: string }>;
440
+ get_bio(input?: {
441
+ detailed?: boolean;
442
+ }): Promise<{ bio: string; source?: string }>;
388
443
 
389
444
  /** Search for authors (static) */
390
445
  static search(input: { query: string }): Promise<{ results?: any[] }>;
@@ -403,47 +458,67 @@ export declare class Book {
403
458
 
404
459
  static count(): Promise<number>;
405
460
 
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>;
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>;
411
484
 
412
485
  /** Navigate to author (many_to_one) */
413
486
  get_author(): Promise<Author | null>;
414
487
  }
415
488
 
416
489
  /** Search across everything */
417
- 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[] }>;
418
493
  ```
419
494
 
420
495
  The LLM or user writes code against these typed classes:
421
496
 
422
497
  ```typescript
423
- import { Author, Book, global_search } from './sandbox-tools';
498
+ import { Author, Book, global_search } from "./sandbox-tools";
424
499
 
425
500
  const totalAuthors = await Author.count();
426
501
  const firstPage = await Author.page({ page: 1, perPage: 25 });
427
502
  const authors = firstPage.items;
428
- const tolkien = authors.find((author) => author.name === 'J.R.R. Tolkien');
429
- if (!tolkien) throw new Error('Author not found');
503
+ const tolkien = authors.find((author) => author.name === "J.R.R. Tolkien");
504
+ if (!tolkien) throw new Error("Author not found");
430
505
  console.log(totalAuthors, firstPage.hasMore);
431
- console.log(tolkien.name); // "J.R.R. Tolkien"
506
+ console.log(tolkien.name); // "J.R.R. Tolkien"
432
507
 
433
508
  for await (const author of Author.iterate({ perPage: 100, maxItems: 200 })) {
434
509
  console.log(author.id);
435
510
  }
436
511
 
437
512
  const bio = await tolkien.get_bio({ detailed: true });
438
- console.log(bio.bio); // typed as string
513
+ console.log(bio.bio); // typed as string
439
514
 
440
515
  const books = await tolkien.get_books();
441
516
  for (const book of books) {
442
- console.log(book.title, book.isbn); // typed properties
517
+ console.log(book.title, book.isbn); // typed properties
443
518
  }
444
519
 
445
- const results = await Author.search({ query: 'tolkien' });
446
- const globalResults = await global_search({ query: 'rings' });
520
+ const results = await Author.search({ query: "tolkien" });
521
+ const globalResults = await global_search({ query: "rings" });
447
522
  ```
448
523
 
449
524
  ## GraphQL API
@@ -453,7 +528,7 @@ Every environment has a built-in GraphQL API for direct graph queries:
453
528
  ```typescript
454
529
  // Authenticated automatically with your API key
455
530
  const result = await env.graphql(
456
- `query { model(path: "author") { path label submodels { path label } } }`
531
+ `query { model(path: "author") { path label submodels { path label } } }`,
457
532
  );
458
533
 
459
534
  // The endpoint is also available as a URL
@@ -475,21 +550,27 @@ See [examples/](./examples/) for runnable code:
475
550
  ## API Reference
476
551
 
477
552
  ### `granular.recordUser(options)`
553
+
478
554
  Registers a user identity and their assigned permission profiles.
479
555
 
480
556
  ### `granular.connect(options)`
557
+
481
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.
482
559
 
483
560
  ### `environment.applyManifest(manifest)`
561
+
484
562
  Defines domain ontology: classes (with typed properties), and relationships (with cardinality).
485
563
 
486
564
  ### `environment.recordObject(options)`
565
+
487
566
  Creates or updates a class instance with fields and relationships. Returns `{ path, id, created }`.
488
567
 
489
568
  ### `environment.recordObjects(records, options?)`
569
+
490
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.
491
571
 
492
572
  Optional **`options`**:
573
+
493
574
  - **`batchSize`** — max rows per request (default 100).
494
575
  - **`concurrency`** — how many chunk requests may run in parallel (default 1, max 16); can reduce wall time when the server can overlap work.
495
576
  - **`onChunkComplete`** — async-friendly hook after each chunk for progress UIs; the returned array is always ordered like `records`.
@@ -497,30 +578,39 @@ Optional **`options`**:
497
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.
498
579
 
499
580
  ### `environment.getRelationships(modelPath)`
581
+
500
582
  Returns relationship definitions for a given class.
501
583
 
502
584
  ### `environment.listRelated(modelPath, submodelPath)`
585
+
503
586
  Lists related instances through a relationship.
504
587
 
505
588
  ### `granular.registerEffects(sandboxId, effects)`
589
+
506
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`).
507
591
 
508
592
  ### `environment.submitJob(code)`
593
+
509
594
  Submits code to be executed in the sandbox. The code imports typed classes from `./sandbox-tools`.
510
595
 
511
596
  ### `environment.getDomainDocumentation()`
597
+
512
598
  Get auto-generated TypeScript class declarations. Pass this to LLMs to help them write correct code.
513
599
 
514
600
  ### `environment.graphql(query, variables?)`
601
+
515
602
  Execute a GraphQL query against the environment's graph. Authenticated automatically.
516
603
 
517
604
  ### `environment.on(event, handler)`
605
+
518
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.
519
607
 
520
608
  ### `Environment.toGraphPath(className, id)`
609
+
521
610
  Convert a class name + real-world ID to a unique graph path (`{className}_{id}`).
522
611
 
523
612
  ### `Environment.extractIdFromGraphPath(graphPath, className)`
613
+
524
614
  Extract the real-world ID from a graph path by stripping the class prefix.
525
615
 
526
616
  ## License
@@ -1,4 +1,4 @@
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';
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-CQFKsCTd.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, 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';
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-CQFKsCTd.js';
2
2
  import { GeneratedJobCodeIssue, HarnessControllerBudgets } from './agent-harness.js';
3
3
  import '@automerge/automerge';
4
4
  import '@automerge/automerge/slim';