@granular-software/sdk 0.4.30 → 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.
package/dist/index.mjs CHANGED
@@ -4647,27 +4647,27 @@ var Session = class {
4647
4647
  }
4648
4648
  async publishTools(tools, revision = "1.0.0") {
4649
4649
  throw new Error(
4650
- "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
4650
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4651
4651
  );
4652
4652
  }
4653
4653
  async publishEffect(effect) {
4654
4654
  throw new Error(
4655
- "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
4655
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
4656
4656
  );
4657
4657
  }
4658
4658
  async publishEffects(effects) {
4659
4659
  throw new Error(
4660
- "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
4660
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4661
4661
  );
4662
4662
  }
4663
4663
  async unpublishEffect(name) {
4664
4664
  throw new Error(
4665
- "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
4665
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
4666
4666
  );
4667
4667
  }
4668
4668
  async unpublishAllEffects() {
4669
4669
  throw new Error(
4670
- "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
4670
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
4671
4671
  );
4672
4672
  }
4673
4673
  /**
@@ -11691,6 +11691,22 @@ function normalizeHeapSnapshot(raw) {
11691
11691
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
11692
11692
  };
11693
11693
  }
11694
+ function deriveRuntimeBaseUrl(apiEndpoint) {
11695
+ try {
11696
+ const endpoint = new URL(apiEndpoint);
11697
+ const graphqlSuffix = "/orchestrator/graphql";
11698
+ if (endpoint.pathname.endsWith(graphqlSuffix)) {
11699
+ endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11700
+ } else if (endpoint.pathname.endsWith("/graphql")) {
11701
+ endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11702
+ }
11703
+ endpoint.search = "";
11704
+ endpoint.hash = "";
11705
+ return endpoint.toString().replace(/\/$/, "");
11706
+ } catch {
11707
+ return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11708
+ }
11709
+ }
11694
11710
  function normalizeSubject(subject) {
11695
11711
  const granularId = subject.granularId || subject.subjectId;
11696
11712
  const userId = subject.userId || subject.identityId || granularId;
@@ -11730,12 +11746,13 @@ function normalizeEnvironmentData(environment) {
11730
11746
  tracking: environment.tracking || buildPolicy
11731
11747
  };
11732
11748
  }
11733
- var Environment = class extends Session {
11749
+ var Environment = class {
11750
+ granular;
11734
11751
  envData;
11735
11752
  _apiKey;
11736
11753
  _apiEndpoint;
11737
- constructor(client, envData, clientId, apiKey, apiEndpoint) {
11738
- super(client, clientId);
11754
+ constructor(granular, envData, apiKey, apiEndpoint) {
11755
+ this.granular = granular;
11739
11756
  this.envData = envData;
11740
11757
  this._apiKey = apiKey;
11741
11758
  this._apiEndpoint = apiEndpoint;
@@ -11776,35 +11793,126 @@ var Environment = class extends Session {
11776
11793
  get permissionProfileId() {
11777
11794
  return this.envData.permissionProfileId;
11778
11795
  }
11796
+ /** The current build policy backing this environment */
11797
+ get buildPolicy() {
11798
+ return this.envData.buildPolicy;
11799
+ }
11800
+ /** The current update state relative to the followed tag */
11801
+ get updateState() {
11802
+ return this.envData.updateState;
11803
+ }
11804
+ /** Convenience flag for whether this environment trails the current tag target */
11805
+ get isOutdated() {
11806
+ return this.envData.updateState === "update_available";
11807
+ }
11808
+ /** The followed tag name when this environment is tag-tracked */
11809
+ get tag() {
11810
+ return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
11811
+ }
11779
11812
  /** The GraphQL API endpoint URL */
11780
11813
  get apiEndpoint() {
11781
11814
  return this._apiEndpoint;
11782
11815
  }
