@granular-software/sdk 0.4.29 → 0.4.31

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.
@@ -4649,27 +4649,27 @@ var Session = class {
4649
4649
  }
4650
4650
  async publishTools(tools, revision = "1.0.0") {
4651
4651
  throw new Error(
4652
- "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
4652
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4653
4653
  );
4654
4654
  }
4655
4655
  async publishEffect(effect) {
4656
4656
  throw new Error(
4657
- "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
4657
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
4658
4658
  );
4659
4659
  }
4660
4660
  async publishEffects(effects) {
4661
4661
  throw new Error(
4662
- "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
4662
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4663
4663
  );
4664
4664
  }
4665
4665
  async unpublishEffect(name) {
4666
4666
  throw new Error(
4667
- "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
4667
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
4668
4668
  );
4669
4669
  }
4670
4670
  async unpublishAllEffects() {
4671
4671
  throw new Error(
4672
- "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
4672
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
4673
4673
  );
4674
4674
  }
4675
4675
  /**
@@ -10955,17 +10955,8 @@ var searchableMetamodelPackage = defineMetamodelPackage({
10955
10955
  }
10956
10956
  },
10957
10957
  domain: {
10958
- applyToPropertyIR(propertyIR, propertySummary) {
10959
- if (String(propertySummary.type || "").toLowerCase() !== "string" || propertySummary.searchable === false) {
10960
- return propertyIR;
10961
- }
10962
- return {
10963
- ...propertyIR,
10964
- docs: [
10965
- ...propertyIR.docs,
10966
- propertySummary.searchablePhonetic ? "Searchable via `search` using FalkorDB full-text query syntax with phonetic matching enabled." : "Searchable via `search` using FalkorDB full-text query syntax."
10967
- ]
10968
- };
10958
+ applyToPropertyIR(propertyIR, _propertySummary) {
10959
+ return propertyIR;
10969
10960
  }
10970
10961
  }
10971
10962
  });
@@ -11579,17 +11570,40 @@ function buildEffectMetamodelMutations(toolPath, spec) {
11579
11570
 
11580
11571
  // src/client.ts
11581
11572
  var STANDARD_MODULES_OPERATIONS = [
11582
- { create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
11573
+ {
11574
+ create: "entity",
11575
+ has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } }
11576
+ },
11583
11577
  { create: "class", extends: "entity", has: {} },
11584
- { create: "user", extends: "entity", has: { email: { value: void 0 }, firstName: { value: void 0 }, lastName: { value: void 0 } } },
11585
- { create: "company", extends: "entity", has: { name: { value: void 0 }, website: { value: void 0 } } },
11578
+ {
11579
+ create: "user",
11580
+ extends: "entity",
11581
+ has: {
11582
+ email: { value: void 0 },
11583
+ firstName: { value: void 0 },
11584
+ lastName: { value: void 0 }
11585
+ }
11586
+ },
11587
+ {
11588
+ create: "company",
11589
+ extends: "entity",
11590
+ has: { name: { value: void 0 }, website: { value: void 0 } }
11591
+ },
11586
11592
  { create: "string", has: {} },
11587
11593
  { create: "number", has: {} },
11588
11594
  { create: "boolean", has: {} },
11589
- { create: "tool_parameter", has: { name: { value: void 0 }, type: { value: "string" }, description: { value: void 0 }, required: { value: false } } }
11595
+ {
11596
+ create: "tool_parameter",
11597
+ has: {
11598
+ name: { value: void 0 },
11599
+ type: { value: "string" },
11600
+ description: { value: void 0 },
11601
+ required: { value: false }
11602
+ }
11603
+ }
11590
11604
  ];
11591
11605
  var BUILTIN_MODULES = {
11592
- "standard_modules": STANDARD_MODULES_OPERATIONS
11606
+ standard_modules: STANDARD_MODULES_OPERATIONS
11593
11607
  };
11594
11608
  var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
11595
11609
  var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
@@ -11624,7 +11638,9 @@ function isRetryableLocalWorkerRestart(status, body, url) {
11624
11638
  }
11625
11639
  function isRetryableRecordObjectsError(error) {
11626
11640
  const message = error instanceof Error ? error.message : String(error);
11627
- return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(message);
11641
+ return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
11642
+ message
11643
+ );
11628
11644
  }
11629
11645
  function computeEffectKey2(effect) {
11630
11646
  const attachedClass = effect.className?.trim();
@@ -11677,6 +11693,22 @@ function normalizeHeapSnapshot(raw) {
11677
11693
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
11678
11694
  };
11679
11695
  }
11696
+ function deriveRuntimeBaseUrl(apiEndpoint) {
11697
+ try {
11698
+ const endpoint = new URL(apiEndpoint);
11699
+ const graphqlSuffix = "/orchestrator/graphql";
11700
+ if (endpoint.pathname.endsWith(graphqlSuffix)) {
11701
+ endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11702
+ } else if (endpoint.pathname.endsWith("/graphql")) {
11703
+ endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11704
+ }
11705
+ endpoint.search = "";
11706
+ endpoint.hash = "";
11707
+ return endpoint.toString().replace(/\/$/, "");
11708
+ } catch {
11709
+ return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11710
+ }
11711
+ }
11680
11712
  function normalizeSubject(subject) {
11681
11713
  const granularId = subject.granularId || subject.subjectId;
11682
11714
  const userId = subject.userId || subject.identityId || granularId;
@@ -11701,7 +11733,10 @@ function normalizeUser(user) {
11701
11733
  };
11702
11734
  }
11703
11735
  function normalizeEnvironmentData(environment) {
11704
- const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : { mode: "pinned", versionId: environment.versionId || environment.buildId });
11736
+ const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
11737
+ mode: "pinned",
11738
+ versionId: environment.versionId || environment.buildId
11739
+ });
11705
11740
  const environmentName = environment.environment || environment.envName || "prod";
11706
11741
  return {
11707
11742
  ...environment,
@@ -11713,12 +11748,13 @@ function normalizeEnvironmentData(environment) {
11713
11748
  tracking: environment.tracking || buildPolicy
11714
11749
  };
11715
11750
  }
11716
- var Environment = class extends Session {
11751
+ var Environment = class {
11752
+ granular;
11717
11753
  envData;
11718
11754
  _apiKey;
11719
11755
  _apiEndpoint;
11720
- constructor(client, envData, clientId, apiKey, apiEndpoint) {
11721
- super(client, clientId);
11756
+ constructor(granular, envData, apiKey, apiEndpoint) {
11757
+ this.granular = granular;
11722
11758
  this.envData = envData;
11723
11759
  this._apiKey = apiKey;
11724
11760
  this._apiEndpoint = apiEndpoint;
@@ -11759,35 +11795,126 @@ var Environment = class extends Session {
11759
11795
  get permissionProfileId() {
11760
11796
  return this.envData.permissionProfileId;
11761
11797
  }
11798
+ /** The current build policy backing this environment */
11799
+ get buildPolicy() {
11800
+ return this.envData.buildPolicy;
11801
+ }
11802
+ /** The current update state relative to the followed tag */
11803
+ get updateState() {
11804
+ return this.envData.updateState;
11805
+ }
11806
+ /** Convenience flag for whether this environment trails the current tag target */
11807
+ get isOutdated() {
11808
+ return this.envData.updateState === "update_available";
11809
+ }
11810
+ /** The followed tag name when this environment is tag-tracked */
11811
+ get tag() {
11812
+ return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
11813
+ }
11762
11814
  /** The GraphQL API endpoint URL */
11763
11815
  get apiEndpoint() {
11764
11816
  return this._apiEndpoint;
11765
11817
  }
11818
+ /** Internal auth token used for control-plane and runtime fallback requests */
11819
+ get authToken() {
11820
+ return this._apiKey;
11821
+ }
11822
+ /** Base runtime URL derived from the GraphQL endpoint */
11823
+ get runtimeBaseUrl() {
11824
+ return this.getRuntimeBaseUrl();
11825
+ }
11826
+ get sessions() {
11827
+ return {
11828
+ list: async (options) => this.listSessions(options?.status || "active"),
11829
+ create: async (options) => this.createSession(options),
11830
+ connect: async (sessionId, options) => this.connectSession(sessionId, options),
11831
+ reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
11832
+ close: async (sessionId, session) => this.closeSession(sessionId, session)
11833
+ };
11834
+ }
11835
+ get data() {
11836
+ return {
11837
+ record: async (record) => this.recordObject(record),
11838
+ recordMany: async (records, options) => this.recordObjects(records, options),
11839
+ import: async (records, options) => this.enqueueRecordImport(records, options),
11840
+ listImports: async (status) => this.listRecordImports(status),
11841
+ getImport: async (importId) => this.getRecordImport(importId),
11842
+ getImportSummary: async () => this.getRecordImportSummary(),
11843
+ cancelImport: async (importId) => this.cancelRecordImport(importId),
11844
+ getAwaitingCount: async () => this.getAwaitingRecordCount()
11845
+ };
11846
+ }
11847
+ get feedback() {
11848
+ return {
11849
+ list: async () => this.listFeedback()
11850
+ };
11851
+ }
11766
11852
  /**
11767
- * Return a plain JS snapshot of the synced session heap.
11768
- *
11769
- * The heap lives in the Automerge document, so this method does not perform
11770
- * any extra network roundtrip.
11853
+ * Sessionless environments do not own a live transport, so disconnecting the
11854
+ * environment handle itself is a no-op. This keeps the public surface
11855
+ * symmetric with `EnvironmentSession.disconnect()` and lets callers always
11856
+ * clean up safely without tracking whether they currently hold an environment
11857
+ * or a session.
11771
11858
  */