11816
+ /** Internal auth token used for control-plane and runtime fallback requests */
11817
+ get authToken() {
11818
+ return this._apiKey;
11819
+ }
11820
+ /** Base runtime URL derived from the GraphQL endpoint */
11821
+ get runtimeBaseUrl() {
11822
+ return this.getRuntimeBaseUrl();
11823
+ }
11824
+ get sessions() {
11825
+ return {
11826
+ list: async (options) => this.listSessions(options?.status || "active"),
11827
+ create: async (options) => this.createSession(options),
11828
+ connect: async (sessionId, options) => this.connectSession(sessionId, options),
11829
+ reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
11830
+ close: async (sessionId, session) => this.closeSession(sessionId, session)
11831
+ };
11832
+ }
11833
+ get data() {
11834
+ return {
11835
+ record: async (record) => this.recordObject(record),
11836
+ recordMany: async (records, options) => this.recordObjects(records, options),
11837
+ import: async (records, options) => this.enqueueRecordImport(records, options),
11838
+ listImports: async (status) => this.listRecordImports(status),
11839
+ getImport: async (importId) => this.getRecordImport(importId),
11840
+ getImportSummary: async () => this.getRecordImportSummary(),
11841
+ cancelImport: async (importId) => this.cancelRecordImport(importId),
11842
+ getAwaitingCount: async () => this.getAwaitingRecordCount()
11843
+ };
11844
+ }
11845
+ get feedback() {
11846
+ return {
11847
+ list: async () => this.listFeedback()
11848
+ };
11849
+ }
11783
11850
  /**
11784
- * Return a plain JS snapshot of the synced session heap.
11785
- *
11786
- * The heap lives in the Automerge document, so this method does not perform
11787
- * any extra network roundtrip.
11851
+ * Sessionless environments do not own a live transport, so disconnecting the
11852
+ * environment handle itself is a no-op. This keeps the public surface
11853
+ * symmetric with `EnvironmentSession.disconnect()` and lets callers always
11854
+ * clean up safely without tracking whether they currently hold an environment
11855
+ * or a session.
11788
11856
  */
11789
- getHeap() {
11790
- const doc = this.document;
11791
- return normalizeHeapSnapshot(doc?.heap);
11857
+ async disconnect() {
11792
11858
  }
11793
- getRuntimeBaseUrl() {
11794
- try {
11795
- const endpoint = new URL(this._apiEndpoint);
11796
- const graphqlSuffix = "/orchestrator/graphql";
11797
- if (endpoint.pathname.endsWith(graphqlSuffix)) {
11798
- endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11799
- } else if (endpoint.pathname.endsWith("/graphql")) {
11800
- endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11801
- }
11802
- endpoint.search = "";
11803
- endpoint.hash = "";
11804
- return endpoint.toString().replace(/\/$/, "");
11805
- } catch {
11806
- return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11859
+ async listSessions(status = "active") {
11860
+ if (status === "all") {
11861
+ const [active, closed] = await Promise.all([
11862
+ this.granular.listOpenSessions({ environmentId: this.environmentId }),
11863
+ this.granular.listClosedSessions({ environmentId: this.environmentId })
11864
+ ]);
11865
+ return [...active, ...closed].sort(
11866
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
11867
+ );
11807
11868
  }
11869
+ return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
11870
+ }
11871
+ async createSession(options) {
11872
+ return this.granular.createSession({
11873
+ environmentId: this.environmentId,
11874
+ clientId: options?.clientId,
11875
+ initialHeap: options?.initialHeap
11876
+ });
11877
+ }
11878
+ async connectSession(sessionId, options) {
11879
+ const session = await this.granular["connectSession"]({
11880
+ sessionId,
11881
+ clientId: options?.clientId
11882
+ });
11883
+ if (session.environmentId !== this.environmentId) {
11884
+ await session.disconnect().catch(() => {
11885
+ session.disconnectTransport();
11886
+ });
11887
+ throw new Error(
11888
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11889
+ );
11890
+ }
11891
+ return session;
11892
+ }
11893
+ async reopenSession(sessionId, options) {
11894
+ const session = await this.granular.reopenSession(sessionId, {
11895
+ clientId: options?.clientId
11896
+ });
11897
+ if (session.environmentId !== this.environmentId) {
11898
+ await session.disconnect().catch(() => {
11899
+ session.disconnectTransport();
11900
+ });
11901
+ throw new Error(
11902
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11903
+ );
11904
+ }
11905
+ return session;
11906
+ }
11907
+ async closeSession(sessionId, session) {
11908
+ await this.granular.closeSession(sessionId, session);
11909
+ }
11910
+ async listFeedback() {
11911
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
11912
+ return Array.isArray(response.items) ? response.items : [];
11913
+ }
11914
+ getRuntimeBaseUrl() {
11915
+ return deriveRuntimeBaseUrl(this._apiEndpoint);
11808
11916
  }
11809
11917
  async controlPlaneRequest(path, options = {}) {
11810
11918
  const runtimeBase = this.getRuntimeBaseUrl();
@@ -11825,95 +11933,6 @@ var Environment = class extends Session {
11825
11933
  }
11826
11934
  return response.json();
11827
11935
  }
11828
- /**
11829
- * Close the session and disconnect from the sandbox.
11830
- *
11831
- * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
11832
- * to the runtime goodbye endpoint if no definitive WS-side runtime notify
11833
- * acknowledgement was observed.
11834
- */
11835
- async disconnect() {
11836
- let wsNotifiedRuntime = false;
11837
- try {
11838
- const goodbye = await this.rpc(
11839
- "client.goodbye",
11840
- {
11841
- timestamp: Date.now()
11842
- }
11843
- );
11844
- wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
11845
- } catch {
11846
- wsNotifiedRuntime = false;
11847
- }
11848
- if (!wsNotifiedRuntime) {
11849
- try {
11850
- const runtimeBase = this.getRuntimeBaseUrl();
11851
- await fetch(
11852
- `${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
11853
- {
11854
- method: "POST",
11855
- headers: {
11856
- "Content-Type": "application/json",
11857
- Authorization: `Bearer ${this._apiKey}`,
11858
- Connection: "close"
11859
- },
11860
- body: JSON.stringify({
11861
- reason: "sdk_disconnect_http_fallback",
11862
- sessionId: this.client.currentSessionId
11863
- })
11864
- }
11865
- );
11866
- } catch {
11867
- }
11868
- }
11869
- this.client.disconnect();
11870
- }
11871
- /**
11872
- * Close only the socket transport without sending `client.goodbye`.
11873
- *
11874
- * Use this when the caller intends to immediately reattach to the same
11875
- * session after an unexpected disconnect.
11876
- */
11877
- disconnectTransport() {
11878
- this.client.disconnect();
11879
- }
11880
- // ==================== GRAPH CONTAINER READINESS ====================
11881
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
11882
- graphContainerStatus = null;
11883
- /**
11884
- * Check if the graph container is ready and warm.
11885
- *
11886
- * Sends a lightweight heartbeat RPC to the Session DO which internally
11887
- * pings the FalkorDB container. The response includes `graphContainerStatus`,
11888
- * which is stored locally and emitted as a `readiness` event.
11889
- *
11890
- * Use this method to proactively warm the graph container before any
11891
- * GraphQL query that requires it, or to poll the container's state in
11892
- * the background.
11893
- *
11894
- * @returns The current graph container status object
11895
- *
11896
- * @example
11897
- * ```typescript
11898
- * const status = await env.checkReadiness();
11899
- * console.log(status.status); // 'hot' | 'warming' | 'unknown'
11900
- *
11901
- * // Or listen for live updates
11902
- * env.on('readiness', (status) => {
11903
- * console.log('Graph is now:', status.status);
11904
- * });
11905
- * ```
11906
- */
11907
- async checkReadiness() {
11908
- const result = await this.client.call("client.heartbeat", {});
11909
- const containerStatus = result?.graphContainerStatus ?? {
11910
- lastKeepAliveAt: Date.now(),
11911
- status: "unknown"
11912
- };
11913
- this.graphContainerStatus = containerStatus;
11914
- this.emit("readiness", containerStatus);
11915
- return containerStatus;
11916
- }
11917
11936
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
11918
11937
  /**
11919
11938
  * Convert a class name + real-world ID into a unique graph path.
@@ -12774,36 +12793,186 @@ var Environment = class extends Session {
12774
12793
  }
12775
12794
  );
12776
12795
  }
12777
- // ==================== PUBLISH TOOLS ====================
12796
+ };
12797
+ var EnvironmentSession = class extends Session {
12798
+ environment;
12799
+ /** The last known graph container status, updated by checkReadiness() or on heartbeat */
12800
+ graphContainerStatus = null;
12801
+ constructor(client, environment, clientId) {
12802
+ super(client, clientId);
12803
+ this.environment = environment;
12804
+ }
12805
+ get environmentId() {
12806
+ return this.environment.environmentId;
12807
+ }
12808
+ get sandboxId() {
12809
+ return this.environment.sandboxId;
12810
+ }
12811
+ get ontologyId() {
12812
+ return this.environment.ontologyId;
12813
+ }
12814
+ get subjectId() {
12815
+ return this.environment.subjectId;
12816
+ }
12817
+ get envName() {
12818
+ return this.environment.envName;
12819
+ }
12820
+ get versionId() {
12821
+ return this.environment.versionId;
12822
+ }
12823
+ get granularId() {
12824
+ return this.environment.granularId;
12825
+ }
12826
+ get permissionProfileId() {
12827
+ return this.environment.permissionProfileId;
12828
+ }
12829
+ get apiEndpoint() {
12830
+ return this.environment.apiEndpoint;
12831
+ }
12832
+ get data() {
12833
+ return this.environment.data;
12834
+ }
12835
+ get feedback() {
12836
+ return this.environment.feedback;
12837
+ }
12778
12838
  /**
12779
- * Removed: environment-scoped effect publication is no longer supported.
12839
+ * Return a plain JS snapshot of the synced session heap.
12780
12840
  */
12781
- async publishTools(tools, revision = "1.0.0") {
12782
- return super.publishTools(tools, revision);
12841
+ getHeap() {
12842
+ const doc = this.document;
12843
+ return normalizeHeapSnapshot(doc?.heap);
12844
+ }
12845
+ async graphql(query, variables) {
12846
+ return this.environment.graphql(query, variables);
12847
+ }
12848
+ async defineRelationship(options) {
12849
+ return this.environment.defineRelationship(options);
12850
+ }
12851
+ async getRelationships(modelPath) {
12852
+ return this.environment.getRelationships(modelPath);
12853
+ }
12854
+ async attach(modelPath, submodelPath, targetPath) {
12855
+ return this.environment.attach(modelPath, submodelPath, targetPath);
12856
+ }
12857
+ async detach(modelPath, submodelPath, targetPath) {
12858
+ return this.environment.detach(modelPath, submodelPath, targetPath);
12859
+ }
12860
+ async listRelated(modelPath, submodelPath) {
12861
+ return this.environment.listRelated(modelPath, submodelPath);
12862
+ }
12863
+ async applyManifest(manifest) {
12864
+ return this.environment.applyManifest(manifest);
12865
+ }
12866
+ async recordObject(options) {
12867
+ return this.environment.recordObject(options);
12868
+ }
12869
+ async recordObjects(records, options) {
12870
+ return this.environment.recordObjects(records, options);
12871
+ }
12872
+ async enqueueRecordImport(records, options = {}) {
12873
+ return this.environment.enqueueRecordImport(records, options);
12874
+ }
12875
+ async listRecordImports(status) {
12876
+ return this.environment.listRecordImports(status);
12877
+ }
12878
+ async getRecordImportSummary() {
12879
+ return this.environment.getRecordImportSummary();
12880
+ }
12881
+ async getAwaitingRecordCount() {
12882
+ return this.environment.getAwaitingRecordCount();
12883
+ }
12884
+ async getRecordImport(importId) {
12885
+ return this.environment.getRecordImport(importId);
12886
+ }
12887
+ async cancelRecordImport(importId) {
12888
+ return this.environment.cancelRecordImport(importId);
12889
+ }
12890
+ async listFeedback() {
12891
+ return this.environment.listFeedback();
12783
12892
  }
12784
12893
  /**
12785
- * Removed: environment-scoped effect publication is no longer supported.
12894
+ * Close the session and disconnect from the sandbox.
12895
+ *
12896
+ * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
12897
+ * to the runtime goodbye endpoint if no definitive WS-side runtime notify
12898
+ * acknowledgement was observed.
12786
12899
  */
12787
- async publishEffect(effect) {
12788
- return super.publishEffect(effect);
12900
+ async disconnect() {
12901
+ let wsNotifiedRuntime = false;
12902
+ try {
12903
+ const goodbye = await this.rpc(
12904
+ "client.goodbye",
12905
+ {
12906
+ timestamp: Date.now()
12907
+ }
12908
+ );
12909
+ wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
12910
+ } catch {
12911
+ wsNotifiedRuntime = false;
12912
+ }
12913
+ if (!wsNotifiedRuntime) {
12914
+ try {
12915
+ await fetch(
12916
+ `${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
12917
+ {
12918
+ method: "POST",
12919
+ headers: {
12920
+ "Content-Type": "application/json",
12921
+ Authorization: `Bearer ${this.environment.authToken}`,
12922
+ Connection: "close"
12923
+ },
12924
+ body: JSON.stringify({
12925
+ reason: "sdk_disconnect_http_fallback",
12926
+ sessionId: this.client.currentSessionId
12927
+ })
12928
+ }
12929
+ );
12930
+ } catch {
12931
+ }
12932
+ }
12933
+ this.client.disconnect();
12789
12934
  }
12790
12935
  /**
12791
- * Removed: environment-scoped effect publication is no longer supported.
12936
+ * Close only the socket transport without sending `client.goodbye`.
12792
12937
  */
12793
- async publishEffects(effects) {
12794
- return super.publishEffects(effects);
12938
+ disconnectTransport() {
12939
+ this.client.disconnect();
12795
12940
  }
12796
12941
  /**
12797
- * Removed: environment-scoped effect publication is no longer supported.
12942
+ * Backwards-compatible alias for `disconnect()`.
12798
12943
  */
12799
- async unpublishEffect(name) {
12800
- return super.unpublishEffect(name);
12944
+ async close() {
12945
+ await this.disconnect();
12801
12946
  }
12802
12947
  /**
12803
- * Removed: environment-scoped effect publication is no longer supported.
12948
+ * Check if the graph container is ready and warm.
12804
12949
  */
12805
- async unpublishAllEffects() {
12806
- return super.unpublishAllEffects();
12950
+ async checkReadiness() {
12951
+ const result = await this.client.call("client.heartbeat", {});
12952
+ const containerStatus = result?.graphContainerStatus ?? {
12953
+ lastKeepAliveAt: Date.now(),
12954
+ status: "unknown"
12955
+ };
12956
+ this.graphContainerStatus = containerStatus;
12957
+ this.emit("readiness", containerStatus);
12958
+ return containerStatus;
12959
+ }
12960
+ };
12961
+ var OntologyHandle = class {
12962
+ granular;
12963
+ ontologyNameOrId;
12964
+ constructor(granular, ontologyNameOrId) {
12965
+ this.granular = granular;
12966
+ this.ontologyNameOrId = ontologyNameOrId;
12967
+ }
12968
+ get effects() {
12969
+ return {
12970
+ register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
12971
+ registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
12972
+ unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
12973
+ clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
12974
+ disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
12975
+ };
12807
12976
  }
12808
12977
  };
12809
12978
  var Granular = class _Granular {
@@ -12840,6 +13009,12 @@ var Granular = class _Granular {
12840
13009
  this.onReconnectError = options.onReconnectError;
12841
13010
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
12842
13011
  }
13012
+ /**
13013
+ * Return an ontology-scoped handle for effects and other ontology-level APIs.
13014
+ */
13015
+ ontology(ontologyNameOrId) {
13016
+ return new OntologyHandle(this, ontologyNameOrId);
13017
+ }
12843
13018
  /**
12844
13019
  * Records/upserts a user and prepares them for sandbox connections
12845
13020
  *
@@ -12876,7 +13051,23 @@ var Granular = class _Granular {
12876
13051
  permissions: options.permissions || []
12877
13052
  });
12878
13053
  }
13054
+ /**
13055
+ * Alias for `recordUser()` with user-facing naming that matches upsert semantics.
13056
+ */
13057
+ async upsertUser(options) {
13058
+ return this.recordUser(options);
13059
+ }
12879
13060
  async resolveConnectUser(options) {
13061
+ const providedIdentityCount = [
13062
+ Boolean(options.user),
13063
+ Boolean(options.userId),
13064
+ Boolean(options.granularId)
13065
+ ].filter(Boolean).length;
13066
+ if (providedIdentityCount !== 1) {
13067
+ throw new Error(
13068
+ "openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
13069
+ );
13070
+ }
12880
13071
  if (options.user) {
12881
13072
  const user = normalizeUser(options.user);
12882
13073
  return {
@@ -12913,56 +13104,85 @@ var Granular = class _Granular {
12913
13104
  };
12914
13105
  }
12915
13106
  throw new Error(
12916
- "connect() requires either userId, granularId, or a user object returned by recordUser()."
13107
+ "openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
12917
13108
  );
12918
13109
  }
12919
13110
  /**
12920
- * Connect to an ontology environment and establish a real-time session.
12921
- *
12922
- * Effects are registered at the sandbox level via `granular.registerEffect()`
12923
- * or `granular.registerEffects()`. Sessions pick up live availability from
12924
- * the sandbox registry automatically.
12925
- *
12926
- * @param options - Connection options
12927
- * @returns An active environment session
13111
+ * Open or resolve an ontology environment for one user without opening a session.
12928
13112
  *
12929
13113
  * @example
12930
13114
  * ```typescript
12931
- * const environment = await granular.connect({
13115
+ * const environment = await granular.openEnvironment({
12932
13116
  * ontology: 'my-ontology',
12933
- * environment: 'dev',
13117
+ * tag: 'dev',
12934
13118
  * userId: 'user_123',
12935
13119
  * permissions: ['agent'],
12936
13120
  * });
12937
13121
  *
12938
- * await granular.registerEffect('my-sandbox', {
12939
- * name: 'greet',
12940
- * description: 'Say hello',
12941
- * inputSchema: { type: 'object', properties: {} },
12942
- * handler: async () => 'Hello!',
13122
+ * await environment.data.record({
13123
+ * className: 'customer',
13124
+ * id: 'acme',
13125
+ * fields: { name: 'Acme' },
12943
13126
  * });
12944
13127
  *
12945
- * // Submit job
12946
- * const job = await environment.submitJob(`
12947
- * import { tools } from './sandbox-tools';
12948
- * return await tools.greet({});
12949
- * `);
12950
- *
12951
- * console.log(await job.result); // 'Hello!'
13128
+ * const session = await environment.sessions.create();
13129
+ * const job = await session.submitJob(`return "hello";`);
13130
+ * console.log(await job.result);
12952
13131
  * ```
12953
13132
  */
13133
+ async openEnvironment(options) {
13134
+ const envData = await this.resolveOpenEnvironmentData(
13135
+ options,
13136
+ "openEnvironment"
13137
+ );
13138
+ return this.bindEnvironmentHandle(envData);
13139
+ }
13140
+ /**
13141
+ * Deprecated compatibility alias for `openEnvironment()`.
13142
+ *
13143
+ * `connect()` no longer opens a runtime session automatically.
13144
+ */
12954
13145
  async connect(options) {
12955
- const clientId = options.clientId || `client_${Date.now()}`;
13146
+ return this.openEnvironment({
13147
+ ...options,
13148
+ tag: this.resolveRequestedTag(options, "connect"),
13149
+ permissions: options.permissions || options.user?.permissions || []
13150
+ });
13151
+ }
13152
+ resolveRequestedTag(options, methodName) {
13153
+ const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
13154
+ if (!tag) {
13155
+ throw new Error(`${methodName}() requires \`tag\`.`);
13156
+ }
13157
+ return tag;
13158
+ }
13159
+ buildManagedEnvironmentName(tag, versionId) {
13160
+ return `__sdk__${tag}__${versionId}`;
13161
+ }
13162
+ matchesTagTrackedEnvironment(environment, tagName, tagId) {
13163
+ const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
13164
+ 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);
13165
+ }
13166
+ sortEnvironmentsByRecency(environments) {
13167
+ return [...environments].sort(
13168
+ (left, right) => right.updatedAt - left.updatedAt
13169
+ );
13170
+ }
13171
+ async resolveOpenEnvironmentData(options, methodName) {
12956
13172
  const ontology = options.ontology;
12957
13173
  if (!ontology) {
12958
- throw new Error("connect() requires `ontology`.");
13174
+ throw new Error(`${methodName}() requires \`ontology\`.`);
12959
13175
  }
12960
- const environmentName = options.environment;
12961
- if (!environmentName) {
12962
- throw new Error("connect() requires `environment`.");
13176
+ const tagName = options.tag?.trim();
13177
+ if (!tagName) {
13178
+ throw new Error(`${methodName}() requires \`tag\`.`);
12963
13179
  }
12964
- const tagName = options.tagName?.trim() || void 0;
12965
13180
  const user = await this.resolveConnectUser(options);
13181
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
13182
+ throw new Error(
13183
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
13184
+ );
13185
+ }
12966
13186
  const sandbox = await this.findOrCreateSandbox(ontology);
12967
13187
  for (const profileName of user.permissions) {
12968
13188
  const profileId = await this.ensurePermissionProfile(
@@ -12975,22 +13195,49 @@ var Granular = class _Granular {
12975
13195
  profileId
12976
13196
  );
12977
13197
  }
12978
- const envData = await this.environments.create(sandbox.sandboxId, {
13198
+ const tags = await this.request(
13199
+ `/control/sandboxes/${sandbox.sandboxId}/tags`
13200
+ );
13201
+ const tag = (tags.items || []).find(
13202
+ (candidate) => Boolean(candidate?.name === tagName)
13203
+ );
13204
+ if (!tag) {
13205
+ throw new Error(
13206
+ `Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
13207
+ );
13208
+ }
13209
+ const targetVersionId = tag.targetVersionId || tag.targetBuildId;
13210
+ if (!targetVersionId) {
13211
+ throw new Error(
13212
+ `Tag "${tagName}" does not currently point to a build/version.`
13213
+ );
13214
+ }
13215
+ const allEnvironments = await this.environments.list(sandbox.sandboxId);
13216
+ const userEnvironments = allEnvironments.filter(
13217
+ (environment) => environment.subjectId === user.granularId
13218
+ );
13219
+ const currentMatches = this.sortEnvironmentsByRecency(
13220
+ userEnvironments.filter(
13221
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
13222
+ )
13223
+ );
13224
+ if (currentMatches.length > 0) {
13225
+ return currentMatches[0];
13226
+ }
13227
+ const outdatedMatches = this.sortEnvironmentsByRecency(
13228
+ userEnvironments.filter(
13229
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
13230
+ )
13231
+ );
13232
+ if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13233
+ return outdatedMatches[0];
13234
+ }
13235
+ return this.environments.create(sandbox.sandboxId, {
12979
13236
  subjectId: user.granularId,
12980
- environment: environmentName,
12981
- tagName,
13237
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13238
+ tagId: tag.tagId,
12982
13239
  permissionProfileId: null
12983
13240
  });
12984
- await this.activateEnvironment(envData.environmentId);
12985
- const session = await this.request("/ws/sessions", {
12986
- method: "POST",
12987
- body: JSON.stringify({
12988
- environmentId: envData.environmentId,
12989
- clientId,
12990
- initialHeap: options.initialHeap
12991
- })
12992
- });
12993
- return this.bindWebSocketEnvironment(envData, clientId, session);
12994
13241
  }
12995
13242
  /**
12996
13243
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -13053,6 +13300,7 @@ var Granular = class _Granular {
13053
13300
  const clientId = options.clientId || `client_${Date.now()}`;
13054
13301
  await this.activateEnvironment(options.environmentId);
13055
13302
  const envData = await this.environments.get(options.environmentId);
13303
+ const environment = this.bindEnvironmentHandle(envData);
13056
13304
  const session = await this.request("/ws/sessions", {
13057
13305
  method: "POST",
13058
13306
  body: JSON.stringify({
@@ -13061,7 +13309,7 @@ var Granular = class _Granular {
13061
13309
  initialHeap: options.initialHeap
13062
13310
  })
13063
13311
  });
13064
- return this.bindWebSocketEnvironment(envData, clientId, session);
13312
+ return this.bindWebSocketEnvironmentSession(environment, clientId, session);
13065
13313
  }
13066
13314
  /**
13067
13315
  * Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
@@ -13073,7 +13321,8 @@ var Granular = class _Granular {
13073
13321
  body: JSON.stringify({})
13074
13322
  });
13075
13323
  const envData = await this.environments.get(minted.environmentId);
13076
- return this.bindWebSocketEnvironment(envData, clientId, minted);
13324
+ const environment = this.bindEnvironmentHandle(envData);
13325
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
13077
13326
  }
13078
13327
  /**
13079
13328
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
@@ -13105,7 +13354,11 @@ var Granular = class _Granular {
13105
13354
  });
13106
13355
  return this.connectSession({ sessionId, clientId: options?.clientId });
13107
13356
  }
13108
- async bindWebSocketEnvironment(envData, clientId, session) {
13357
+ bindEnvironmentHandle(envData) {
13358
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13359
+ return new Environment(this, envData, this.apiKey, graphqlEndpoint);
13360
+ }
13361
+ async bindWebSocketEnvironmentSession(environment, clientId, session) {
13109
13362
  const client = new WSClient({
13110
13363
  url: session.wsUrl,
13111
13364
  sessionId: session.sessionId,
@@ -13116,16 +13369,13 @@ var Granular = class _Granular {
13116
13369
  onReconnectError: this.onReconnectError
13117
13370
  });
13118
13371
  await client.connect();
13119
- const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13120
- const environment = new Environment(
13372
+ const environmentSession = new EnvironmentSession(
13121
13373
  client,
13122
- envData,
13123
- clientId,
13124
- this.apiKey,
13125
- graphqlEndpoint
13374
+ environment,
13375
+ clientId
13126
13376
  );
13127
- await environment.hello();
13128
- return environment;
13377
+ await environmentSession.hello();
13378
+ return environmentSession;
13129
13379
  }
13130
13380
  async activateEnvironment(environmentId) {
13131
13381
  await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
@@ -15516,6 +15766,6 @@ function buildSessionTranscript(input) {
15516
15766
  });
15517
15767
  }
15518
15768
 
15519
- export { Environment, Granular, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentReferentBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildSessionTranscript, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizePrompt, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch };
15769
+ export { Environment, EnvironmentSession, Granular, OntologyHandle, Session, WSClient, buildContinuationInstruction, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentReferentBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildSessionTranscript, createHarnessVerifierSnapshot, evaluateContinuation, extractPromptTokens, getCurrentClosureId, getExclusivePromptTarget, hasOpenPrompt, invokeRegisteredEffect, isLocalApiUrl, normalizeEffectBehaviors, normalizePrompt, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectWorkflowFocus, projectWorkflowSummary, resolveApiUrl, resolveAuthTokenForApiUrl, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch };
15520
15770
  //# sourceMappingURL=index.mjs.map
15521
15771
  //# sourceMappingURL=index.mjs.map