11772
- getHeap() {
11773
- const doc = this.document;
11774
- return normalizeHeapSnapshot(doc?.heap);
11859
+ async disconnect() {
11775
11860
  }
11776
- getRuntimeBaseUrl() {
11777
- try {
11778
- const endpoint = new URL(this._apiEndpoint);
11779
- const graphqlSuffix = "/orchestrator/graphql";
11780
- if (endpoint.pathname.endsWith(graphqlSuffix)) {
11781
- endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11782
- } else if (endpoint.pathname.endsWith("/graphql")) {
11783
- endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11784
- }
11785
- endpoint.search = "";
11786
- endpoint.hash = "";
11787
- return endpoint.toString().replace(/\/$/, "");
11788
- } catch {
11789
- return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11861
+ async listSessions(status = "active") {
11862
+ if (status === "all") {
11863
+ const [active, closed] = await Promise.all([
11864
+ this.granular.listOpenSessions({ environmentId: this.environmentId }),
11865
+ this.granular.listClosedSessions({ environmentId: this.environmentId })
11866
+ ]);
11867
+ return [...active, ...closed].sort(
11868
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
11869
+ );
11870
+ }
11871
+ return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
11872
+ }
11873
+ async createSession(options) {
11874
+ return this.granular.createSession({
11875
+ environmentId: this.environmentId,
11876
+ clientId: options?.clientId,
11877
+ initialHeap: options?.initialHeap
11878
+ });
11879
+ }
11880
+ async connectSession(sessionId, options) {
11881
+ const session = await this.granular["connectSession"]({
11882
+ sessionId,
11883
+ clientId: options?.clientId
11884
+ });
11885
+ if (session.environmentId !== this.environmentId) {
11886
+ await session.disconnect().catch(() => {
11887
+ session.disconnectTransport();
11888
+ });
11889
+ throw new Error(
11890
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11891
+ );
11790
11892
  }
11893
+ return session;
11894
+ }
11895
+ async reopenSession(sessionId, options) {
11896
+ const session = await this.granular.reopenSession(sessionId, {
11897
+ clientId: options?.clientId
11898
+ });
11899
+ if (session.environmentId !== this.environmentId) {
11900
+ await session.disconnect().catch(() => {
11901
+ session.disconnectTransport();
11902
+ });
11903
+ throw new Error(
11904
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11905
+ );
11906
+ }
11907
+ return session;
11908
+ }
11909
+ async closeSession(sessionId, session) {
11910
+ await this.granular.closeSession(sessionId, session);
11911
+ }
11912
+ async listFeedback() {
11913
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
11914
+ return Array.isArray(response.items) ? response.items : [];
11915
+ }
11916
+ getRuntimeBaseUrl() {
11917
+ return deriveRuntimeBaseUrl(this._apiEndpoint);
11791
11918
  }
11792
11919
  async controlPlaneRequest(path2, options = {}) {
11793
11920
  const runtimeBase = this.getRuntimeBaseUrl();
@@ -11795,94 +11922,19 @@ var Environment = class extends Session {
11795
11922
  const response = await fetch(url, {
11796
11923
  ...options,
11797
11924
  headers: {
11798
- "Authorization": `Bearer ${this._apiKey}`,
11925
+ Authorization: `Bearer ${this._apiKey}`,
11799
11926
  "Content-Type": "application/json",
11800
- "Connection": "close",
11927
+ Connection: "close",
11801
11928
  ...options.headers
11802
11929
  }
11803
11930
  });
11804
11931
  if (!response.ok) {
11805
- throw new Error(`Control Plane API Error (${response.status}): ${await response.text()}`);
11932
+ throw new Error(
11933
+ `Control Plane API Error (${response.status}): ${await response.text()}`
11934
+ );
11806
11935
  }
11807
11936
  return response.json();
11808
11937
  }
11809
- /**
11810
- * Close the session and disconnect from the sandbox.
11811
- *
11812
- * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
11813
- * to the runtime goodbye endpoint if no definitive WS-side runtime notify
11814
- * acknowledgement was observed.
11815
- */
11816
- async disconnect() {
11817
- let wsNotifiedRuntime = false;
11818
- try {
11819
- const goodbye = await this.rpc("client.goodbye", {
11820
- timestamp: Date.now()
11821
- });
11822
- wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
11823
- } catch {
11824
- wsNotifiedRuntime = false;
11825
- }
11826
- if (!wsNotifiedRuntime) {
11827
- try {
11828
- const runtimeBase = this.getRuntimeBaseUrl();
11829
- await fetch(
11830
- `${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
11831
- {
11832
- method: "POST",
11833
- headers: {
11834
- "Content-Type": "application/json",
11835
- "Authorization": `Bearer ${this._apiKey}`,
11836
- "Connection": "close"
11837
- },
11838
- body: JSON.stringify({
11839
- reason: "sdk_disconnect_http_fallback",
11840
- sessionId: this.client.currentSessionId
11841
- })
11842
- }
11843
- );
11844
- } catch {
11845
- }
11846
- }
11847
- this.client.disconnect();
11848
- }
11849
- // ==================== GRAPH CONTAINER READINESS ====================
11850
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
11851
- graphContainerStatus = null;
11852
- /**
11853
- * Check if the graph container is ready and warm.
11854
- *
11855
- * Sends a lightweight heartbeat RPC to the Session DO which internally
11856
- * pings the FalkorDB container. The response includes `graphContainerStatus`,
11857
- * which is stored locally and emitted as a `readiness` event.
11858
- *
11859
- * Use this method to proactively warm the graph container before any
11860
- * GraphQL query that requires it, or to poll the container's state in
11861
- * the background.
11862
- *
11863
- * @returns The current graph container status object
11864
- *
11865
- * @example
11866
- * ```typescript
11867
- * const status = await env.checkReadiness();
11868
- * console.log(status.status); // 'hot' | 'warming' | 'unknown'
11869
- *
11870
- * // Or listen for live updates
11871
- * env.on('readiness', (status) => {
11872
- * console.log('Graph is now:', status.status);
11873
- * });
11874
- * ```
11875
- */
11876
- async checkReadiness() {
11877
- const result = await this.client.call("client.heartbeat", {});
11878
- const containerStatus = result?.graphContainerStatus ?? {
11879
- lastKeepAliveAt: Date.now(),
11880
- status: "unknown"
11881
- };
11882
- this.graphContainerStatus = containerStatus;
11883
- this.emit("readiness", containerStatus);
11884
- return containerStatus;
11885
- }
11886
11938
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
11887
11939
  /**
11888
11940
  * Convert a class name + real-world ID into a unique graph path.
@@ -11911,14 +11963,14 @@ var Environment = class extends Session {
11911
11963
  }
11912
11964
  /**
11913
11965
  * Execute a GraphQL query against the environment's graph.
11914
- *
11966
+ *
11915
11967
  * The query uses the Granular graph query language (based on Cypher/GraphQL).
11916
11968
  * Authentication is handled automatically using the SDK's API key.
11917
- *
11969
+ *
11918
11970
  * @param query - The GraphQL query string
11919
11971
  * @param variables - Optional variables for the query
11920
11972
  * @returns The query result data
11921
- *
11973
+ *
11922
11974
  * @example
11923
11975
  * ```typescript
11924
11976
  * // Read the workspace
@@ -11926,7 +11978,7 @@ var Environment = class extends Session {
11926
11978
  * `query { model(path: "workspace") { path label submodels { path label } } }`
11927
11979
  * );
11928
11980
  * console.log(result.data);
11929
- *
11981
+ *
11930
11982
  * // Create a model
11931
11983
  * const created = await env.graphql(
11932
11984
  * `mutation { at(path: "workspace") { create_submodel(subpath: "my_node", label: "My Node", prototype: "Model") { model { path label } } } }`
@@ -11938,7 +11990,7 @@ var Environment = class extends Session {
11938
11990
  method: "POST",
11939
11991
  headers: {
11940
11992
  "Content-Type": "application/json",
11941
- "Authorization": `Bearer ${this._apiKey}`
11993
+ Authorization: `Bearer ${this._apiKey}`
11942
11994
  },
11943
11995
  body: JSON.stringify({
11944
11996
  environmentId: this.environmentId,
@@ -11955,10 +12007,10 @@ var Environment = class extends Session {
11955
12007
  // ==================== RELATIONSHIP METHODS ====================
11956
12008
  /**
11957
12009
  * Define a relationship between two model types.
11958
- *
12010
+ *
11959
12011
  * Creates both submodels (if they don't exist) and links them with
11960
12012
  * a RelationshipDef node that encodes cardinality.
11961
- *
12013
+ *
11962
12014
  * @example
11963
12015
  * ```typescript
11964
12016
  * // Author has many Books, Book has one Author
@@ -12018,10 +12070,10 @@ var Environment = class extends Session {
12018
12070
  }
12019
12071
  /**
12020
12072
  * Get all relationships for a model type.
12021
- *
12073
+ *
12022
12074
  * @param modelPath - The model type path (e.g., "author")
12023
12075
  * @returns Array of relationships from this model's perspective
12024
- *
12076
+ *
12025
12077
  * @example
12026
12078
  * ```typescript
12027
12079
  * const rels = await env.getRelationships('author');
@@ -12054,18 +12106,18 @@ var Environment = class extends Session {
12054
12106
  }
12055
12107
  /**
12056
12108
  * Attach a target model to a relationship submodel.
12057
- *
12109
+ *
12058
12110
  * Handles cardinality automatically:
12059
12111
  * - "One" side: sets/replaces the reference
12060
12112
  * - "Many" side: adds the target to the collection
12061
- *
12113
+ *
12062
12114
  * If the target model doesn't exist, it's created as an instance of the foreign type.
12063
12115
  * Bidirectional sync is automatic.
12064
- *
12116
+ *
12065
12117
  * @param modelPath - The model instance path (e.g., "tolkien")
12066
12118
  * @param submodelPath - The relationship submodel (e.g., "books")
12067
12119
  * @param targetPath - The target model to attach (e.g., "lord_of_the_rings")
12068
- *
12120
+ *
12069
12121
  * @example
12070
12122
  * ```typescript
12071
12123
  * // Attach a book to an author (many side)
@@ -12092,18 +12144,18 @@ var Environment = class extends Session {
12092
12144
  }
12093
12145
  /**
12094
12146
  * Detach a target model from a relationship submodel.
12095
- *
12147
+ *
12096
12148
  * Handles bidirectional cleanup automatically.
12097
- *
12149
+ *
12098
12150
  * @param modelPath - The model instance path
12099
12151
  * @param submodelPath - The relationship submodel
12100
12152
  * @param targetPath - The target to detach (optional for "one" side; omit on "many" side to detach all)
12101
- *
12153
+ *
12102
12154
  * @example
12103
12155
  * ```typescript
12104
12156
  * // Detach a specific book
12105
12157
  * await env.detach('tolkien', 'books', 'lord_of_the_rings');
12106
- *
12158
+ *
12107
12159
  * // Detach all books
12108
12160
  * await env.detach('tolkien', 'books');
12109
12161
  * ```
@@ -12127,11 +12179,11 @@ var Environment = class extends Session {
12127
12179
  }
12128
12180
  /**
12129
12181
  * List all related models through a relationship submodel.
12130
- *
12182
+ *
12131
12183
  * @param modelPath - The model instance path
12132
12184
  * @param submodelPath - The relationship submodel
12133
12185
  * @returns Array of related model references
12134
- *
12186
+ *
12135
12187
  * @example
12136
12188
  * ```typescript
12137
12189
  * const books = await env.listRelated('tolkien', 'books');
@@ -12155,14 +12207,14 @@ var Environment = class extends Session {
12155
12207
  }
12156
12208
  /**
12157
12209
  * Apply a manifest to the current environment's graph.
12158
- *
12210
+ *
12159
12211
  * Translates each manifest operation into GraphQL mutations and executes them
12160
12212
  * in order. This is the core mechanism for creating classes, fields, and
12161
12213
  * relationships from a declarative manifest.
12162
- *
12214
+ *
12163
12215
  * @param manifest - The manifest content to apply
12164
12216
  * @returns Summary of applied operations
12165
- *
12217
+ *
12166
12218
  * @example
12167
12219
  * ```typescript
12168
12220
  * await environment.applyManifest({
@@ -12200,12 +12252,16 @@ var Environment = class extends Session {
12200
12252
  applied++;
12201
12253
  } catch (err) {
12202
12254
  if (!err.message?.includes("already exists")) {
12203
- errors.push(`Import ${imp.name} operation failed: ${err.message}`);
12255
+ errors.push(
12256
+ `Import ${imp.name} operation failed: ${err.message}`
12257
+ );
12204
12258
  }
12205
12259
  }
12206
12260
  }
12207
12261
  } else {
12208
- errors.push(`Unknown module: "${imp.name}" (only built-in modules are supported)`);
12262
+ errors.push(
12263
+ `Unknown module: "${imp.name}" (only built-in modules are supported)`
12264
+ );
12209
12265
  }
12210
12266
  }
12211
12267
  }
@@ -12289,7 +12345,10 @@ var Environment = class extends Session {
12289
12345
  }
12290
12346
  }
12291
12347
  async _applyEffectMetamodels(toolPath, metamodels) {
12292
- for (const mutation of buildEffectMetamodelMutations(toolPath, metamodels)) {
12348
+ for (const mutation of buildEffectMetamodelMutations(
12349
+ toolPath,
12350
+ metamodels
12351
+ )) {
12293
12352
  await this._runGraphql(mutation.query, mutation.label);
12294
12353
  }
12295
12354
  }
@@ -12338,7 +12397,9 @@ var Environment = class extends Session {
12338
12397
  }
12339
12398
  if (eventType.payloadSchema?.properties) {
12340
12399
  const fieldSpecs = {};
12341
- for (const [propName, propSchema] of Object.entries(eventType.payloadSchema.properties)) {
12400
+ for (const [propName, propSchema] of Object.entries(
12401
+ eventType.payloadSchema.properties
12402
+ )) {
12342
12403
  const schema = propSchema;
12343
12404
  fieldSpecs[propName] = {
12344
12405
  type: schema.type ?? "string",
@@ -12621,7 +12682,9 @@ var Environment = class extends Session {
12621
12682
  const wave = plans.slice(waveStart, waveStart + concurrency);
12622
12683
  await Promise.all(
12623
12684
  wave.map(async (plan) => {
12624
- const { items, durationMs } = await this.executeRecordObjectsChunk(plan.slice);
12685
+ const { items, durationMs } = await this.executeRecordObjectsChunk(
12686
+ plan.slice
12687
+ );
12625
12688
  if (items.length !== plan.slice.length) {
12626
12689
  throw new Error(
12627
12690
  `recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
@@ -12651,13 +12714,10 @@ var Environment = class extends Session {
12651
12714
  let lastError;
12652
12715
  for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
12653
12716
  try {
12654
- const response = await this.controlPlaneRequest(
12655
- `/control/environments/${this.environmentId}/records/batch`,
12656
- {
12657
- method: "POST",
12658
- body: JSON.stringify({ records: chunk })
12659
- }
12660
- );
12717
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/records/batch`, {
12718
+ method: "POST",
12719
+ body: JSON.stringify({ records: chunk })
12720
+ });
12661
12721
  const items = Array.isArray(response.items) ? response.items : [];
12662
12722
  return { items, durationMs: Date.now() - wallStart };
12663
12723
  } catch (error) {
@@ -12720,7 +12780,9 @@ var Environment = class extends Session {
12720
12780
  * Fetch a single record import by id.
12721
12781
  */
12722
12782
  async getRecordImport(importId) {
12723
- return this.controlPlaneRequest(`/control/record-imports/${importId}`);
12783
+ return this.controlPlaneRequest(
12784
+ `/control/record-imports/${importId}`
12785
+ );
12724
12786
  }
12725
12787
  /**
12726
12788
  * Cancel a queued/background record import.
@@ -12733,36 +12795,186 @@ var Environment = class extends Session {
12733
12795
  }
12734
12796
  );
12735
12797
  }
12736
- // ==================== PUBLISH TOOLS ====================
12798
+ };
12799
+ var EnvironmentSession = class extends Session {
12800
+ environment;
12801
+ /** The last known graph container status, updated by checkReadiness() or on heartbeat */
12802
+ graphContainerStatus = null;
12803
+ constructor(client, environment, clientId) {
12804
+ super(client, clientId);
12805
+ this.environment = environment;
12806
+ }
12807
+ get environmentId() {
12808
+ return this.environment.environmentId;
12809
+ }
12810
+ get sandboxId() {
12811
+ return this.environment.sandboxId;
12812
+ }
12813
+ get ontologyId() {
12814
+ return this.environment.ontologyId;
12815
+ }
12816
+ get subjectId() {
12817
+ return this.environment.subjectId;
12818
+ }
12819
+ get envName() {
12820
+ return this.environment.envName;
12821
+ }
12822
+ get versionId() {
12823
+ return this.environment.versionId;
12824
+ }
12825
+ get granularId() {
12826
+ return this.environment.granularId;
12827
+ }
12828
+ get permissionProfileId() {
12829
+ return this.environment.permissionProfileId;
12830
+ }
12831
+ get apiEndpoint() {
12832
+ return this.environment.apiEndpoint;
12833
+ }
12834
+ get data() {
12835
+ return this.environment.data;
12836
+ }
12837
+ get feedback() {
12838
+ return this.environment.feedback;
12839
+ }
12737
12840
  /**
12738
- * Removed: environment-scoped effect publication is no longer supported.
12841
+ * Return a plain JS snapshot of the synced session heap.
12739
12842
  */
12740
- async publishTools(tools, revision = "1.0.0") {
12741
- return super.publishTools(tools, revision);
12843
+ getHeap() {
12844
+ const doc = this.document;
12845
+ return normalizeHeapSnapshot(doc?.heap);
12846
+ }
12847
+ async graphql(query, variables) {
12848
+ return this.environment.graphql(query, variables);
12849
+ }
12850
+ async defineRelationship(options) {
12851
+ return this.environment.defineRelationship(options);
12852
+ }
12853
+ async getRelationships(modelPath) {
12854
+ return this.environment.getRelationships(modelPath);
12855
+ }
12856
+ async attach(modelPath, submodelPath, targetPath) {
12857
+ return this.environment.attach(modelPath, submodelPath, targetPath);
12858
+ }
12859
+ async detach(modelPath, submodelPath, targetPath) {
12860
+ return this.environment.detach(modelPath, submodelPath, targetPath);
12861
+ }
12862
+ async listRelated(modelPath, submodelPath) {
12863
+ return this.environment.listRelated(modelPath, submodelPath);
12864
+ }
12865
+ async applyManifest(manifest) {
12866
+ return this.environment.applyManifest(manifest);
12867
+ }
12868
+ async recordObject(options) {
12869
+ return this.environment.recordObject(options);
12870
+ }
12871
+ async recordObjects(records, options) {
12872
+ return this.environment.recordObjects(records, options);
12873
+ }
12874
+ async enqueueRecordImport(records, options = {}) {
12875
+ return this.environment.enqueueRecordImport(records, options);
12876
+ }
12877
+ async listRecordImports(status) {
12878
+ return this.environment.listRecordImports(status);
12879
+ }
12880
+ async getRecordImportSummary() {
12881
+ return this.environment.getRecordImportSummary();
12882
+ }
12883
+ async getAwaitingRecordCount() {
12884
+ return this.environment.getAwaitingRecordCount();
12885
+ }
12886
+ async getRecordImport(importId) {
12887
+ return this.environment.getRecordImport(importId);
12888
+ }
12889
+ async cancelRecordImport(importId) {
12890
+ return this.environment.cancelRecordImport(importId);
12891
+ }
12892
+ async listFeedback() {
12893
+ return this.environment.listFeedback();
12742
12894
  }
12743
12895
  /**
12744
- * Removed: environment-scoped effect publication is no longer supported.
12896
+ * Close the session and disconnect from the sandbox.
12897
+ *
12898
+ * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
12899
+ * to the runtime goodbye endpoint if no definitive WS-side runtime notify
12900
+ * acknowledgement was observed.
12745
12901
  */
12746
- async publishEffect(effect) {
12747
- return super.publishEffect(effect);
12902
+ async disconnect() {
12903
+ let wsNotifiedRuntime = false;
12904
+ try {
12905
+ const goodbye = await this.rpc(
12906
+ "client.goodbye",
12907
+ {
12908
+ timestamp: Date.now()
12909
+ }
12910
+ );
12911
+ wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
12912
+ } catch {
12913
+ wsNotifiedRuntime = false;
12914
+ }
12915
+ if (!wsNotifiedRuntime) {
12916
+ try {
12917
+ await fetch(
12918
+ `${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
12919
+ {
12920
+ method: "POST",
12921
+ headers: {
12922
+ "Content-Type": "application/json",
12923
+ Authorization: `Bearer ${this.environment.authToken}`,
12924
+ Connection: "close"
12925
+ },
12926
+ body: JSON.stringify({
12927
+ reason: "sdk_disconnect_http_fallback",
12928
+ sessionId: this.client.currentSessionId
12929
+ })
12930
+ }
12931
+ );
12932
+ } catch {
12933
+ }
12934
+ }
12935
+ this.client.disconnect();
12748
12936
  }
12749
12937
  /**
12750
- * Removed: environment-scoped effect publication is no longer supported.
12938
+ * Close only the socket transport without sending `client.goodbye`.
12751
12939
  */
12752
- async publishEffects(effects) {
12753
- return super.publishEffects(effects);
12940
+ disconnectTransport() {
12941
+ this.client.disconnect();
12754
12942
  }
12755
12943
  /**
12756
- * Removed: environment-scoped effect publication is no longer supported.
12944
+ * Backwards-compatible alias for `disconnect()`.
12757
12945
  */
12758
- async unpublishEffect(name) {
12759
- return super.unpublishEffect(name);
12946
+ async close() {
12947
+ await this.disconnect();
12760
12948
  }
12761
12949
  /**
12762
- * Removed: environment-scoped effect publication is no longer supported.
12950
+ * Check if the graph container is ready and warm.
12763
12951
  */
12764
- async unpublishAllEffects() {
12765
- return super.unpublishAllEffects();
12952
+ async checkReadiness() {
12953
+ const result = await this.client.call("client.heartbeat", {});
12954
+ const containerStatus = result?.graphContainerStatus ?? {
12955
+ lastKeepAliveAt: Date.now(),
12956
+ status: "unknown"
12957
+ };
12958
+ this.graphContainerStatus = containerStatus;
12959
+ this.emit("readiness", containerStatus);
12960
+ return containerStatus;
12961
+ }
12962
+ };
12963
+ var OntologyHandle = class {
12964
+ granular;
12965
+ ontologyNameOrId;
12966
+ constructor(granular, ontologyNameOrId) {
12967
+ this.granular = granular;
12968
+ this.ontologyNameOrId = ontologyNameOrId;
12969
+ }
12970
+ get effects() {
12971
+ return {
12972
+ register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
12973
+ registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
12974
+ unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
12975
+ clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
12976
+ disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
12977
+ };
12766
12978
  }
12767
12979
  };
12768
12980
  var Granular = class _Granular {
@@ -12787,7 +12999,9 @@ var Granular = class _Granular {
12787
12999
  constructor(options) {
12788
13000
  const auth = options.token ?? options.apiKey;
12789
13001
  if (!auth) {
12790
- throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
13002
+ throw new Error(
13003
+ "Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options."
13004
+ );
12791
13005
  }
12792
13006
  this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
12793
13007
  this.apiKey = resolveAuthTokenForApiUrl(auth, this.apiUrl);
@@ -12797,12 +13011,18 @@ var Granular = class _Granular {
12797
13011
  this.onReconnectError = options.onReconnectError;
12798
13012
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
12799
13013
  }
13014
+ /**
13015
+ * Return an ontology-scoped handle for effects and other ontology-level APIs.
13016
+ */
13017
+ ontology(ontologyNameOrId) {
13018
+ return new OntologyHandle(this, ontologyNameOrId);
13019
+ }
12800
13020
  /**
12801
13021
  * Records/upserts a user and prepares them for sandbox connections
12802
- *
13022
+ *
12803
13023
  * @param options - User options
12804
13024
  * @returns The recorded user with both `userId` and `granularId`
12805
- *
13025
+ *
12806
13026
  * @example
12807
13027
  * ```typescript
12808
13028
  * const user = await granular.recordUser({
@@ -12813,14 +13033,16 @@ var Granular = class _Granular {
12813
13033
  * ```
12814
13034
  */
12815
13035
  async recordUser(options) {
12816
- const subject = normalizeSubject(await this.request("/control/subjects", {
12817
- method: "POST",
12818
- body: JSON.stringify({
12819
- identityId: options.userId,
12820
- name: options.name,
12821
- email: options.email
13036
+ const subject = normalizeSubject(
13037
+ await this.request("/control/subjects", {
13038
+ method: "POST",
13039
+ body: JSON.stringify({
13040
+ identityId: options.userId,
13041
+ name: options.name,
13042
+ email: options.email
13043
+ })
12822
13044
  })
12823
- }));
13045
+ );
12824
13046
  return normalizeUser({
12825
13047
  granularId: subject.granularId,
12826
13048
  userId: options.userId,
@@ -12831,7 +13053,23 @@ var Granular = class _Granular {
12831
13053
  permissions: options.permissions || []
12832
13054
  });
12833
13055
  }
13056
+ /**
13057
+ * Alias for `recordUser()` with user-facing naming that matches upsert semantics.
13058
+ */
13059
+ async upsertUser(options) {
13060
+ return this.recordUser(options);
13061
+ }
12834
13062
  async resolveConnectUser(options) {
13063
+ const providedIdentityCount = [
13064
+ Boolean(options.user),
13065
+ Boolean(options.userId),
13066
+ Boolean(options.granularId)
13067
+ ].filter(Boolean).length;
13068
+ if (providedIdentityCount !== 1) {
13069
+ throw new Error(
13070
+ "openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
13071
+ );
13072
+ }
12835
13073
  if (options.user) {
12836
13074
  const user = normalizeUser(options.user);
12837
13075
  return {
@@ -12867,76 +13105,141 @@ var Granular = class _Granular {
12867
13105
  permissions: options.permissions || []
12868
13106
  };
12869
13107
  }
12870
- throw new Error("connect() requires either userId, granularId, or a user object returned by recordUser().");
13108
+ throw new Error(
13109
+ "openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
13110
+ );
12871
13111
  }
12872
13112
  /**
12873
- * Connect to an ontology environment and establish a real-time session.
12874
- *
12875
- * Effects are registered at the sandbox level via `granular.registerEffect()`
12876
- * or `granular.registerEffects()`. Sessions pick up live availability from
12877
- * the sandbox registry automatically.
12878
- *
12879
- * @param options - Connection options
12880
- * @returns An active environment session
12881
- *
13113
+ * Open or resolve an ontology environment for one user without opening a session.
13114
+ *
12882
13115
  * @example
12883
13116
  * ```typescript
12884
- * const environment = await granular.connect({
13117
+ * const environment = await granular.openEnvironment({
12885
13118
  * ontology: 'my-ontology',
12886
- * environment: 'dev',
13119
+ * tag: 'dev',
12887
13120
  * userId: 'user_123',
12888
13121
  * permissions: ['agent'],
12889
13122
  * });
12890
- *
12891
- * await granular.registerEffect('my-sandbox', {
12892
- * name: 'greet',
12893
- * description: 'Say hello',
12894
- * inputSchema: { type: 'object', properties: {} },
12895
- * handler: async () => 'Hello!',
13123
+ *
13124
+ * await environment.data.record({
13125
+ * className: 'customer',
13126
+ * id: 'acme',
13127
+ * fields: { name: 'Acme' },
12896
13128
  * });
12897
- *
12898
- * // Submit job
12899
- * const job = await environment.submitJob(`
12900
- * import { tools } from './sandbox-tools';
12901
- * return await tools.greet({});
12902
- * `);
12903
- *
12904
- * console.log(await job.result); // 'Hello!'
13129
+ *
13130
+ * const session = await environment.sessions.create();
13131
+ * const job = await session.submitJob(`return "hello";`);
13132
+ * console.log(await job.result);
12905
13133
  * ```
12906
- */
13134
+ */
13135
+ async openEnvironment(options) {
13136
+ const envData = await this.resolveOpenEnvironmentData(
13137
+ options,
13138
+ "openEnvironment"
13139
+ );
13140
+ return this.bindEnvironmentHandle(envData);
13141
+ }
13142
+ /**
13143
+ * Deprecated compatibility alias for `openEnvironment()`.
13144
+ *
13145
+ * `connect()` no longer opens a runtime session automatically.
13146
+ */
12907
13147
  async connect(options) {
12908
- const clientId = options.clientId || `client_${Date.now()}`;
13148
+ return this.openEnvironment({
13149
+ ...options,
13150
+ tag: this.resolveRequestedTag(options, "connect"),
13151
+ permissions: options.permissions || options.user?.permissions || []
13152
+ });
13153
+ }
13154
+ resolveRequestedTag(options, methodName) {
13155
+ const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
13156
+ if (!tag) {
13157
+ throw new Error(`${methodName}() requires \`tag\`.`);
13158
+ }
13159
+ return tag;
13160
+ }
13161
+ buildManagedEnvironmentName(tag, versionId) {
13162
+ return `__sdk__${tag}__${versionId}`;
13163
+ }
13164
+ matchesTagTrackedEnvironment(environment, tagName, tagId) {
13165
+ const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
13166
+ return environment.tagId === tagId || environmentTagName === tagName || environment.environment === tagName || environment.envName === tagName || environment.environment === this.buildManagedEnvironmentName(tagName, environment.versionId) || environment.envName === this.buildManagedEnvironmentName(tagName, environment.versionId);
13167
+ }
13168
+ sortEnvironmentsByRecency(environments) {
13169
+ return [...environments].sort(
13170
+ (left, right) => right.updatedAt - left.updatedAt
13171
+ );
13172
+ }
13173
+ async resolveOpenEnvironmentData(options, methodName) {
12909
13174
  const ontology = options.ontology;
12910
13175
  if (!ontology) {
12911
- throw new Error("connect() requires `ontology`.");
13176
+ throw new Error(`${methodName}() requires \`ontology\`.`);
12912
13177
  }
12913
- const environmentName = options.environment;
12914
- if (!environmentName) {
12915
- throw new Error("connect() requires `environment`.");
13178
+ const tagName = options.tag?.trim();
13179
+ if (!tagName) {
13180
+ throw new Error(`${methodName}() requires \`tag\`.`);
12916
13181
  }
12917
- const tagName = options.tagName?.trim() || void 0;
12918
13182
  const user = await this.resolveConnectUser(options);
13183
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
13184
+ throw new Error(
13185
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
13186
+ );
13187
+ }
12919
13188
  const sandbox = await this.findOrCreateSandbox(ontology);
12920
13189
  for (const profileName of user.permissions) {
12921
- const profileId = await this.ensurePermissionProfile(sandbox.sandboxId, profileName);
12922
- await this.ensureAssignment(user.granularId, sandbox.sandboxId, profileId);
13190
+ const profileId = await this.ensurePermissionProfile(
13191
+ sandbox.sandboxId,
13192
+ profileName
13193
+ );
13194
+ await this.ensureAssignment(
13195
+ user.granularId,
13196
+ sandbox.sandboxId,
13197
+ profileId
13198
+ );
13199
+ }
13200
+ const tags = await this.request(
13201
+ `/control/sandboxes/${sandbox.sandboxId}/tags`
13202
+ );
13203
+ const tag = (tags.items || []).find(
13204
+ (candidate) => Boolean(candidate?.name === tagName)
13205
+ );
13206
+ if (!tag) {
13207
+ throw new Error(
13208
+ `Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
13209
+ );
13210
+ }
13211
+ const targetVersionId = tag.targetVersionId || tag.targetBuildId;
13212
+ if (!targetVersionId) {
13213
+ throw new Error(
13214
+ `Tag "${tagName}" does not currently point to a build/version.`
13215
+ );
13216
+ }
13217
+ const allEnvironments = await this.environments.list(sandbox.sandboxId);
13218
+ const userEnvironments = allEnvironments.filter(
13219
+ (environment) => environment.subjectId === user.granularId
13220
+ );
13221
+ const currentMatches = this.sortEnvironmentsByRecency(
13222
+ userEnvironments.filter(
13223
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
13224
+ )
13225
+ );
13226
+ if (currentMatches.length > 0) {
13227
+ return currentMatches[0];
13228
+ }
13229
+ const outdatedMatches = this.sortEnvironmentsByRecency(
13230
+ userEnvironments.filter(
13231
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
13232
+ )
13233
+ );
13234
+ if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13235
+ return outdatedMatches[0];
12923
13236
  }
12924
- const envData = await this.environments.create(sandbox.sandboxId, {
13237
+ return this.environments.create(sandbox.sandboxId, {
12925
13238
  subjectId: user.granularId,
12926
- environment: environmentName,
12927
- tagName,
13239
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13240
+ tagId: tag.tagId,
12928
13241
  permissionProfileId: null
12929
13242
  });
12930
- await this.activateEnvironment(envData.environmentId);
12931
- const session = await this.request("/ws/sessions", {
12932
- method: "POST",
12933
- body: JSON.stringify({
12934
- environmentId: envData.environmentId,
12935
- clientId,
12936
- initialHeap: options.initialHeap
12937
- })
12938
- });
12939
- return this.bindWebSocketEnvironment(envData, clientId, session);
12940
13243
  }
12941
13244
  /**
12942
13245
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -12971,7 +13274,9 @@ var Granular = class _Granular {
12971
13274
  createdAt: _Granular.coerceIsoDate(row.createdAt ?? row.created_at),
12972
13275
  lastSeenAt: _Granular.coerceIsoDate(row.lastSeenAt ?? row.last_seen_at),
12973
13276
  summary: row.summary != null ? String(row.summary) : null,
12974
- summaryUpdatedAt: row.summaryUpdatedAt != null || row.summary_updated_at != null ? _Granular.coerceIsoDate(row.summaryUpdatedAt ?? row.summary_updated_at) : null,
13277
+ summaryUpdatedAt: row.summaryUpdatedAt != null || row.summary_updated_at != null ? _Granular.coerceIsoDate(
13278
+ row.summaryUpdatedAt ?? row.summary_updated_at
13279
+ ) : null,
12975
13280
  subjectId: row.subjectId != null ? String(row.subjectId) : null,
12976
13281
  jobCount: typeof row.jobCount === "number" ? row.jobCount : void 0,
12977
13282
  toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
@@ -12997,6 +13302,7 @@ var Granular = class _Granular {
12997
13302
  const clientId = options.clientId || `client_${Date.now()}`;
12998
13303
  await this.activateEnvironment(options.environmentId);
12999
13304
  const envData = await this.environments.get(options.environmentId);
13305
+ const environment = this.bindEnvironmentHandle(envData);
13000
13306
  const session = await this.request("/ws/sessions", {
13001
13307
  method: "POST",
13002
13308
  body: JSON.stringify({
@@ -13005,7 +13311,7 @@ var Granular = class _Granular {
13005
13311
  initialHeap: options.initialHeap
13006
13312
  })
13007
13313
  });
13008
- return this.bindWebSocketEnvironment(envData, clientId, session);
13314
+ return this.bindWebSocketEnvironmentSession(environment, clientId, session);
13009
13315
  }
13010
13316
  /**
13011
13317
  * Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
@@ -13017,7 +13323,8 @@ var Granular = class _Granular {
13017
13323
  body: JSON.stringify({})
13018
13324
  });
13019
13325
  const envData = await this.environments.get(minted.environmentId);
13020
- return this.bindWebSocketEnvironment(envData, clientId, minted);
13326
+ const environment = this.bindEnvironmentHandle(envData);
13327
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
13021
13328
  }
13022
13329
  /**
13023
13330
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
@@ -13049,7 +13356,11 @@ var Granular = class _Granular {
13049
13356
  });
13050
13357
  return this.connectSession({ sessionId, clientId: options?.clientId });
13051
13358
  }
13052
- async bindWebSocketEnvironment(envData, clientId, session) {
13359
+ bindEnvironmentHandle(envData) {
13360
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13361
+ return new Environment(this, envData, this.apiKey, graphqlEndpoint);
13362
+ }
13363
+ async bindWebSocketEnvironmentSession(environment, clientId, session) {
13053
13364
  const client = new WSClient({
13054
13365
  url: session.wsUrl,
13055
13366
  sessionId: session.sessionId,
@@ -13060,10 +13371,13 @@ var Granular = class _Granular {
13060
13371
  onReconnectError: this.onReconnectError
13061
13372
  });
13062
13373
  await client.connect();
13063
- const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13064
- const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
13065
- await environment.hello();
13066
- return environment;
13374
+ const environmentSession = new EnvironmentSession(
13375
+ client,
13376
+ environment,
13377
+ clientId
13378
+ );
13379
+ await environmentSession.hello();
13380
+ return environmentSession;
13067
13381
  }
13068
13382
  async activateEnvironment(environmentId) {
13069
13383
  await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
@@ -13095,14 +13409,18 @@ var Granular = class _Granular {
13095
13409
  };
13096
13410
  }
13097
13411
  async publishSandboxEffectCatalog(host) {
13098
- const effects = Array.from(this.getSandboxEffectMap(host.sandboxId).values()).map(
13099
- (effect) => this.serializeEffect(effect)
13100
- );
13101
- const result = await host.wsClient.call("effects.publishCatalog", { effects });
13412
+ const effects = Array.from(
13413
+ this.getSandboxEffectMap(host.sandboxId).values()
13414
+ ).map((effect) => this.serializeEffect(effect));
13415
+ const result = await host.wsClient.call("effects.publishCatalog", {
13416
+ effects
13417
+ });
13102
13418
  const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
13103
13419
  const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
13104
13420
  if (acceptedCount === 0 && rejected.length > 0) {
13105
- const detail = rejected.map((entry) => `${entry.name || "unknown"}: ${entry.reason || "rejected"}`).join("; ");
13421
+ const detail = rejected.map(
13422
+ (entry) => `${entry.name || "unknown"}: ${entry.reason || "rejected"}`
13423
+ ).join("; ");
13106
13424
  throw new Error(
13107
13425
  `Failed to publish live effects for sandbox ${host.sandboxId}: ${detail}`
13108
13426
  );
@@ -13136,13 +13454,15 @@ var Granular = class _Granular {
13136
13454
  disconnectError
13137
13455
  );
13138
13456
  }
13139
- void this.ensureSandboxEffectHost(host.sandboxId).catch((reconnectError) => {
13140
- console.error(
13141
- `[Granular] Failed to recover effect host for sandbox ${host.sandboxId} after heartbeat failure:`,
13142
- reconnectError
13143
- );
13144
- console.error("[Granular] Original heartbeat failure:", error);
13145
- });
13457
+ void this.ensureSandboxEffectHost(host.sandboxId).catch(
13458
+ (reconnectError) => {
13459
+ console.error(
13460
+ `[Granular] Failed to recover effect host for sandbox ${host.sandboxId} after heartbeat failure:`,
13461
+ reconnectError
13462
+ );
13463
+ console.error("[Granular] Original heartbeat failure:", error);
13464
+ }
13465
+ );
13146
13466
  }
13147
13467
  startEffectHostHeartbeat(host) {
13148
13468
  if (host.heartbeatTimer) {
@@ -13163,9 +13483,15 @@ var Granular = class _Granular {
13163
13483
  host.heartbeatInFlight = false;
13164
13484
  });
13165
13485
  };
13166
- sendHeartbeat("[Granular] Initial effect host heartbeat failed for sandbox", false);
13486
+ sendHeartbeat(
13487
+ "[Granular] Initial effect host heartbeat failed for sandbox",
13488
+ false
13489
+ );
13167
13490
  host.heartbeatTimer = setInterval(() => {
13168
- sendHeartbeat("[Granular] Effect host heartbeat failed for sandbox", true);
13491
+ sendHeartbeat(
13492
+ "[Granular] Effect host heartbeat failed for sandbox",
13493
+ true
13494
+ );
13169
13495
  }, 1e4);
13170
13496
  }
13171
13497
  stopEffectHostHeartbeat(host) {
@@ -13197,7 +13523,12 @@ var Granular = class _Granular {
13197
13523
  const effectClientId = crypto.randomUUID();
13198
13524
  const clientId = `effect-host:${sandboxId}:${effectClientId}`;
13199
13525
  const wsClient = new WSClient({
13200
- url: buildEffectHostUrl(this.apiUrl, sandboxId, effectClientId, clientId),
13526
+ url: buildEffectHostUrl(
13527
+ this.apiUrl,
13528
+ sandboxId,
13529
+ effectClientId,
13530
+ clientId
13531
+ ),
13201
13532
  sessionId: `effect-host:${effectClientId}`,
13202
13533
  token: this.apiKey,
13203
13534
  tokenProvider: this.tokenProvider,
@@ -13216,7 +13547,10 @@ var Granular = class _Granular {
13216
13547
  };
13217
13548
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
13218
13549
  const request = params;
13219
- return invokeRegisteredEffect(this.getSandboxEffectMap(sandboxId), request);
13550
+ return invokeRegisteredEffect(
13551
+ this.getSandboxEffectMap(sandboxId),
13552
+ request
13553
+ );
13220
13554
  });
13221
13555
  wsClient.on("open", () => {
13222
13556
  void this.synchronizeEffectHost(host).catch((error) => {
@@ -13264,7 +13598,7 @@ var Granular = class _Granular {
13264
13598
  }
13265
13599
  /**
13266
13600
  * Register multiple effects (tools) for a specific sandbox.
13267
- *
13601
+ *
13268
13602
  * batch version of `registerEffect`.
13269
13603
  */
13270
13604
  async registerEffects(sandboxNameOrId, effects) {
@@ -13278,7 +13612,7 @@ var Granular = class _Granular {
13278
13612
  }
13279
13613
  /**
13280
13614
  * Unregister an effect from a sandbox.
13281
- *
13615
+ *
13282
13616
  * Removes it from the local sandbox registry and updates the
13283
13617
  * sandbox-scoped live catalog.
13284
13618
  */
@@ -13377,27 +13711,31 @@ var Granular = class _Granular {
13377
13711
  const assignments = await this.request(
13378
13712
  `/control/subjects/${subjectId}/assignments`
13379
13713
  );
13380
- const existing = assignments.items.find(
13381
- (a) => a.sandboxId === sandboxId
13382
- );
13714
+ const existing = assignments.items.find((a) => a.sandboxId === sandboxId);
13383
13715
  if (existing) {
13384
13716
  if (existing.permissionProfileId === permissionProfileId) {
13385
13717
  return;
13386
13718
  }
13387
- await this.request(`/control/assignments/${existing.assignmentId}`, {
13388
- method: "DELETE"
13389
- });
13719
+ await this.request(
13720
+ `/control/assignments/${existing.assignmentId}`,
13721
+ {
13722
+ method: "DELETE"
13723
+ }
13724
+ );
13390
13725
  }
13391
13726
  } catch {
13392
13727
  }
13393
- await this.request(`/control/subjects/${subjectId}/assignments`, {
13394
- method: "POST",
13395
- body: JSON.stringify({
13396
- sandboxId,
13397
- subjectId,
13398
- permissionProfileId
13399
- })
13400
- });
13728
+ await this.request(
13729
+ `/control/subjects/${subjectId}/assignments`,
13730
+ {
13731
+ method: "POST",
13732
+ body: JSON.stringify({
13733
+ sandboxId,
13734
+ subjectId,
13735
+ permissionProfileId
13736
+ })
13737
+ }
13738
+ );
13401
13739
  }
13402
13740
  /**
13403
13741
  * Sandbox management API
@@ -13477,23 +13815,33 @@ var Granular = class _Granular {
13477
13815
  },
13478
13816
  get: async (environmentId) => {
13479
13817
  return normalizeEnvironmentData(
13480
- await this.request(`/control/environments/${environmentId}`)
13818
+ await this.request(
13819
+ `/control/environments/${environmentId}`
13820
+ )
13481
13821
  );
13482
13822
  },
13483
13823
  create: async (sandboxId, data) => {
13484
13824
  const environmentName = data.environment || data.envName;
13485
- return normalizeEnvironmentData(await this.request(`/control/sandboxes/${sandboxId}/environments`, {
13486
- method: "POST",
13487
- body: JSON.stringify({
13488
- ...data,
13489
- envName: environmentName
13490
- })
13491
- }));
13825
+ return normalizeEnvironmentData(
13826
+ await this.request(
13827
+ `/control/sandboxes/${sandboxId}/environments`,
13828
+ {
13829
+ method: "POST",
13830
+ body: JSON.stringify({
13831
+ ...data,
13832
+ envName: environmentName
13833
+ })
13834
+ }
13835
+ )
13836
+ );
13492
13837
  },
13493
13838
  delete: async (environmentId) => {
13494
- return this.request(`/control/environments/${environmentId}`, {
13495
- method: "DELETE"
13496
- });
13839
+ return this.request(
13840
+ `/control/environments/${environmentId}`,
13841
+ {
13842
+ method: "DELETE"
13843
+ }
13844
+ );
13497
13845
  }
13498
13846
  };
13499
13847
  }
@@ -13513,10 +13861,13 @@ var Granular = class _Granular {
13513
13861
  }
13514
13862
  if (params.since) query.set("since", params.since.toISOString());
13515
13863
  if (params.until) query.set("until", params.until.toISOString());
13516
- if (params.isAcked !== void 0) query.set("isAcked", params.isAcked ? "1" : "0");
13864
+ if (params.isAcked !== void 0)
13865
+ query.set("isAcked", params.isAcked ? "1" : "0");
13517
13866
  if (params.limit) query.set("limit", String(params.limit));
13518
13867
  if (params.offset) query.set("offset", String(params.offset));
13519
- const result = await this.request(`/control/stream-events?${query.toString()}`);
13868
+ const result = await this.request(
13869
+ `/control/stream-events?${query.toString()}`
13870
+ );
13520
13871
  return (result.items || []).map((row) => ({
13521
13872
  eventId: row.event_id,
13522
13873
  streamName: row.stream_name,
@@ -13547,7 +13898,9 @@ var Granular = class _Granular {
13547
13898
  since: cursor,
13548
13899
  limit: 100
13549
13900
  });
13550
- const orderedEvents = [...events].sort((a, b) => a.createdAt - b.createdAt);
13901
+ const orderedEvents = [...events].sort(
13902
+ (a, b) => a.createdAt - b.createdAt
13903
+ );
13551
13904
  for (const event of orderedEvents) {
13552
13905
  if (seenEventIds.has(event.eventId)) {
13553
13906
  continue;
@@ -13560,15 +13913,19 @@ var Granular = class _Granular {
13560
13913
  params.onEvent(event);
13561
13914
  }
13562
13915
  } catch (err) {
13563
- params.onError?.(err instanceof Error ? err : new Error(String(err)));
13916
+ params.onError?.(
13917
+ err instanceof Error ? err : new Error(String(err))
13918
+ );
13564
13919
  }
13565
13920
  await new Promise((resolve) => setTimeout(resolve, interval));
13566
13921
  }
13567
13922
  };
13568
13923
  poll();
13569
- return { unsubscribe: () => {
13570
- running = false;
13571
- } };
13924
+ return {
13925
+ unsubscribe: () => {
13926
+ running = false;
13927
+ }
13928
+ };
13572
13929
  },
13573
13930
  ack: async (eventId) => {
13574
13931
  await this.request("/control/stream-events/ack", {
@@ -13586,7 +13943,9 @@ var Granular = class _Granular {
13586
13943
  const sandbox = await this._resolveSandboxId(params.ontology);
13587
13944
  const query = new URLSearchParams({ sandboxId: sandbox });
13588
13945
  if (params.environment) query.set("environmentId", params.environment);
13589
- const result = await this.request(`/control/stream-events/stats?${query.toString()}`);
13946
+ const result = await this.request(
13947
+ `/control/stream-events/stats?${query.toString()}`
13948
+ );
13590
13949
  return (result.items || []).map((row) => ({
13591
13950
  streamName: row.stream_name,
13592
13951
  eventType: row.event_type,
@@ -13604,10 +13963,14 @@ var Granular = class _Granular {
13604
13963
  get subjects() {
13605
13964
  return {
13606
13965
  get: async (subjectId) => {
13607
- return normalizeSubject(await this.request(`/control/subjects/${subjectId}`));
13966
+ return normalizeSubject(
13967
+ await this.request(`/control/subjects/${subjectId}`)
13968
+ );
13608
13969
  },
13609
13970
  listAssignments: async (subjectId) => {
13610
- return this.request(`/control/subjects/${subjectId}/assignments`);
13971
+ return this.request(
13972
+ `/control/subjects/${subjectId}/assignments`
13973
+ );
13611
13974
  }
13612
13975
  };
13613
13976
  }
@@ -13617,24 +13980,31 @@ var Granular = class _Granular {
13617
13980
  get users() {
13618
13981
  return {
13619
13982
  create: async (data) => {
13620
- return normalizeSubject(await this.request("/control/subjects", {
13621
- method: "POST",
13622
- body: JSON.stringify({
13623
- identityId: data.id,
13624
- name: data.name,
13625
- email: data.email
13983
+ return normalizeSubject(
13984
+ await this.request("/control/subjects", {
13985
+ method: "POST",
13986
+ body: JSON.stringify({
13987
+ identityId: data.id,
13988
+ name: data.name,
13989
+ email: data.email
13990
+ })
13626
13991
  })
13627
- }));
13992
+ );
13628
13993
  },
13629
13994
  get: async (id) => {
13630
- return normalizeSubject(await this.request(`/control/subjects/${id}`));
13995
+ return normalizeSubject(
13996
+ await this.request(`/control/subjects/${id}`)
13997
+ );
13631
13998
  }
13632
13999
  };
13633
14000
  }
13634
14001
  async _resolveSandboxId(ontologyNameOrId) {
13635
14002
  if (ontologyNameOrId.startsWith("sbx_")) return ontologyNameOrId;
13636
- const result = await this.request(`/control/sandboxes?name=${encodeURIComponent(ontologyNameOrId)}`);
13637
- if (result.items.length === 0) throw new Error(`Ontology not found: ${ontologyNameOrId}`);
14003
+ const result = await this.request(
14004
+ `/control/sandboxes?name=${encodeURIComponent(ontologyNameOrId)}`
14005
+ );
14006
+ if (result.items.length === 0)
14007
+ throw new Error(`Ontology not found: ${ontologyNameOrId}`);
13638
14008
  return result.items[0].sandboxId;
13639
14009
  }
13640
14010
  /**
@@ -13649,9 +14019,9 @@ var Granular = class _Granular {
13649
14019
  const response = await fetch(url, {
13650
14020
  ...options,
13651
14021
  headers: {
13652
- "Authorization": `Bearer ${this.apiKey}`,
14022
+ Authorization: `Bearer ${this.apiKey}`,
13653
14023
  "Content-Type": "application/json",
13654
- "Connection": "close",
14024
+ Connection: "close",
13655
14025
  ...options.headers
13656
14026
  }
13657
14027
  });
@@ -13662,7 +14032,11 @@ var Granular = class _Granular {
13662
14032
  return response.json();
13663
14033
  }
13664
14034
  const errorText = await response.text();
13665
- const retryable = isRetryableLocalWorkerRestart(response.status, errorText, url);
14035
+ const retryable = isRetryableLocalWorkerRestart(
14036
+ response.status,
14037
+ errorText,
14038
+ url
14039
+ );
13666
14040
  if (retryable && attempt < LOCAL_CONTROL_REQUEST_RETRY_COUNT) {
13667
14041
  if (this.debugHttp) {
13668
14042
  console.warn(
@@ -13801,21 +14175,6 @@ function reviewGeneratedJobCode(code) {
13801
14175
  message: "After await loop.ask_user(...) returns a usable answer, continue the workflow in the same resumed run instead of stopping with placeholder text about doing the work later."
13802
14176
  });
13803
14177
  }
13804
- const askUserCalls = normalized.match(/await\s+loop\.ask_user\s*\(\s*\{[\s\S]*?\}\s*\)/g) || [];
13805
- for (const call of askUserCalls) {
13806
- const usesChoiceType = /type\s*:\s*['"]choice['"]/.test(call);
13807
- const usesInputType = /type\s*:\s*['"]input['"]/.test(call);
13808
- const hasDisambiguationLanguage = /(which|choose|pick|select)/i.test(call) && /(invoice|order|shipment|request|case|work[\s_-]?order)/i.test(call);
13809
- const includesShortlistOptions = /options\s*:\s*\[/.test(call);
13810
- if (!usesChoiceType && (usesInputType || hasDisambiguationLanguage || includesShortlistOptions)) {
13811
- issues.push({
13812
- code: "disambiguation_requires_choice",
13813
- severity: "error",
13814
- message: "When asking the user to choose between known concrete records such as invoices, orders, shipments, requests, cases, or work orders, use loop.ask_user({ type: 'choice', options: [...] }) with a short explicit shortlist instead of a free-text input."
13815
- });
13816
- break;
13817
- }
13818
- }
13819
14178
  }
13820
14179
  const hasConversationalReturn = /return\s+[`'"]/.test(normalized) || /\breply\s*:/.test(normalized) || /\bagent_message\s*\(/.test(normalized) || /\bagent_text_message\s*\(/.test(normalized);
13821
14180
  const returnsObjectLiteral = /return\s+\{[\s\S]*?\}/.test(normalized);
@@ -13861,6 +14220,9 @@ function extractFocusHintsFromActionSummary(actionSummaryLines) {
13861
14220
  entryPaths: uniqueStrings(entryPaths, 8)
13862
14221
  };
13863
14222
  }
14223
+ function normalizeActionSummaryForPrompt(line) {
14224
+ return line.replace(/\blimit=/g, "perPage=").replace(/\blimit:/g, "perPage:");
14225
+ }
13864
14226
  function getCurrentClosureId(liveDoc) {
13865
14227
  const loop = asRecord2(liveDoc?.loop);
13866
14228
  return typeof loop?.currentClosureId === "string" ? loop.currentClosureId : null;
@@ -14073,7 +14435,9 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
14073
14435
  variableNames: uniqueStrings(variableNames, 4),
14074
14436
  listNames: uniqueStrings(listNames, 4),
14075
14437
  entryPaths: uniqueStrings(entryPaths, 6),
14076
- recentActionSummary: uniqueStrings(actionSummaryLines, 8)
14438
+ recentActionSummary: uniqueStrings(actionSummaryLines, 8).map(
14439
+ normalizeActionSummaryForPrompt
14440
+ )
14077
14441
  };
14078
14442
  }
14079
14443
  function projectWorkflowSummary(liveDoc, pendingPrompts = [], options) {
@@ -14465,17 +14829,14 @@ ${resultPreview}` : null
14465
14829
  ].filter(Boolean).join("\n\n");
14466
14830
  }
14467
14831
  function buildGranularAgentDomainBlock(domainDocumentation) {
14468
- return domainDocumentation?.trim() || "No domain types available. The graph may not be ready yet.";
14832
+ return domainDocumentation?.trim() || "No domain reference available. The graph may not be ready yet.";
14469
14833
  }
14470
14834
  function buildGranularAgentSessionBlock(sessionContext) {
14471
14835
  if (!sessionContext) return "No session metadata available.";
14472
14836
  const rows = [
14473
14837
  ["sandboxId", sessionContext.sandboxId],
14474
14838
  ["environmentId", sessionContext.environmentId],
14475
- ["userId", sessionContext.userId],
14476
- ["granularId", sessionContext.granularId],
14477
- ["userName", sessionContext.userName],
14478
- ["domainRevision", sessionContext.domainRevision]
14839
+ ["userName", sessionContext.userName]
14479
14840
  ];
14480
14841
  const activeRows = rows.filter(([, value]) => Boolean(value));
14481
14842
  if (activeRows.length === 0) return "No session metadata available.";
@@ -14484,6 +14845,9 @@ function buildGranularAgentSessionBlock(sessionContext) {
14484
14845
  function buildGranularAgentHeapBlock(heapSummary) {
14485
14846
  return heapSummary?.trim() || "Heap is empty for this session.";
14486
14847
  }
14848
+ function buildGranularAgentReferentBlock(referentSummary) {
14849
+ return referentSummary?.trim() || "No recent referents recorded from prior assistant replies.";
14850
+ }
14487
14851
  function buildGranularAgentLoopBlock(loopSummary) {
14488
14852
  return loopSummary?.trim() || "No active loop state recorded for this session.";
14489
14853
  }
@@ -14507,7 +14871,7 @@ function buildGranularAgentToolBlock(tools) {
14507
14871
  (tool) => Boolean(tool.className && !tool.static)
14508
14872
  );
14509
14873
  const lines = [
14510
- "Treat this block as the planning map. Use DOMAIN TYPES below for exact signatures."
14874
+ "Treat this block as the planning map. Use DOMAIN REFERENCE below for exact signatures and query examples."
14511
14875
  ];
14512
14876
  const appendGroup = (title, group) => {
14513
14877
  lines.push(`- ${title}:`);
@@ -14555,7 +14919,10 @@ function buildGranularAgentCheckpointBlock(checkpoint) {
14555
14919
  if (Array.isArray(checkpoint.latestActionSummary) && checkpoint.latestActionSummary.length > 0) {
14556
14920
  lines.push("latestActionSummary:");
14557
14921
  for (const line of checkpoint.latestActionSummary.slice(0, 8)) {
14558
- lines.push(line.startsWith("- ") ? line : `- ${line}`);
14922
+ const normalizedLine = normalizeActionSummaryForPrompt(line);
14923
+ lines.push(
14924
+ normalizedLine.startsWith("- ") ? normalizedLine : `- ${normalizedLine}`
14925
+ );
14559
14926
  }
14560
14927
  }
14561
14928
  if (checkpoint.latestJobResult?.trim()) {
@@ -14571,9 +14938,10 @@ function buildGranularAgentSystemPrompt(input) {
14571
14938
  const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
14572
14939
  const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
14573
14940
  const heapBlock = buildGranularAgentHeapBlock(input.heapSummary);
14941
+ const referentBlock = buildGranularAgentReferentBlock(input.referentSummary);
14574
14942
  const loopBlock = buildGranularAgentLoopBlock(input.loopSummary);
14575
14943
  return `You are an AI assistant for a live Granular session.
14576
- You can help the user understand the domain, answer questions, or generate and execute TypeScript code.
14944
+ You can help the user understand the domain, answer questions, or generate and execute code against the live session.
14577
14945
  Your tone must be natural and human-like.
14578
14946
 
14579
14947
  Call the \`execute_code\` effect ONLY when the user's intent matches the domain's capabilities and requires executing code against the live session. If the user is just asking a general question or if their request doesn't match the available effects or domain types, respond with text to explain.
@@ -14582,6 +14950,8 @@ When you call \`execute_code\`, additional assistant text must be either:
14582
14950
  - a brief summary of the actions the generated code will perform.
14583
14951
  Do not include any other kind of commentary when calling \`execute_code\`.
14584
14952
  - If the next step needs to create or update workflow state in the live session, you must call \`execute_code\`. This includes \`loop.ask_user(...)\`, \`loop.confirm(...)\`, \`loop.open_decision(...)\`, \`loop.close_decision(...)\`, \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`, and \`loop.close_loop(...)\`.
14953
+ - If the next step is an interactive clarification that should be resumable in the live workflow, you must call \`execute_code\`. A missing preference, rule, metric, target, or option selection is not a plain-text reply when the answer should drive the next live step.
14954
+ - If you can offer a short grounded shortlist, that clarification should usually be \`loop.ask_user({ type: 'choice', ... })\` instead of a plain-text question with bullet options.
14585
14955
  - Never simulate a live prompt, confirmation, decision, task change, or loop closure in plain text. Plain-text replies are only for conversational answers that do not need to mutate session state.
14586
14956
 
14587
14957
  \u2500\u2500\u2500 STREAMING COMMENT RULES \u2500\u2500\u2500
@@ -14601,6 +14971,7 @@ Do not include any other kind of commentary when calling \`execute_code\`.
14601
14971
  - Do not say "sandbox" in user-facing text unless the user is explicitly asking about the runtime environment itself.
14602
14972
  - If you need clarification, ask in everyday language.
14603
14973
  - If the missing information should pause the live workflow for later continuation, ask through \`loop.ask_user(...)\` in generated code rather than with a plain-text question.
14974
+ - If you are asking the user to pick from explicit options, prefer a live \`loop.ask_user({ type: 'choice', ... })\` prompt over a direct reply that lists those options in text.
14604
14975
  - Keep replies concise and clear.
14605
14976
  - This is a conversation UI, not an API console. Favor human answers over machine-shaped payloads.
14606
14977
 
@@ -14610,9 +14981,9 @@ ${sessionBlock}
14610
14981
  \u2500\u2500\u2500 CAPABILITY SNAPSHOT \u2500\u2500\u2500
14611
14982
  ${toolBlock}
14612
14983
 
14613
- \u2500\u2500\u2500 DOMAIN TYPES (TypeScript declarations from ./sandbox-tools) \u2500\u2500\u2500
14984
+ \u2500\u2500\u2500 DOMAIN REFERENCE (from ./sandbox-tools) \u2500\u2500\u2500
14614
14985
  Import classes and effect functions from \`./sandbox-tools\` in generated code.
14615
- Published effects appear as instance or static methods on the classes below, or as top-level \`export declare function\` entries for global effects.
14986
+ Use the TypeScript declarations for exact signatures. When present, the generated usage notes below them show query patterns and examples.
14616
14987
 
14617
14988
  ${domainBlock}
14618
14989
 
@@ -14622,6 +14993,9 @@ ${checkpointBlock}
14622
14993
  \u2500\u2500\u2500 WORKFLOW SNAPSHOT \u2500\u2500\u2500
14623
14994
  ${workflowBlock}
14624
14995
 
14996
+ \u2500\u2500\u2500 RECENT REFERENTS \u2500\u2500\u2500
14997
+ ${referentBlock}
14998
+
14625
14999
  \u2500\u2500\u2500 SESSION HEAP \u2500\u2500\u2500
14626
15000
  ${heapBlock}
14627
15001
 
@@ -14629,109 +15003,54 @@ ${heapBlock}
14629
15003
  ${loopBlock}
14630
15004
 
14631
15005
  \u2500\u2500\u2500 LOOP PLAYBOOK \u2500\u2500\u2500
14632
- - A single user request may span several assistant turns and several jobs. Continue from the latest structured session state instead of restarting.
14633
- - Treat WORKFLOW SNAPSHOT, EXECUTION CHECKPOINT, SESSION HEAP, and AGENT LOOP STATE as the authoritative working memory for the current request.
14634
- - Use CAPABILITY SNAPSHOT to choose the next step quickly, then use DOMAIN TYPES to write exact valid code.
14635
- - Use WORKFLOW SNAPSHOT to understand the current boundary, recent actions, and working set before fetching more data.
14636
- - If the user names a concrete customer, case, order, shipment, or other record that is not already in the heap, fetch it from the graph. "Not in the current context" is not a sufficient reason to stop.
14637
- - Treat user-provided names as human references, not exact database keys. If the user says "Northwind", "the Alpine compressor case", or another shorthand, prefer sensible case-insensitive partial matching across likely records before concluding that nothing matches.
14638
- - If exactly one strong partial-name match exists, use it. If several plausible partial matches exist, ask the user to choose instead of failing on an exact-equality lookup.
14639
- - Take the minimum next step that directly advances the user's request. Do not do speculative cleanup, enrichment, or bookkeeping.
14640
- - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from the AGENT LOOP STATE block. Never guess or slugify IDs.
14641
- - If the request is ambiguous or clearly multi-step, start by creating 2-4 meaningful user-visible tasks. Do not create a detailed internal checklist.
14642
- - Keep tasks updated as the workflow advances. Complete tasks as soon as they are actually done.
14643
- - Write the smallest straightforward code that fits the current step. Avoid defensive branches for hypothetical states that are not currently true.
14644
- - Before asking a new question, check whether the answer is already present in the current heap, open decisions, or checkpoint.
14645
- - If the previous step made no progress, prefer a different concrete action, a narrower fetch, or a user question instead of repeating equivalent code.
14646
- - Use \`loop.open_decision(...)\` in one job to store grounded candidates, then \`loop.close_decision(...)\` in a later job to pick one stored candidate with \`selectedId\`.
14647
- - Use \`loop.ask_user({ type: 'input', ... })\` only for open-ended preferences or missing free-form text that cannot be represented as a short explicit shortlist.
14648
- - If you already have a short concrete shortlist, usually 2-7 candidates, generate code and call \`loop.ask_user({ type: 'choice', ... })\`. Do not downgrade that to a text input.
14649
- - If multiple concrete records match a singular user reference such as "the invoice", "the order", "the shipment", or "the request", do not silently choose one by heuristic. Ask the user to choose unless the request already uniquely identifies the record.
14650
- - For disambiguation between concrete known records, prefer \`type: 'choice'\` over \`type: 'input'\`. This is especially important for invoices, orders, shipments, requests, work orders, and cases.
14651
- - For \`type: 'choice'\` prompts, make the options directly pickable by a human: use a stable value and a readable label that includes the identifier or title they are likely to recognize.
14652
- - If a shortlist already exists, do not ask the user to type an exact database key or identifier manually. Present the shortlist as clickable choices instead.
14653
- - When reasoning about free-form status strings, do not use brittle substring checks such as \`status.includes("paid")\` because values like \`"unpaid"\` would be misclassified. Prefer explicit positive matches such as \`unpaid\`, \`open\`, or \`overdue\`, or exact normalized comparisons.
14654
- - When multiple concrete records match and \`loop.ask_user(...)\` is available, do not stop with a plain-text question like "Which invoice do you mean?". Persist the live workflow and ask through \`await loop.ask_user(...)\` instead.
14655
- - If the user could reasonably answer with a partial identifier such as \`abcd\` for \`INV-abcd\`, that is another sign the question should be a \`type: 'choice'\` prompt with visible options rather than a free-text input.
14656
- - Never ask for approval in plain text when \`loop.confirm(...)\` is available. Use \`loop.confirm(...)\` for consequential approval.
14657
- - When the correct next step is a loop helper action, generate code and call that helper. Do not replace it with a conversational reply.
14658
- - If you ask the user a new question in the current job, do not also call \`loop.close_loop(...)\` in that same job.
14659
- - When you need user input or approval, await \`loop.ask_user(...)\` or \`loop.confirm(...)\`. The job will pause until the user answers, then resume from that awaited call.
14660
- - It is valid to branch on the value returned by \`await loop.ask_user(...)\` or \`await loop.confirm(...)\` after the job resumes.
14661
- - After \`await loop.ask_user(...)\` returns a concrete choice, continue the workflow in the same resumed job whenever that answer is enough to act. Do not stop with placeholder text such as "I can do that next" or "I'm ready to continue".
14662
- - After \`await loop.confirm(...)\` returns \`true\`, execute the approved mutation in that same resumed job before returning. Do not end with placeholder text like "Approved, ready to make the change next."
14663
- - Only stop immediately after a resumed prompt when the user declined, the workflow is now blocked, or you truly still need another missing piece of information.
14664
- - If the user says stop, enough, or no further action, close the loop and end cleanly without asking another question.
14665
- - If one clear item is already selected and the next step matters, prefer \`loop.confirm(...)\` over another exploratory question.
14666
- - If one clear item is already selected and the only missing input is approval to proceed, use \`loop.confirm(...)\` rather than \`loop.ask_user(...)\`.
14667
- - If the user already gave a usable scheduling window such as "Tuesday morning", treat that as enough to choose a reasonable concrete slot. Do not open another menu just to choose between nearby sub-slots unless a real conflict or hard business rule forces that follow-up.
14668
- - For schedule changes, prefer one grounded recommendation plus one approval prompt. Avoid a second prompt for optional time-window micro-choices when you can pick a sensible default that still satisfies the user request.
14669
- - If the user explicitly instructs you to perform a consequential action now, that instruction counts as approval. Do not add an extra confirmation step unless the user expressed hesitation, ambiguity, or asked you not to execute yet.
14670
- - Direct imperatives such as "cancel this order", "send the reminder now", "approve this refund", or "charge it now" already authorize that exact step. Execute them directly instead of inserting \`loop.confirm(...)\`.
14671
- - If the user says not to do anything irreversible yet, stop at recommendation, review, or approval. Do not collect checkout-only details like quantity, delivery notes, gift message, or optional preferences unless the user explicitly asks to move closer to purchase.
14672
- - Once you have one solid recommendation, prefer summarizing it and asking for approval over gathering more optional preferences.
14673
- - Prefer asking the user for the next missing input over fetching extra related data they did not ask for yet.
14674
- - Avoid serial menus. After one clarifying choice, prefer acting on it, asking one short text question, or confirming rather than opening another menu.
14675
- - When the user asks for a summary "including" concrete records such as unpaid invoices, open cases, orders, or shipments, include the actual identifiers or titles of those records in the reply, not just aggregate counts.
14676
- - Call \`loop.close_loop(...)\` before stopping whenever the current workflow is completed, canceled, or clearly blocked.
15006
+ - Continue from the latest structured state. Treat WORKFLOW SNAPSHOT, EXECUTION CHECKPOINT, RECENT REFERENTS, SESSION HEAP, and AGENT LOOP STATE as the working memory for this request.
15007
+ - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
15008
+ - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
15009
+ - If the user names a record that is not already in the heap, fetch it from the graph instead of saying it is not in context.
15010
+ - Treat user-provided names as human references, not exact keys. If one strong partial match exists, use it. If several plausible matches exist, ask the user to choose.
15011
+ - Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
15012
+ - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
15013
+ - For comparisons, rankings, selections, or summaries, first identify the rule you are using. If that rule is not clear from the user request and DOMAIN REFERENCE, ask the user before choosing anything.
15014
+ - When the ranking, comparison, or selection rule is unclear, the minimum next step is the clarification itself. Do not run a placeholder query for a provisional winner before asking.
15015
+ - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
15016
+ - If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
15017
+ - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
15018
+ - For an unclear ranking, comparison, or selection rule, prefer \`type: 'choice'\` when you can offer a short grounded list of plausible interpretations from the domain or nearby context.
15019
+ - When \`type: 'choice'\` fits, do not ask the same question as plain text with bullets such as "Common options:" or "Choose one of these:".
15020
+ - Use \`loop.confirm(...)\` for consequential approval unless the user already clearly instructed you to perform that exact action now.
15021
+ - Await \`loop.ask_user(...)\` and \`loop.confirm(...)\`. After the job resumes, continue in the same job whenever the answer is enough to act.
15022
+ - Use \`loop.open_decision(...)\` to persist grounded candidates, \`loop.close_decision(...)\` to resolve one, and \`loop.close_loop(...)\` when the workflow is completed, canceled, or blocked.
15023
+ - If you ask a new question in the current job, do not also close the loop in that same job.
14677
15024
 
14678
15025
  \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
14679
15026
  - Import from \`./sandbox-tools\`.
14680
- - If you use \`heap\`, \`loop\`, \`agent_text_message\`, \`agent_heap_objects\`, or legacy \`agent_message\`, import them explicitly from \`./sandbox-tools\`.
15027
+ - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
14681
15028
  - Write top-level executable code with \`await\` at top level.
14682
- - The generated job body must be plain runnable JavaScript. The DOMAIN TYPES block is only a reference for shapes and available methods.
14683
- - Do not write TypeScript-only syntax in executable code: no type annotations, no interfaces, no enums, no \`as Type\` casts, no \`satisfies\`, and no generic type parameters in code.
14684
- - Generated code must be valid against the DOMAIN TYPES block above.
14685
- - Only use classes, methods, and parameter shapes that are explicitly declared in those typedefs.
14686
- - Never invent helper methods such as \`find(...)\` or unsupported parameters such as \`id\` when the typedefs require \`path\`.
14687
- - Use \`ClassName.get({ path })\` only when you already know an object's graph path.
14688
- - Use \`ClassName.count()\` when you only need a total.
14689
- - Use \`ClassName.page({ page, perPage, saveAs })\` when you need both records and pagination metadata like \`totalCount\` or \`hasMore\`. \`perPage\` defaults to \`100\` and larger values are clamped to \`100\`.
14690
- - Use \`ClassName.list({ page, perPage, saveAs })\` to load one typed page of records. \`limit\` is only a legacy alias for \`perPage\`, \`perPage\` defaults to \`100\`, and larger values are clamped to \`100\`.
14691
- - 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. \`perPage\` defaults to \`100\` and larger values are clamped to \`100\`.
14692
- - Instance methods: \`await instance.method_name(params)\`.
14693
- - Static methods: \`await ClassName.static_method(params)\`.
14694
- - Global effects: \`await effect_name(params)\`.
14695
- - When a child record has sparse fields, identify it through nearby graph context instead of only string-matching that child\u2019s local fields. Prefer traversing linked customer, case, work order, part request, and shipment records over broad guesswork.
14696
- - For blocker, delay, ETA, or "what is holding this up?" questions, do not stop at a parent status like \`in_progress\` or \`scheduled\` if linked dependencies exist. Trace into the likely dependency chain first: approval -> work order -> part request -> shipment -> carrier update.
14697
- - A generic parent status is not a sufficient blocker explanation when a linked part request, approval, shipment, customs hold, or vendor delay may be the real cause.
14698
- - If a case summary or latest customer message mentions a part, shipment, ETA, customs, vendor, approval, regulator, kit, or delay, treat that as a strong hint to inspect the linked dependency records before answering.
14699
- - When you already found the correct parent case, inspect its linked child records even if the child summaries use different wording. Do not require a work order, part request, or shipment description to repeat the exact phrase that identified the parent case.
14700
- - 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\`.
14701
- - 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.
14702
- - 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.
14703
- - 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)\`.
14704
- - 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)\`.
14705
- - 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.
14706
- - 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.
14707
- - If a helper returns plain JSON candidates, keep them local, store only a scalar like the chosen id, or resolve the matching sandbox instance before writing it into the heap.
14708
- - When reading heap values, prefer generated generic typings such as \`await heap.getVar<Book[]>("my_books")\` or \`await heap.getVar<Book>("selected_book")\`.
14709
- - If a focused heap variable already points to a known class, read it with that exact generic type and act on it directly. Do not use \`heap.getVar<any>(...)\` or cast through \`any\` when the class is already clear from the prompt. For example, prefer \`await heap.getVar<Order>("selected_order")\` over \`await heap.getVar<any>("selected_order")\`.
14710
- - Use the injected \`loop\` helpers when you need to manage the workflow itself:
14711
- \`loop.ask_user(...)\`, \`loop.confirm(...)\`, \`loop.open_decision(...)\`, \`loop.close_decision(...)\`,
14712
- \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`, and \`loop.close_loop(...)\`.
14713
- - Loop helper semantics:
14714
- - \`loop.open_decision(...)\`: store explicit candidates from the current job so a later job can revisit the same decision. Keep and reuse the returned \`decisionId\`.
14715
- - \`loop.close_decision(...)\`: resolve an open decision by choosing one stored candidate with \`selectedId\` and recording why. Candidates may be any JSON objects, but each one must have an \`id\`.
14716
- - \`loop.ask_user(...)\`: pause the job and ask the user for missing input. Default to \`type: 'input'\`; use \`type: 'choice'\` only for a short explicit shortlist. Write \`const answer = await loop.ask_user(...)\`, then continue the same job once the user answers.
14717
- - \`loop.confirm(...)\`: pause the job for approval before a consequential action. Do not simulate confirmation in plain text. Write \`const approved = await loop.confirm(...)\`, then branch on that approval once the job resumes.
14718
- - \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`: keep a short task list that later jobs can continue and finish.
14719
- - \`loop.close_loop(...)\`: record the current workflow outcome with a short summary before stopping. Do not call it in the same job that opens a new user prompt unless the workflow is explicitly blocked. This does not end the session forever.
14720
- - Avoid \`as any\` and other broad casts when the DOMAIN TYPES block already tells you the correct class or list type.
14721
- - Prefer manipulating heap-backed instances and typed lists instead of returning raw JSON blobs or object IDs unless the user explicitly asks for them.
14722
- - Every job that intends to answer the user must emit at least one explicit UI message with \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
14723
- - Use \`agent_text_message("...")\` for all user-visible text shown in the UI.
14724
- - Use \`agent_heap_objects(...)\` only when you want the UI to render heap-backed records or lists.
14725
- - If you want to show both text and records, call \`agent_text_message(...)\` and \`agent_heap_objects(...)\` separately in whatever order fits the interaction.
14726
- - \`agent_text_message(...)\` should be used with a plain text string in normal generated code.
14727
- - \`agent_heap_objects(...)\` should point at heap-backed values: explicit \`entryPaths\` / \`listNames\` / \`variableNames\`, a named list saved with \`saveAs\`, or values read back from \`heap.getVar(...)\`.
14728
- - If you just fetched records and want to show them in the UI, save or reference them through the heap first, then call \`agent_heap_objects(...)\`. Do not try to hand-build UI payloads in job code.
14729
- - Do not assume heap changes will be displayed automatically. If records should appear in the UI, you must call \`agent_heap_objects(...)\`.
14730
- - Never write \`return { reply, show }\` or \`return { show: ... }\` for UI. If you want the UI to render records or lists, call \`agent_heap_objects(...)\` instead.
14731
- - Do not rely on the final return value for user-visible output. A plain return value is not considered a displayed UI answer.
14732
- - \`agent_message(...)\` remains available as a legacy compatibility alias, but prefer \`agent_text_message(...)\` and \`agent_heap_objects(...)\` in new code.
14733
- - Do not return bare structured JSON, low-level diagnostics, or database-shaped payloads as the final answer unless the user explicitly asks for them.
14734
- - Prefer simple executable JavaScript over clever interpolation. Avoid nested template literals or unusually dense inline expressions when a small temporary variable or string concatenation would be clearer and safer.
15029
+ - The generated job body must be plain runnable JavaScript. Do not use TypeScript-only syntax.
15030
+ - Follow the exact classes, methods, and parameter shapes in DOMAIN REFERENCE. Do not invent helpers or unsupported arguments.
15031
+ - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
15032
+ - Use \`ClassName.count()\` for totals, \`ClassName.page({ page, perPage, saveAs })\` when you need \`items\` plus \`totalCount\` or \`hasMore\`, \`ClassName.list({ page, perPage, saveAs })\` for one page of records, and \`ClassName.iterate({ perPage, maxItems })\` for large scans.
15033
+ - \`perPage\` defaults to \`100\` and is capped at \`100\`.
15034
+ - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
15035
+ - A property appearing on a record does not make it valid in \`filter\` or \`sort\`; only use fields and operators that are explicitly exposed in DOMAIN REFERENCE.
15036
+ - Choose \`sort.field\` verbatim from the sortable fields listed in DOMAIN REFERENCE. Do not sort by relationship names, related-record collections, counts, totals, or other derived metrics unless they are explicitly listed as sortable.
15037
+ - If ordering alone answers the request, use \`sort\` without inventing a \`filter\`.
15038
+ - Do not invent proxy metrics, fallback heuristics, or made-up tie-breakers to resolve ambiguity. If the rule is unclear, ask the user with \`loop.ask_user(...)\`.
15039
+ - Do not fetch, sort, or show a provisional record just to have something to display while the real ranking or selection rule is still ambiguous.
15040
+ - Call instance methods on instances, static methods on classes, and global effects by name.
15041
+ - Use \`heap.getEntry(path)\` for remembered heap entries, \`heap.getList(name)\` for remembered lists, and \`heap.getVar(name)\` only for named variables.
15042
+ - Use \`heap.setVar(...)\` and \`heap.deleteVar(...)\` only when they help the next step.
15043
+ - Prefer \`heap.setVar(...)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for reusable typed lists. Empty arrays are allowed.
15044
+ - Only store sandbox instances, typed lists, or scalars in the heap. If a helper returns plain JSON, keep it local or store only the chosen scalar.
15045
+ - Use the \`loop\` helpers to manage workflow state: \`ask_user\`, \`confirm\`, \`open_decision\`, \`close_decision\`, \`create_task\`, \`update_task\`, \`complete_task\`, and \`close_loop\`.
15046
+ - Use \`type: 'choice'\` only for short grounded options. Use \`type: 'input'\` when the answer should stay open-ended.
15047
+ - \`loop.confirm(...)\` is for consequential approval. Do not ask for approval in plain text.
15048
+ - After \`await loop.ask_user(...)\` or \`await loop.confirm(...)\`, continue in the same resumed job when the answer is enough to act.
15049
+ - Every job that answers the user must emit \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
15050
+ - Use \`agent_text_message(...)\` for user-visible text.
15051
+ - Use \`agent_heap_objects(...)\` for user-visible records. You may pass sandbox instances directly, or heap-backed \`entryPaths\`, \`listNames\`, and \`variableNames\` when you already have them. Use \`saveAs\` or \`heap.setVar(...)\` when you need a reusable named selection.
15052
+ - Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit agent message calls.
15053
+ - Keep the code small and direct. Avoid speculative branches, broad casts, and raw JSON dumps unless the user asked for them.
14735
15054
  - Use \`console.log()\` only for intermediate diagnostics, not for the final user-facing answer.`;
14736
15055
  }
14737
15056
 
@@ -14890,6 +15209,10 @@ function fallbackResponseText(entries, lists) {
14890
15209
  return entries.length === 1 ? "I found one relevant record." : `I found ${entries.length} relevant records.`;
14891
15210
  }
14892
15211
  if (lists.length > 0) {
15212
+ const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
15213
+ if (emptyOnly) {
15214
+ return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
15215
+ }
14893
15216
  return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
14894
15217
  }
14895
15218
  return null;
@@ -14993,19 +15316,27 @@ function matchesPattern(text, matcher) {
14993
15316
  function assertMatches(label, text, includes = [], excludes = []) {
14994
15317
  for (const matcher of includes) {
14995
15318
  if (!matchesPattern(text, matcher)) {
14996
- throw new Error(`${label} did not match ${matcherToString(matcher)}:
14997
- ${text}`);
15319
+ throw new Error(
15320
+ `${label} did not match ${matcherToString(matcher)}:
15321
+ ${text}`
15322
+ );
14998
15323
  }
14999
15324
  }
15000
15325
  for (const matcher of excludes) {
15001
15326
  if (matchesPattern(text, matcher)) {
15002
- throw new Error(`${label} matched forbidden ${matcherToString(matcher)}:
15003
- ${text}`);
15327
+ throw new Error(
15328
+ `${label} matched forbidden ${matcherToString(matcher)}:
15329
+ ${text}`
15330
+ );
15004
15331
  }
15005
15332
  }
15006
15333
  }
15007
15334
  function buildArtifactDir(baseDir, suiteName = "granular-agent-evals") {
15008
- return path.join(baseDir || path.join(process.cwd(), "test-artifacts"), suiteName, timestampId());
15335
+ return path.join(
15336
+ baseDir || path.join(process.cwd(), "test-artifacts"),
15337
+ suiteName,
15338
+ timestampId()
15339
+ );
15009
15340
  }
15010
15341
  function createTimestampedArtifactDirectory(options) {
15011
15342
  return buildArtifactDir(options?.baseDir, options?.suiteName);
@@ -15019,17 +15350,16 @@ function buildScenarioSteps(scenario) {
15019
15350
  return scenario.steps;
15020
15351
  }
15021
15352
  if (!scenario.request) {
15022
- throw new Error(`Scenario ${scenario.id} must provide either request or steps`);
15353
+ throw new Error(
15354
+ `Scenario ${scenario.id} must provide either request or steps`
15355
+ );
15023
15356
  }
15024
15357
  const compatibilityStep = {
15025
15358
  id: scenario.id,
15026
15359
  request: scenario.request,
15027
15360
  human: scenario.human,
15028
15361
  expect: scenario.expect,
15029
- inspect: [
15030
- ...asArray2(scenario.inspect),
15031
- ...asArray2(scenario.verify)
15032
- ],
15362
+ inspect: [...asArray2(scenario.inspect), ...asArray2(scenario.verify)],
15033
15363
  check: scenario.check,
15034
15364
  maxIterations: scenario.maxIterations,
15035
15365
  setup: {
@@ -15047,22 +15377,27 @@ function buildAssistantHistoryContent(entry) {
15047
15377
  ${entry.content}`);
15048
15378
  if (entry.jobStatus) parts.push(`[Job status]
15049
15379
  ${entry.jobStatus}`);
15050
- if (entry.jobResultPreview) parts.push(`[Job result]
15380
+ if (entry.jobResultPreview)
15381
+ parts.push(`[Job result]
15051
15382
  ${entry.jobResultPreview}`);
15052
15383
  if (entry.error) parts.push(`[Job error]
15053
15384
  ${entry.error}`);
15054
15385
  return parts.join("\n\n") || entry.content;
15055
15386
  }
15056
15387
  function buildHistory(entries) {
15057
- return entries.reduce((history, entry) => {
15058
- if (entry.role === "user") {
15059
- if (entry.content.trim()) history.push({ role: "user", content: entry.content });
15388
+ return entries.reduce(
15389
+ (history, entry) => {
15390
+ if (entry.role === "user") {
15391
+ if (entry.content.trim())
15392
+ history.push({ role: "user", content: entry.content });
15393
+ return history;
15394
+ }
15395
+ const content = buildAssistantHistoryContent(entry).trim();
15396
+ if (content) history.push({ role: "assistant", content });
15060
15397
  return history;
15061
- }
15062
- const content = buildAssistantHistoryContent(entry).trim();
15063
- if (content) history.push({ role: "assistant", content });
15064
- return history;
15065
- }, []);
15398
+ },
15399
+ []
15400
+ );
15066
15401
  }
15067
15402
  function getOpenPromptsFromDoc(liveDoc) {
15068
15403
  const jobsById = asRecord4(asRecord4(liveDoc?.jobs)?.byId) || {};
@@ -15071,7 +15406,8 @@ function getOpenPromptsFromDoc(liveDoc) {
15071
15406
  const promptRecords = asRecord4(asRecord4(job)?.prompts) || {};
15072
15407
  for (const raw of Object.values(promptRecords)) {
15073
15408
  const record = asRecord4(raw);
15074
- if (!record || record.status !== "open" || typeof record.promptId !== "string") continue;
15409
+ if (!record || record.status !== "open" || typeof record.promptId !== "string")
15410
+ continue;
15075
15411
  const prompt = normalizePrompt({
15076
15412
  promptId: record.promptId,
15077
15413
  kind: record.kind,
@@ -15115,7 +15451,9 @@ ${prompt.message || ""}`;
15115
15451
  return resolvePromptAnswer(prompt, rawAnswer);
15116
15452
  }
15117
15453
  if (fallback) return fallback({ prompt, history });
15118
- throw new Error(`No scripted prompt responder matched prompt ${prompt.id}: ${promptText}`);
15454
+ throw new Error(
15455
+ `No scripted prompt responder matched prompt ${prompt.id}: ${promptText}`
15456
+ );
15119
15457
  };
15120
15458
  }
15121
15459
  function extractJsonObject(text) {
@@ -15139,13 +15477,19 @@ function modelOutputInstruction() {
15139
15477
  ].join("\n");
15140
15478
  }
15141
15479
  function createOpenAIChatTurnGenerator(options) {
15142
- const baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(/\/$/, "");
15480
+ const baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(
15481
+ /\/$/,
15482
+ ""
15483
+ );
15143
15484
  const model = options.model || "gpt-5-mini";
15144
15485
  return async (input) => {
15145
15486
  const messages = [
15146
- { role: "system", content: `${input.systemPrompt}
15487
+ {
15488
+ role: "system",
15489
+ content: `${input.systemPrompt}
15147
15490
 
15148
- ${modelOutputInstruction()}` },
15491
+ ${modelOutputInstruction()}`
15492
+ },
15149
15493
  ...input.history,
15150
15494
  { role: "user", content: input.request }
15151
15495
  ];
@@ -15174,10 +15518,14 @@ ${modelOutputInstruction()}` },
15174
15518
  await sleep2(500 * attempt);
15175
15519
  continue;
15176
15520
  }
15177
- throw new Error(`OpenAI chat generation failed: ${response.status} ${errorText}`);
15521
+ throw new Error(
15522
+ `OpenAI chat generation failed: ${response.status} ${errorText}`
15523
+ );
15178
15524
  }
15179
15525
  const raw = await response.json();
15180
- const content = asRecord4(asRecord4(raw.choices?.[0])?.message)?.content;
15526
+ const content = asRecord4(
15527
+ asRecord4(raw.choices?.[0])?.message
15528
+ )?.content;
15181
15529
  const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord4(part)?.text || "").join("") : "";
15182
15530
  const parsed = extractJsonObject(text);
15183
15531
  if (!parsed) {
@@ -15195,7 +15543,9 @@ ${text}`);
15195
15543
  };
15196
15544
  } catch (error) {
15197
15545
  lastError = error instanceof Error ? error : new Error(String(error));
15198
- if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(lastError.message)) {
15546
+ if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
15547
+ lastError.message
15548
+ )) {
15199
15549
  await sleep2(500 * attempt);
15200
15550
  continue;
15201
15551
  }
@@ -15217,7 +15567,10 @@ async function withTimeout(promise, ms, label) {
15217
15567
  return await Promise.race([
15218
15568
  promise,
15219
15569
  new Promise((_, reject) => {
15220
- timeoutId = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
15570
+ timeoutId = setTimeout(
15571
+ () => reject(new Error(`${label} timed out after ${ms}ms`)),
15572
+ ms
15573
+ );
15221
15574
  })
15222
15575
  ]);
15223
15576
  } finally {
@@ -15227,7 +15580,9 @@ async function withTimeout(promise, ms, label) {
15227
15580
  function getActionSummary(liveDoc, jobId) {
15228
15581
  const jobsById = asRecord4(asRecord4(liveDoc?.jobs)?.byId) || {};
15229
15582
  const job = asRecord4(jobsById[jobId]);
15230
- return Array.isArray(job?.actionSummary) ? job.actionSummary.filter((line) => typeof line === "string") : [];
15583
+ return Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
15584
+ (line) => typeof line === "string"
15585
+ ) : [];
15231
15586
  }
15232
15587
  function normalizeHeapSnapshot2(heap) {
15233
15588
  return {
@@ -15257,12 +15612,20 @@ async function waitForJobOutcome(input) {
15257
15612
  const startedAt = Date.now();
15258
15613
  while (Date.now() - startedAt < input.timeoutMs) {
15259
15614
  const liveDoc = cloneJson(input.environment.document);
15260
- const prompts = filterPromptsByBoundary(liveDoc, getOpenPromptsFromDoc(liveDoc), input.boundaryTimestamp);
15615
+ const prompts = filterPromptsByBoundary(
15616
+ liveDoc,
15617
+ getOpenPromptsFromDoc(liveDoc),
15618
+ input.boundaryTimestamp
15619
+ );
15261
15620
  if (prompts.length > 0) {
15262
15621
  return { kind: "prompt", prompts, liveDoc, stdout, stderr };
15263
15622
  }
15264
15623
  try {
15265
- const result = await withTimeout(input.job.result, input.pollIntervalMs, `job ${input.job.id} tick`);
15624
+ const result = await withTimeout(
15625
+ input.job.result,
15626
+ input.pollIntervalMs,
15627
+ `job ${input.job.id} tick`
15628
+ );
15266
15629
  return { kind: "completed", result, liveDoc, stdout, stderr };
15267
15630
  } catch (error) {
15268
15631
  const message = error instanceof Error ? error.message : String(error);
@@ -15328,7 +15691,9 @@ function buildResultReport(result) {
15328
15691
  ...result.actionSummary.length ? result.actionSummary.map((line) => `- ${line}`) : ["- None"],
15329
15692
  "",
15330
15693
  "## Prompt Interactions",
15331
- ...result.promptInteractions.length ? result.promptInteractions.map((interaction) => `- [${interaction.type}] ${interaction.message || interaction.title} -> ${JSON.stringify(interaction.answer)}`) : ["- None"],
15694
+ ...result.promptInteractions.length ? result.promptInteractions.map(
15695
+ (interaction) => `- [${interaction.type}] ${interaction.message || interaction.title} -> ${JSON.stringify(interaction.answer)}`
15696
+ ) : ["- None"],
15332
15697
  "",
15333
15698
  ...stepSection,
15334
15699
  "## Raw Files",
@@ -15343,7 +15708,9 @@ function buildSuiteIndex(results) {
15343
15708
  const lines = [
15344
15709
  "# Agent Eval Report Index",
15345
15710
  "",
15346
- ...results.map((result) => `- [${result.scenario.id}](./${result.scenario.id}/REPORT.md) - ${result.status}`)
15711
+ ...results.map(
15712
+ (result) => `- [${result.scenario.id}](./${result.scenario.id}/REPORT.md) - ${result.status}`
15713
+ )
15347
15714
  ];
15348
15715
  return `${lines.join("\n")}
15349
15716
  `;
@@ -15357,7 +15724,7 @@ async function applySetup(setup, context) {
15357
15724
  await context.environment.recordObjects(setup.records);
15358
15725
  }
15359
15726
  if (setup.effects?.length) {
15360
- await context.environment.publishTools(setup.effects);
15727
+ await context.granular.ontology(context.environment.sandboxId).effects.registerMany(setup.effects);
15361
15728
  }
15362
15729
  if (setup.run) {
15363
15730
  await setup.run(context);
@@ -15379,6 +15746,7 @@ async function runAgentEvalSuite(options) {
15379
15746
  );
15380
15747
  try {
15381
15748
  await applySetup(scenario.setup, {
15749
+ granular: options.harness.granular,
15382
15750
  conversation,
15383
15751
  environment: conversation.environment,
15384
15752
  turnDir: conversation.artifactDir
@@ -15391,14 +15759,19 @@ async function runAgentEvalSuite(options) {
15391
15759
  conversation,
15392
15760
  request: step.request,
15393
15761
  prepare: async (ctx) => {
15394
- await applySetup(step.setup, ctx);
15762
+ await applySetup(step.setup, {
15763
+ granular: options.harness.granular,
15764
+ ...ctx
15765
+ });
15395
15766
  },
15396
15767
  human: step.human,
15397
15768
  maxIterations: step.maxIterations,
15398
15769
  autoAnswerPrompts: step.autoAnswerPrompts
15399
15770
  });
15400
15771
  if ("prompts" in completed) {
15401
- throw new Error(`Scenario ${scenario.id} step ${index + 1} paused for human input instead of completing automatically`);
15772
+ throw new Error(
15773
+ `Scenario ${scenario.id} step ${index + 1} paused for human input instead of completing automatically`
15774
+ );
15402
15775
  }
15403
15776
  const inspectionResults = [];
15404
15777
  const stepChecks = asArray2(step.check);
@@ -15414,12 +15787,23 @@ async function runAgentEvalSuite(options) {
15414
15787
  actionSummary: completed.actionSummary,
15415
15788
  promptInteractions: completed.promptInteractions,
15416
15789
  result: completed.result,
15417
- heap: normalizeHeapSnapshot2(asRecord4(cloneJson(conversation.environment.document)?.heap)),
15418
- openPrompts: getOpenPromptsFromDoc(cloneJson(conversation.environment.document)),
15790
+ heap: normalizeHeapSnapshot2(
15791
+ asRecord4(
15792
+ cloneJson(conversation.environment.document)?.heap
15793
+ )
15794
+ ),
15795
+ openPrompts: getOpenPromptsFromDoc(
15796
+ cloneJson(conversation.environment.document)
15797
+ ),
15419
15798
  liveDoc: cloneJson(conversation.environment.document),
15420
15799
  inspect: async (code) => {
15421
- const job = await conversation.environment.submitJob(code);
15422
- return withTimeout(job.result, 9e4, `inspection job ${job.id}`);
15800
+ const session = conversation.environment;
15801
+ const job = await session.submitJob(code);
15802
+ return withTimeout(
15803
+ job.result,
15804
+ 9e4,
15805
+ `inspection job ${job.id}`
15806
+ );
15423
15807
  },
15424
15808
  assertMatches
15425
15809
  };
@@ -15477,7 +15861,9 @@ async function runAgentEvalSuite(options) {
15477
15861
  }
15478
15862
  const lastStep = stepResults[stepResults.length - 1];
15479
15863
  if (!lastStep) {
15480
- throw new Error(`Scenario ${scenario.id} produced no completed steps`);
15864
+ throw new Error(
15865
+ `Scenario ${scenario.id} produced no completed steps`
15866
+ );
15481
15867
  }
15482
15868
  const result = {
15483
15869
  scenario,
@@ -15491,9 +15877,18 @@ async function runAgentEvalSuite(options) {
15491
15877
  steps: stepResults,
15492
15878
  turnDir: conversation.artifactDir
15493
15879
  };
15494
- await writeJson(path.join(conversation.artifactDir, "result.json"), result);
15495
- await writeJson(path.join(conversation.artifactDir, "report.json"), result);
15496
- await writeFile(path.join(conversation.artifactDir, "REPORT.md"), buildResultReport(result));
15880
+ await writeJson(
15881
+ path.join(conversation.artifactDir, "result.json"),
15882
+ result
15883
+ );
15884
+ await writeJson(
15885
+ path.join(conversation.artifactDir, "report.json"),
15886
+ result
15887
+ );
15888
+ await writeFile(
15889
+ path.join(conversation.artifactDir, "REPORT.md"),
15890
+ buildResultReport(result)
15891
+ );
15497
15892
  finalResult = result;
15498
15893
  } catch (error) {
15499
15894
  const failureMessage = error instanceof Error ? error.message : String(error);
@@ -15515,7 +15910,10 @@ async function runAgentEvalSuite(options) {
15515
15910
  await ensureDir(failed.turnDir);
15516
15911
  await writeJson(path.join(failed.turnDir, "result.json"), failed);
15517
15912
  await writeJson(path.join(failed.turnDir, "report.json"), failed);
15518
- await writeFile(path.join(failed.turnDir, "REPORT.md"), buildResultReport(failed));
15913
+ await writeFile(
15914
+ path.join(failed.turnDir, "REPORT.md"),
15915
+ buildResultReport(failed)
15916
+ );
15519
15917
  finalResult = failed;
15520
15918
  } finally {
15521
15919
  await options.harness.closeConversation(conversation);
@@ -15536,12 +15934,21 @@ async function runAgentEvalSuite(options) {
15536
15934
  }
15537
15935
  results.push(finalResult);
15538
15936
  }
15539
- await writeJson(path.join(options.harness.artifactDir, "summary.json"), results);
15540
- await writeFile(path.join(options.harness.artifactDir, "REPORT_INDEX.md"), buildSuiteIndex(results));
15937
+ await writeJson(
15938
+ path.join(options.harness.artifactDir, "summary.json"),
15939
+ results
15940
+ );
15941
+ await writeFile(
15942
+ path.join(options.harness.artifactDir, "REPORT_INDEX.md"),
15943
+ buildSuiteIndex(results)
15944
+ );
15541
15945
  return { artifactDir: options.harness.artifactDir, results };
15542
15946
  }
15543
15947
  function createAgentEvalHarness(options) {
15544
- const artifactDir = buildArtifactDir(options.artifactBaseDir, options.suiteName);
15948
+ const artifactDir = buildArtifactDir(
15949
+ options.artifactBaseDir,
15950
+ options.suiteName
15951
+ );
15545
15952
  const controllerBudgets = {
15546
15953
  ...DEFAULT_CONTROLLER_BUDGETS,
15547
15954
  ...options.controllerBudgets || {}
@@ -15553,7 +15960,9 @@ function createAgentEvalHarness(options) {
15553
15960
  await ensureDir(artifactDir);
15554
15961
  const clientId = `${slugify(label)}-${Date.now()}`;
15555
15962
  if (!options.openEnvironment && !options.environmentId) {
15556
- throw new Error("createAgentEvalHarness requires either environmentId or openEnvironment");
15963
+ throw new Error(
15964
+ "createAgentEvalHarness requires either environmentId or openEnvironment"
15965
+ );
15557
15966
  }
15558
15967
  const environment = options.openEnvironment ? await options.openEnvironment({ label, clientId }) : await options.granular.createSession({
15559
15968
  environmentId: options.environmentId,
@@ -15577,12 +15986,15 @@ function createAgentEvalHarness(options) {
15577
15986
  }
15578
15987
  async function closeConversation(conversation) {
15579
15988
  try {
15580
- await options.granular.closeSession(conversation.environment.sessionId, conversation.environment);
15989
+ await options.granular.closeSession(
15990
+ conversation.environment.sessionId,
15991
+ conversation.environment
15992
+ );
15581
15993
  } catch {
15582
15994
  }
15583
15995
  }
15584
- async function runCheckJob(code, environment) {
15585
- const job = await environment.submitJob(code);
15996
+ async function runCheckJob(code, session) {
15997
+ const job = await session.submitJob(code);
15586
15998
  return withTimeout(job.result, jobTimeoutMs, `check job ${job.id}`);
15587
15999
  }
15588
16000
  function buildCheckContext(conversation, completed, turnDir) {
@@ -15627,8 +16039,12 @@ function createAgentEvalHarness(options) {
15627
16039
  async function resumePendingTurn(pending, responder) {
15628
16040
  const prompt = pending.prompts[0];
15629
16041
  if (!prompt) throw new Error("Pending turn has no prompts to answer");
15630
- const answer = await responder({ prompt, history: pending.promptInteractions });
15631
- await pending.conversation.environment.answerPrompt(prompt.id, answer);
16042
+ const answer = await responder({
16043
+ prompt,
16044
+ history: pending.promptInteractions
16045
+ });
16046
+ const session = pending.conversation.environment;
16047
+ await session.answerPrompt(prompt.id, answer);
15632
16048
  pending.promptInteractions.push({
15633
16049
  promptId: prompt.id,
15634
16050
  type: prompt.type,
@@ -15652,7 +16068,9 @@ function createAgentEvalHarness(options) {
15652
16068
  };
15653
16069
  }
15654
16070
  await sleep2(350);
15655
- const liveDoc = cloneJson(pending.conversation.environment.document);
16071
+ const liveDoc = cloneJson(
16072
+ pending.conversation.environment.document
16073
+ );
15656
16074
  const presentation = resolveJobPresentation({
15657
16075
  jobId: pending.job.id,
15658
16076
  result: resumed.result,
@@ -15697,25 +16115,40 @@ function createAgentEvalHarness(options) {
15697
16115
  await conversation.environment.recordObjects(input.prepareRecords);
15698
16116
  }
15699
16117
  if (input.prepareTools?.length) {
15700
- await conversation.environment.publishTools(input.prepareTools);
16118
+ await options.granular.ontology(conversation.environment.sandboxId).effects.registerMany(input.prepareTools);
15701
16119
  }
15702
16120
  if (input.prepare) {
15703
- await input.prepare({ conversation, environment: conversation.environment, turnDir });
16121
+ await input.prepare({
16122
+ conversation,
16123
+ environment: conversation.environment,
16124
+ turnDir
16125
+ });
15704
16126
  }
15705
16127
  const boundaryTimestamp = Date.now();
15706
16128
  conversation.history.push({ role: "user", content: input.request });
15707
- await writeJson(path.join(turnDir, "request.json"), { request: input.request, boundaryTimestamp });
16129
+ await writeJson(path.join(turnDir, "request.json"), {
16130
+ request: input.request,
16131
+ boundaryTimestamp
16132
+ });
15708
16133
  let iteration = 0;
15709
16134
  let noProgressCount = 0;
15710
16135
  let previousSnapshot = null;
15711
16136
  let latestCheckpoint = null;
15712
16137
  const maxIterations = input.maxIterations || controllerBudgets.maxIterations;
15713
16138
  const autoAnswerPrompts = input.autoAnswerPrompts ?? true;
15714
- const baselineClosureId = getCurrentClosureId(cloneJson(conversation.environment.document));
16139
+ const baselineClosureId = getCurrentClosureId(
16140
+ cloneJson(conversation.environment.document)
16141
+ );
15715
16142
  while (iteration < maxIterations) {
15716
16143
  const liveDoc = cloneJson(conversation.environment.document);
15717
- const pendingPrompts = filterPromptsByBoundary(liveDoc, getOpenPromptsFromDoc(liveDoc), boundaryTimestamp);
15718
- const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, { boundaryTimestamp });
16144
+ const pendingPrompts = filterPromptsByBoundary(
16145
+ liveDoc,
16146
+ getOpenPromptsFromDoc(liveDoc),
16147
+ boundaryTimestamp
16148
+ );
16149
+ const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, {
16150
+ boundaryTimestamp
16151
+ });
15719
16152
  const systemPrompt = buildGranularAgentSystemPrompt({
15720
16153
  domainDocumentation: await conversation.environment.getDomainDocumentation(),
15721
16154
  sessionContext: {
@@ -15726,8 +16159,12 @@ function createAgentEvalHarness(options) {
15726
16159
  heapSummary: projectHeapSummary(liveDoc, {
15727
16160
  focus: workflowFocus
15728
16161
  }),
15729
- loopSummary: projectLoopSummary(liveDoc, pendingPrompts, { boundaryTimestamp }),
15730
- workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, { boundaryTimestamp }),
16162
+ loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
16163
+ boundaryTimestamp
16164
+ }),
16165
+ workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, {
16166
+ boundaryTimestamp
16167
+ }),
15731
16168
  tools: conversation.environment.getEffects().map((tool) => ({
15732
16169
  name: tool.name,
15733
16170
  description: tool.description,
@@ -15737,14 +16174,23 @@ function createAgentEvalHarness(options) {
15737
16174
  })),
15738
16175
  checkpoint: latestCheckpoint
15739
16176
  });
15740
- const request = iteration === 0 ? input.request : buildContinuationInstruction(buildContinuationPreview(latestCheckpoint, noProgressCount));
15741
- const generation = await withTimeout(generateTurnWithRepair(options.generator, {
15742
- systemPrompt,
15743
- history: buildHistory(conversation.history),
15744
- request,
15745
- attempt: 1
15746
- }), chatTimeoutMs, `chat generation for ${conversation.label} iteration ${iteration + 1}`);
15747
- await writeJson(path.join(turnDir, `iteration-${iteration + 1}-generation.json`), generation);
16177
+ const request = iteration === 0 ? input.request : buildContinuationInstruction(
16178
+ buildContinuationPreview(latestCheckpoint, noProgressCount)
16179
+ );
16180
+ const generation = await withTimeout(
16181
+ generateTurnWithRepair(options.generator, {
16182
+ systemPrompt,
16183
+ history: buildHistory(conversation.history),
16184
+ request,
16185
+ attempt: 1
16186
+ }),
16187
+ chatTimeoutMs,
16188
+ `chat generation for ${conversation.label} iteration ${iteration + 1}`
16189
+ );
16190
+ await writeJson(
16191
+ path.join(turnDir, `iteration-${iteration + 1}-generation.json`),
16192
+ generation
16193
+ );
15748
16194
  if (!generation.code) {
15749
16195
  const responseText2 = generation.reply?.trim() || "Done.";
15750
16196
  conversation.history.push({ role: "assistant", content: responseText2 });
@@ -15760,12 +16206,18 @@ function createAgentEvalHarness(options) {
15760
16206
  result: generation.reply?.trim() || responseText2
15761
16207
  };
15762
16208
  if (input.verification) {
15763
- completed.verification = await runInspection(conversation, input.verification, completed, turnDir);
16209
+ completed.verification = await runInspection(
16210
+ conversation,
16211
+ input.verification,
16212
+ completed,
16213
+ turnDir
16214
+ );
15764
16215
  }
15765
16216
  await writeJson(path.join(turnDir, "result.json"), completed);
15766
16217
  return completed;
15767
16218
  }
15768
- const job = await conversation.environment.submitJob(generation.code);
16219
+ const session = conversation.environment;
16220
+ const job = await session.submitJob(generation.code);
15769
16221
  const outcome = await waitForJobOutcome({
15770
16222
  environment: conversation.environment,
15771
16223
  job,
@@ -15790,7 +16242,9 @@ function createAgentEvalHarness(options) {
15790
16242
  };
15791
16243
  }
15792
16244
  if (!input.human) {
15793
- throw new Error("This turn reached a human prompt but no responder was provided");
16245
+ throw new Error(
16246
+ "This turn reached a human prompt but no responder was provided"
16247
+ );
15794
16248
  }
15795
16249
  let pending = {
15796
16250
  conversation,
@@ -15812,16 +16266,25 @@ function createAgentEvalHarness(options) {
15812
16266
  continue;
15813
16267
  }
15814
16268
  if (input.verification) {
15815
- resumed.verification = await runInspection(conversation, input.verification, resumed, turnDir);
16269
+ resumed.verification = await runInspection(
16270
+ conversation,
16271
+ input.verification,
16272
+ resumed,
16273
+ turnDir
16274
+ );
15816
16275
  }
15817
16276
  return resumed;
15818
16277
  }
15819
16278
  }
15820
16279
  if (outcome.kind !== "completed") {
15821
- throw new Error("Unexpected non-completed outcome after prompt handling");
16280
+ throw new Error(
16281
+ "Unexpected non-completed outcome after prompt handling"
16282
+ );
15822
16283
  }
15823
16284
  await sleep2(350);
15824
- const settledLiveDoc = cloneJson(conversation.environment.document);
16285
+ const settledLiveDoc = cloneJson(
16286
+ conversation.environment.document
16287
+ );
15825
16288
  const sessionHeap = normalizeHeapSnapshot2(asRecord4(settledLiveDoc?.heap));
15826
16289
  const presentation = resolveJobPresentation({
15827
16290
  jobId: job.id,
@@ -15842,7 +16305,11 @@ function createAgentEvalHarness(options) {
15842
16305
  baselineClosureId,
15843
16306
  currentClosureId: getCurrentClosureId(settledLiveDoc),
15844
16307
  liveDoc: settledLiveDoc,
15845
- pendingPrompts: filterPromptsByBoundary(settledLiveDoc, getOpenPromptsFromDoc(settledLiveDoc), boundaryTimestamp),
16308
+ pendingPrompts: filterPromptsByBoundary(
16309
+ settledLiveDoc,
16310
+ getOpenPromptsFromDoc(settledLiveDoc),
16311
+ boundaryTimestamp
16312
+ ),
15846
16313
  projectionOptions: { boundaryTimestamp },
15847
16314
  latestResponseText: responseText,
15848
16315
  previousSnapshot,
@@ -15867,12 +16334,15 @@ function createAgentEvalHarness(options) {
15867
16334
  jobStatus: "succeeded",
15868
16335
  jobResultPreview: JSON.stringify(outcome.result, null, 2)
15869
16336
  });
15870
- await writeJson(path.join(turnDir, `iteration-${iteration + 1}-result.json`), {
15871
- responseText,
15872
- continuation,
15873
- actionSummary: latestCheckpoint.latestActionSummary,
15874
- result: outcome.result
15875
- });
16337
+ await writeJson(
16338
+ path.join(turnDir, `iteration-${iteration + 1}-result.json`),
16339
+ {
16340
+ responseText,
16341
+ continuation,
16342
+ actionSummary: latestCheckpoint.latestActionSummary,
16343
+ result: outcome.result
16344
+ }
16345
+ );
15876
16346
  if (!continuation.shouldContinue) {
15877
16347
  const completed = {
15878
16348
  conversation,
@@ -15887,17 +16357,25 @@ function createAgentEvalHarness(options) {
15887
16357
  result: outcome.result
15888
16358
  };
15889
16359
  if (input.verification) {
15890
- completed.verification = await runInspection(conversation, input.verification, completed, turnDir);
16360
+ completed.verification = await runInspection(
16361
+ conversation,
16362
+ input.verification,
16363
+ completed,
16364
+ turnDir
16365
+ );
15891
16366
  }
15892
16367
  await writeJson(path.join(turnDir, "result.json"), completed);
15893
16368
  return completed;
15894
16369
  }
15895
16370
  iteration += 1;
15896
16371
  }
15897
- throw new Error(`Turn exceeded iteration budget (${maxIterations}) for ${conversation.label}`);
16372
+ throw new Error(
16373
+ `Turn exceeded iteration budget (${maxIterations}) for ${conversation.label}`
16374
+ );
15898
16375
  }
15899
16376
  return {
15900
16377
  artifactDir,
16378
+ granular: options.granular,
15901
16379
  openConversation,
15902
16380
  closeConversation,
15903
16381
  runTurn,
@@ -15933,12 +16411,11 @@ function createAgentTester(options) {
15933
16411
  }
15934
16412
  if ("connect" in options.target && !connectSeeded) {
15935
16413
  connectSeeded = true;
15936
- const environment = await granular.connect({
15937
- ...options.target.connect,
15938
- clientId
16414
+ const environment = await granular.openEnvironment({
16415
+ ...options.target.connect
15939
16416
  });
15940
16417
  resolvedEnvironmentId = environment.environmentId;
15941
- return environment;
16418
+ return environment.sessions.create({ clientId });
15942
16419
  }
15943
16420
  if ("createEnvironment" in options.target && !resolvedEnvironmentId) {
15944
16421
  const envData = await granular.environments.create(