@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.js CHANGED
@@ -4669,27 +4669,27 @@ var Session = class {
4669
4669
  }
4670
4670
  async publishTools(tools, revision = "1.0.0") {
4671
4671
  throw new Error(
4672
- "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
4672
+ "Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4673
4673
  );
4674
4674
  }
4675
4675
  async publishEffect(effect) {
4676
4676
  throw new Error(
4677
- "Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
4677
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
4678
4678
  );
4679
4679
  }
4680
4680
  async publishEffects(effects) {
4681
4681
  throw new Error(
4682
- "Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
4682
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
4683
4683
  );
4684
4684
  }
4685
4685
  async unpublishEffect(name) {
4686
4686
  throw new Error(
4687
- "Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
4687
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
4688
4688
  );
4689
4689
  }
4690
4690
  async unpublishAllEffects() {
4691
4691
  throw new Error(
4692
- "Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
4692
+ "Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
4693
4693
  );
4694
4694
  }
4695
4695
  /**
@@ -11713,6 +11713,22 @@ function normalizeHeapSnapshot(raw) {
11713
11713
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
11714
11714
  };
11715
11715
  }
11716
+ function deriveRuntimeBaseUrl(apiEndpoint) {
11717
+ try {
11718
+ const endpoint = new URL(apiEndpoint);
11719
+ const graphqlSuffix = "/orchestrator/graphql";
11720
+ if (endpoint.pathname.endsWith(graphqlSuffix)) {
11721
+ endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11722
+ } else if (endpoint.pathname.endsWith("/graphql")) {
11723
+ endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11724
+ }
11725
+ endpoint.search = "";
11726
+ endpoint.hash = "";
11727
+ return endpoint.toString().replace(/\/$/, "");
11728
+ } catch {
11729
+ return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11730
+ }
11731
+ }
11716
11732
  function normalizeSubject(subject) {
11717
11733
  const granularId = subject.granularId || subject.subjectId;
11718
11734
  const userId = subject.userId || subject.identityId || granularId;
@@ -11752,12 +11768,13 @@ function normalizeEnvironmentData(environment) {
11752
11768
  tracking: environment.tracking || buildPolicy
11753
11769
  };
11754
11770
  }
11755
- var Environment = class extends Session {
11771
+ var Environment = class {
11772
+ granular;
11756
11773
  envData;
11757
11774
  _apiKey;
11758
11775
  _apiEndpoint;
11759
- constructor(client, envData, clientId, apiKey, apiEndpoint) {
11760
- super(client, clientId);
11776
+ constructor(granular, envData, apiKey, apiEndpoint) {
11777
+ this.granular = granular;
11761
11778
  this.envData = envData;
11762
11779
  this._apiKey = apiKey;
11763
11780
  this._apiEndpoint = apiEndpoint;
@@ -11798,35 +11815,126 @@ var Environment = class extends Session {
11798
11815
  get permissionProfileId() {
11799
11816
  return this.envData.permissionProfileId;
11800
11817
  }
11818
+ /** The current build policy backing this environment */
11819
+ get buildPolicy() {
11820
+ return this.envData.buildPolicy;
11821
+ }
11822
+ /** The current update state relative to the followed tag */
11823
+ get updateState() {
11824
+ return this.envData.updateState;
11825
+ }
11826
+ /** Convenience flag for whether this environment trails the current tag target */
11827
+ get isOutdated() {
11828
+ return this.envData.updateState === "update_available";
11829
+ }
11830
+ /** The followed tag name when this environment is tag-tracked */
11831
+ get tag() {
11832
+ return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
11833
+ }
11801
11834
  /** The GraphQL API endpoint URL */
11802
11835
  get apiEndpoint() {
11803
11836
  return this._apiEndpoint;
11804
11837
  }
11838
+ /** Internal auth token used for control-plane and runtime fallback requests */
11839
+ get authToken() {
11840
+ return this._apiKey;
11841
+ }
11842
+ /** Base runtime URL derived from the GraphQL endpoint */
11843
+ get runtimeBaseUrl() {
11844
+ return this.getRuntimeBaseUrl();
11845
+ }
11846
+ get sessions() {
11847
+ return {
11848
+ list: async (options) => this.listSessions(options?.status || "active"),
11849
+ create: async (options) => this.createSession(options),
11850
+ connect: async (sessionId, options) => this.connectSession(sessionId, options),
11851
+ reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
11852
+ close: async (sessionId, session) => this.closeSession(sessionId, session)
11853
+ };
11854
+ }
11855
+ get data() {
11856
+ return {
11857
+ record: async (record) => this.recordObject(record),
11858
+ recordMany: async (records, options) => this.recordObjects(records, options),
11859
+ import: async (records, options) => this.enqueueRecordImport(records, options),
11860
+ listImports: async (status) => this.listRecordImports(status),
11861
+ getImport: async (importId) => this.getRecordImport(importId),
11862
+ getImportSummary: async () => this.getRecordImportSummary(),
11863
+ cancelImport: async (importId) => this.cancelRecordImport(importId),
11864
+ getAwaitingCount: async () => this.getAwaitingRecordCount()
11865
+ };
11866
+ }
11867
+ get feedback() {
11868
+ return {
11869
+ list: async () => this.listFeedback()
11870
+ };
11871
+ }
11805
11872
  /**
11806
- * Return a plain JS snapshot of the synced session heap.
11807
- *
11808
- * The heap lives in the Automerge document, so this method does not perform
11809
- * any extra network roundtrip.
11873
+ * Sessionless environments do not own a live transport, so disconnecting the
11874
+ * environment handle itself is a no-op. This keeps the public surface
11875
+ * symmetric with `EnvironmentSession.disconnect()` and lets callers always
11876
+ * clean up safely without tracking whether they currently hold an environment
11877
+ * or a session.
11810
11878
  */
11811
- getHeap() {
11812
- const doc = this.document;
11813
- return normalizeHeapSnapshot(doc?.heap);
11879
+ async disconnect() {
11814
11880
  }
11815
- getRuntimeBaseUrl() {
11816
- try {
11817
- const endpoint = new URL(this._apiEndpoint);
11818
- const graphqlSuffix = "/orchestrator/graphql";
11819
- if (endpoint.pathname.endsWith(graphqlSuffix)) {
11820
- endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
11821
- } else if (endpoint.pathname.endsWith("/graphql")) {
11822
- endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
11823
- }
11824
- endpoint.search = "";
11825
- endpoint.hash = "";
11826
- return endpoint.toString().replace(/\/$/, "");
11827
- } catch {
11828
- return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
11881
+ async listSessions(status = "active") {
11882
+ if (status === "all") {
11883
+ const [active, closed] = await Promise.all([
11884
+ this.granular.listOpenSessions({ environmentId: this.environmentId }),
11885
+ this.granular.listClosedSessions({ environmentId: this.environmentId })
11886
+ ]);
11887
+ return [...active, ...closed].sort(
11888
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
11889
+ );
11829
11890
  }
11891
+ return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
11892
+ }
11893
+ async createSession(options) {
11894
+ return this.granular.createSession({
11895
+ environmentId: this.environmentId,
11896
+ clientId: options?.clientId,
11897
+ initialHeap: options?.initialHeap
11898
+ });
11899
+ }
11900
+ async connectSession(sessionId, options) {
11901
+ const session = await this.granular["connectSession"]({
11902
+ sessionId,
11903
+ clientId: options?.clientId
11904
+ });
11905
+ if (session.environmentId !== this.environmentId) {
11906
+ await session.disconnect().catch(() => {
11907
+ session.disconnectTransport();
11908
+ });
11909
+ throw new Error(
11910
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11911
+ );
11912
+ }
11913
+ return session;
11914
+ }
11915
+ async reopenSession(sessionId, options) {
11916
+ const session = await this.granular.reopenSession(sessionId, {
11917
+ clientId: options?.clientId
11918
+ });
11919
+ if (session.environmentId !== this.environmentId) {
11920
+ await session.disconnect().catch(() => {
11921
+ session.disconnectTransport();
11922
+ });
11923
+ throw new Error(
11924
+ `Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
11925
+ );
11926
+ }
11927
+ return session;
11928
+ }
11929
+ async closeSession(sessionId, session) {
11930
+ await this.granular.closeSession(sessionId, session);
11931
+ }
11932
+ async listFeedback() {
11933
+ const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
11934
+ return Array.isArray(response.items) ? response.items : [];
11935
+ }
11936
+ getRuntimeBaseUrl() {
11937
+ return deriveRuntimeBaseUrl(this._apiEndpoint);
11830
11938
  }
11831
11939
  async controlPlaneRequest(path, options = {}) {
11832
11940
  const runtimeBase = this.getRuntimeBaseUrl();
@@ -11847,95 +11955,6 @@ var Environment = class extends Session {
11847
11955
  }
11848
11956
  return response.json();
11849
11957
  }
11850
- /**
11851
- * Close the session and disconnect from the sandbox.
11852
- *
11853
- * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
11854
- * to the runtime goodbye endpoint if no definitive WS-side runtime notify
11855
- * acknowledgement was observed.
11856
- */
11857
- async disconnect() {
11858
- let wsNotifiedRuntime = false;
11859
- try {
11860
- const goodbye = await this.rpc(
11861
- "client.goodbye",
11862
- {
11863
- timestamp: Date.now()
11864
- }
11865
- );
11866
- wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
11867
- } catch {
11868
- wsNotifiedRuntime = false;
11869
- }
11870
- if (!wsNotifiedRuntime) {
11871
- try {
11872
- const runtimeBase = this.getRuntimeBaseUrl();
11873
- await fetch(
11874
- `${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
11875
- {
11876
- method: "POST",
11877
- headers: {
11878
- "Content-Type": "application/json",
11879
- Authorization: `Bearer ${this._apiKey}`,
11880
- Connection: "close"
11881
- },
11882
- body: JSON.stringify({
11883
- reason: "sdk_disconnect_http_fallback",
11884
- sessionId: this.client.currentSessionId
11885
- })
11886
- }
11887
- );
11888
- } catch {
11889
- }
11890
- }
11891
- this.client.disconnect();
11892
- }
11893
- /**
11894
- * Close only the socket transport without sending `client.goodbye`.
11895
- *
11896
- * Use this when the caller intends to immediately reattach to the same
11897
- * session after an unexpected disconnect.
11898
- */
11899
- disconnectTransport() {
11900
- this.client.disconnect();
11901
- }
11902
- // ==================== GRAPH CONTAINER READINESS ====================
11903
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
11904
- graphContainerStatus = null;
11905
- /**
11906
- * Check if the graph container is ready and warm.
11907
- *
11908
- * Sends a lightweight heartbeat RPC to the Session DO which internally
11909
- * pings the FalkorDB container. The response includes `graphContainerStatus`,
11910
- * which is stored locally and emitted as a `readiness` event.
11911
- *
11912
- * Use this method to proactively warm the graph container before any
11913
- * GraphQL query that requires it, or to poll the container's state in
11914
- * the background.
11915
- *
11916
- * @returns The current graph container status object
11917
- *
11918
- * @example
11919
- * ```typescript
11920
- * const status = await env.checkReadiness();
11921
- * console.log(status.status); // 'hot' | 'warming' | 'unknown'
11922
- *
11923
- * // Or listen for live updates
11924
- * env.on('readiness', (status) => {
11925
- * console.log('Graph is now:', status.status);
11926
- * });
11927
- * ```
11928
- */
11929
- async checkReadiness() {
11930
- const result = await this.client.call("client.heartbeat", {});
11931
- const containerStatus = result?.graphContainerStatus ?? {
11932
- lastKeepAliveAt: Date.now(),
11933
- status: "unknown"
11934
- };
11935
- this.graphContainerStatus = containerStatus;
11936
- this.emit("readiness", containerStatus);
11937
- return containerStatus;
11938
- }
11939
11958
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
11940
11959
  /**
11941
11960
  * Convert a class name + real-world ID into a unique graph path.
@@ -12796,36 +12815,186 @@ var Environment = class extends Session {
12796
12815
  }
12797
12816
  );
12798
12817
  }
12799
- // ==================== PUBLISH TOOLS ====================
12818
+ };
12819
+ var EnvironmentSession = class extends Session {
12820
+ environment;
12821
+ /** The last known graph container status, updated by checkReadiness() or on heartbeat */
12822
+ graphContainerStatus = null;
12823
+ constructor(client, environment, clientId) {
12824
+ super(client, clientId);
12825
+ this.environment = environment;
12826
+ }
12827
+ get environmentId() {
12828
+ return this.environment.environmentId;
12829
+ }
12830
+ get sandboxId() {
12831
+ return this.environment.sandboxId;
12832
+ }
12833
+ get ontologyId() {
12834
+ return this.environment.ontologyId;
12835
+ }
12836
+ get subjectId() {
12837
+ return this.environment.subjectId;
12838
+ }
12839
+ get envName() {
12840
+ return this.environment.envName;
12841
+ }
12842
+ get versionId() {
12843
+ return this.environment.versionId;
12844
+ }
12845
+ get granularId() {
12846
+ return this.environment.granularId;
12847
+ }
12848
+ get permissionProfileId() {
12849
+ return this.environment.permissionProfileId;
12850
+ }
12851
+ get apiEndpoint() {
12852
+ return this.environment.apiEndpoint;
12853
+ }
12854
+ get data() {
12855
+ return this.environment.data;
12856
+ }
12857
+ get feedback() {
12858
+ return this.environment.feedback;
12859
+ }
12800
12860
  /**
12801
- * Removed: environment-scoped effect publication is no longer supported.
12861
+ * Return a plain JS snapshot of the synced session heap.
12802
12862
  */
12803
- async publishTools(tools, revision = "1.0.0") {
12804
- return super.publishTools(tools, revision);
12863
+ getHeap() {
12864
+ const doc = this.document;
12865
+ return normalizeHeapSnapshot(doc?.heap);
12866
+ }
12867
+ async graphql(query, variables) {
12868
+ return this.environment.graphql(query, variables);
12869
+ }
12870
+ async defineRelationship(options) {
12871
+ return this.environment.defineRelationship(options);
12872
+ }
12873
+ async getRelationships(modelPath) {
12874
+ return this.environment.getRelationships(modelPath);
12875
+ }
12876
+ async attach(modelPath, submodelPath, targetPath) {
12877
+ return this.environment.attach(modelPath, submodelPath, targetPath);
12878
+ }
12879
+ async detach(modelPath, submodelPath, targetPath) {
12880
+ return this.environment.detach(modelPath, submodelPath, targetPath);
12881
+ }
12882
+ async listRelated(modelPath, submodelPath) {
12883
+ return this.environment.listRelated(modelPath, submodelPath);
12884
+ }
12885
+ async applyManifest(manifest) {
12886
+ return this.environment.applyManifest(manifest);
12887
+ }
12888
+ async recordObject(options) {
12889
+ return this.environment.recordObject(options);
12890
+ }
12891
+ async recordObjects(records, options) {
12892
+ return this.environment.recordObjects(records, options);
12893
+ }
12894
+ async enqueueRecordImport(records, options = {}) {
12895
+ return this.environment.enqueueRecordImport(records, options);
12896
+ }
12897
+ async listRecordImports(status) {
12898
+ return this.environment.listRecordImports(status);
12899
+ }
12900
+ async getRecordImportSummary() {
12901
+ return this.environment.getRecordImportSummary();
12902
+ }
12903
+ async getAwaitingRecordCount() {
12904
+ return this.environment.getAwaitingRecordCount();
12905
+ }
12906
+ async getRecordImport(importId) {
12907
+ return this.environment.getRecordImport(importId);
12908
+ }
12909
+ async cancelRecordImport(importId) {
12910
+ return this.environment.cancelRecordImport(importId);
12911
+ }
12912
+ async listFeedback() {
12913
+ return this.environment.listFeedback();
12805
12914
  }
12806
12915
  /**
12807
- * Removed: environment-scoped effect publication is no longer supported.
12916
+ * Close the session and disconnect from the sandbox.
12917
+ *
12918
+ * Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
12919
+ * to the runtime goodbye endpoint if no definitive WS-side runtime notify
12920
+ * acknowledgement was observed.
12808
12921
  */
12809
- async publishEffect(effect) {
12810
- return super.publishEffect(effect);
12922
+ async disconnect() {
12923
+ let wsNotifiedRuntime = false;
12924
+ try {
12925
+ const goodbye = await this.rpc(
12926
+ "client.goodbye",
12927
+ {
12928
+ timestamp: Date.now()
12929
+ }
12930
+ );
12931
+ wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
12932
+ } catch {
12933
+ wsNotifiedRuntime = false;
12934
+ }
12935
+ if (!wsNotifiedRuntime) {
12936
+ try {
12937
+ await fetch(
12938
+ `${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
12939
+ {
12940
+ method: "POST",
12941
+ headers: {
12942
+ "Content-Type": "application/json",
12943
+ Authorization: `Bearer ${this.environment.authToken}`,
12944
+ Connection: "close"
12945
+ },
12946
+ body: JSON.stringify({
12947
+ reason: "sdk_disconnect_http_fallback",
12948
+ sessionId: this.client.currentSessionId
12949
+ })
12950
+ }
12951
+ );
12952
+ } catch {
12953
+ }
12954
+ }
12955
+ this.client.disconnect();
12811
12956
  }
12812
12957
  /**
12813
- * Removed: environment-scoped effect publication is no longer supported.
12958
+ * Close only the socket transport without sending `client.goodbye`.
12814
12959
  */
12815
- async publishEffects(effects) {
12816
- return super.publishEffects(effects);
12960
+ disconnectTransport() {
12961
+ this.client.disconnect();
12817
12962
  }
12818
12963
  /**
12819
- * Removed: environment-scoped effect publication is no longer supported.
12964
+ * Backwards-compatible alias for `disconnect()`.
12820
12965
  */
12821
- async unpublishEffect(name) {
12822
- return super.unpublishEffect(name);
12966
+ async close() {
12967
+ await this.disconnect();
12823
12968
  }
12824
12969
  /**
12825
- * Removed: environment-scoped effect publication is no longer supported.
12970
+ * Check if the graph container is ready and warm.
12826
12971
  */
12827
- async unpublishAllEffects() {
12828
- return super.unpublishAllEffects();
12972
+ async checkReadiness() {
12973
+ const result = await this.client.call("client.heartbeat", {});
12974
+ const containerStatus = result?.graphContainerStatus ?? {
12975
+ lastKeepAliveAt: Date.now(),
12976
+ status: "unknown"
12977
+ };
12978
+ this.graphContainerStatus = containerStatus;
12979
+ this.emit("readiness", containerStatus);
12980
+ return containerStatus;
12981
+ }
12982
+ };
12983
+ var OntologyHandle = class {
12984
+ granular;
12985
+ ontologyNameOrId;
12986
+ constructor(granular, ontologyNameOrId) {
12987
+ this.granular = granular;
12988
+ this.ontologyNameOrId = ontologyNameOrId;
12989
+ }
12990
+ get effects() {
12991
+ return {
12992
+ register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
12993
+ registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
12994
+ unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
12995
+ clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
12996
+ disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
12997
+ };
12829
12998
  }
12830
12999
  };
12831
13000
  var Granular = class _Granular {
@@ -12862,6 +13031,12 @@ var Granular = class _Granular {
12862
13031
  this.onReconnectError = options.onReconnectError;
12863
13032
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
12864
13033
  }
13034
+ /**
13035
+ * Return an ontology-scoped handle for effects and other ontology-level APIs.
13036
+ */
13037
+ ontology(ontologyNameOrId) {
13038
+ return new OntologyHandle(this, ontologyNameOrId);
13039
+ }
12865
13040
  /**
12866
13041
  * Records/upserts a user and prepares them for sandbox connections
12867
13042
  *
@@ -12898,7 +13073,23 @@ var Granular = class _Granular {
12898
13073
  permissions: options.permissions || []
12899
13074
  });
12900
13075
  }
13076
+ /**
13077
+ * Alias for `recordUser()` with user-facing naming that matches upsert semantics.
13078
+ */
13079
+ async upsertUser(options) {
13080
+ return this.recordUser(options);
13081
+ }
12901
13082
  async resolveConnectUser(options) {
13083
+ const providedIdentityCount = [
13084
+ Boolean(options.user),
13085
+ Boolean(options.userId),
13086
+ Boolean(options.granularId)
13087
+ ].filter(Boolean).length;
13088
+ if (providedIdentityCount !== 1) {
13089
+ throw new Error(
13090
+ "openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
13091
+ );
13092
+ }
12902
13093
  if (options.user) {
12903
13094
  const user = normalizeUser(options.user);
12904
13095
  return {
@@ -12935,56 +13126,85 @@ var Granular = class _Granular {
12935
13126
  };
12936
13127
  }
12937
13128
  throw new Error(
12938
- "connect() requires either userId, granularId, or a user object returned by recordUser()."
13129
+ "openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
12939
13130
  );
12940
13131
  }
12941
13132
  /**
12942
- * Connect to an ontology environment and establish a real-time session.
12943
- *
12944
- * Effects are registered at the sandbox level via `granular.registerEffect()`
12945
- * or `granular.registerEffects()`. Sessions pick up live availability from
12946
- * the sandbox registry automatically.
12947
- *
12948
- * @param options - Connection options
12949
- * @returns An active environment session
13133
+ * Open or resolve an ontology environment for one user without opening a session.
12950
13134
  *
12951
13135
  * @example
12952
13136
  * ```typescript
12953
- * const environment = await granular.connect({
13137
+ * const environment = await granular.openEnvironment({
12954
13138
  * ontology: 'my-ontology',
12955
- * environment: 'dev',
13139
+ * tag: 'dev',
12956
13140
  * userId: 'user_123',
12957
13141
  * permissions: ['agent'],
12958
13142
  * });
12959
13143
  *
12960
- * await granular.registerEffect('my-sandbox', {
12961
- * name: 'greet',
12962
- * description: 'Say hello',
12963
- * inputSchema: { type: 'object', properties: {} },
12964
- * handler: async () => 'Hello!',
13144
+ * await environment.data.record({
13145
+ * className: 'customer',
13146
+ * id: 'acme',
13147
+ * fields: { name: 'Acme' },
12965
13148
  * });
12966
13149
  *
12967
- * // Submit job
12968
- * const job = await environment.submitJob(`
12969
- * import { tools } from './sandbox-tools';
12970
- * return await tools.greet({});
12971
- * `);
12972
- *
12973
- * console.log(await job.result); // 'Hello!'
13150
+ * const session = await environment.sessions.create();
13151
+ * const job = await session.submitJob(`return "hello";`);
13152
+ * console.log(await job.result);
12974
13153
  * ```
12975
13154
  */
13155
+ async openEnvironment(options) {
13156
+ const envData = await this.resolveOpenEnvironmentData(
13157
+ options,
13158
+ "openEnvironment"
13159
+ );
13160
+ return this.bindEnvironmentHandle(envData);
13161
+ }
13162
+ /**
13163
+ * Deprecated compatibility alias for `openEnvironment()`.
13164
+ *
13165
+ * `connect()` no longer opens a runtime session automatically.
13166
+ */
12976
13167
  async connect(options) {
12977
- const clientId = options.clientId || `client_${Date.now()}`;
13168
+ return this.openEnvironment({
13169
+ ...options,
13170
+ tag: this.resolveRequestedTag(options, "connect"),
13171
+ permissions: options.permissions || options.user?.permissions || []
13172
+ });
13173
+ }
13174
+ resolveRequestedTag(options, methodName) {
13175
+ const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
13176
+ if (!tag) {
13177
+ throw new Error(`${methodName}() requires \`tag\`.`);
13178
+ }
13179
+ return tag;
13180
+ }
13181
+ buildManagedEnvironmentName(tag, versionId) {
13182
+ return `__sdk__${tag}__${versionId}`;
13183
+ }
13184
+ matchesTagTrackedEnvironment(environment, tagName, tagId) {
13185
+ const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
13186
+ 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);
13187
+ }
13188
+ sortEnvironmentsByRecency(environments) {
13189
+ return [...environments].sort(
13190
+ (left, right) => right.updatedAt - left.updatedAt
13191
+ );
13192
+ }
13193
+ async resolveOpenEnvironmentData(options, methodName) {
12978
13194
  const ontology = options.ontology;
12979
13195
  if (!ontology) {
12980
- throw new Error("connect() requires `ontology`.");
13196
+ throw new Error(`${methodName}() requires \`ontology\`.`);
12981
13197
  }
12982
- const environmentName = options.environment;
12983
- if (!environmentName) {
12984
- throw new Error("connect() requires `environment`.");
13198
+ const tagName = options.tag?.trim();
13199
+ if (!tagName) {
13200
+ throw new Error(`${methodName}() requires \`tag\`.`);
12985
13201
  }
12986
- const tagName = options.tagName?.trim() || void 0;
12987
13202
  const user = await this.resolveConnectUser(options);
13203
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
13204
+ throw new Error(
13205
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
13206
+ );
13207
+ }
12988
13208
  const sandbox = await this.findOrCreateSandbox(ontology);
12989
13209
  for (const profileName of user.permissions) {
12990
13210
  const profileId = await this.ensurePermissionProfile(
@@ -12997,22 +13217,49 @@ var Granular = class _Granular {
12997
13217
  profileId
12998
13218
  );
12999
13219
  }
13000
- const envData = await this.environments.create(sandbox.sandboxId, {
13220
+ const tags = await this.request(
13221
+ `/control/sandboxes/${sandbox.sandboxId}/tags`
13222
+ );
13223
+ const tag = (tags.items || []).find(
13224
+ (candidate) => Boolean(candidate?.name === tagName)
13225
+ );
13226
+ if (!tag) {
13227
+ throw new Error(
13228
+ `Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
13229
+ );
13230
+ }
13231
+ const targetVersionId = tag.targetVersionId || tag.targetBuildId;
13232
+ if (!targetVersionId) {
13233
+ throw new Error(
13234
+ `Tag "${tagName}" does not currently point to a build/version.`
13235
+ );
13236
+ }
13237
+ const allEnvironments = await this.environments.list(sandbox.sandboxId);
13238
+ const userEnvironments = allEnvironments.filter(
13239
+ (environment) => environment.subjectId === user.granularId
13240
+ );
13241
+ const currentMatches = this.sortEnvironmentsByRecency(
13242
+ userEnvironments.filter(
13243
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
13244
+ )
13245
+ );
13246
+ if (currentMatches.length > 0) {
13247
+ return currentMatches[0];
13248
+ }
13249
+ const outdatedMatches = this.sortEnvironmentsByRecency(
13250
+ userEnvironments.filter(
13251
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
13252
+ )
13253
+ );
13254
+ if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13255
+ return outdatedMatches[0];
13256
+ }
13257
+ return this.environments.create(sandbox.sandboxId, {
13001
13258
  subjectId: user.granularId,
13002
- environment: environmentName,
13003
- tagName,
13259
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13260
+ tagId: tag.tagId,
13004
13261
  permissionProfileId: null
13005
13262
  });
13006
- await this.activateEnvironment(envData.environmentId);
13007
- const session = await this.request("/ws/sessions", {
13008
- method: "POST",
13009
- body: JSON.stringify({
13010
- environmentId: envData.environmentId,
13011
- clientId,
13012
- initialHeap: options.initialHeap
13013
- })
13014
- });
13015
- return this.bindWebSocketEnvironment(envData, clientId, session);
13016
13263
  }
13017
13264
  /**
13018
13265
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -13075,6 +13322,7 @@ var Granular = class _Granular {
13075
13322
  const clientId = options.clientId || `client_${Date.now()}`;
13076
13323
  await this.activateEnvironment(options.environmentId);
13077
13324
  const envData = await this.environments.get(options.environmentId);
13325
+ const environment = this.bindEnvironmentHandle(envData);
13078
13326
  const session = await this.request("/ws/sessions", {
13079
13327
  method: "POST",
13080
13328
  body: JSON.stringify({
@@ -13083,7 +13331,7 @@ var Granular = class _Granular {
13083
13331
  initialHeap: options.initialHeap
13084
13332
  })
13085
13333
  });
13086
- return this.bindWebSocketEnvironment(envData, clientId, session);
13334
+ return this.bindWebSocketEnvironmentSession(environment, clientId, session);
13087
13335
  }
13088
13336
  /**
13089
13337
  * Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
@@ -13095,7 +13343,8 @@ var Granular = class _Granular {
13095
13343
  body: JSON.stringify({})
13096
13344
  });
13097
13345
  const envData = await this.environments.get(minted.environmentId);
13098
- return this.bindWebSocketEnvironment(envData, clientId, minted);
13346
+ const environment = this.bindEnvironmentHandle(envData);
13347
+ return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
13099
13348
  }
13100
13349
  /**
13101
13350
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
@@ -13127,7 +13376,11 @@ var Granular = class _Granular {
13127
13376
  });
13128
13377
  return this.connectSession({ sessionId, clientId: options?.clientId });
13129
13378
  }
13130
- async bindWebSocketEnvironment(envData, clientId, session) {
13379
+ bindEnvironmentHandle(envData) {
13380
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13381
+ return new Environment(this, envData, this.apiKey, graphqlEndpoint);
13382
+ }
13383
+ async bindWebSocketEnvironmentSession(environment, clientId, session) {
13131
13384
  const client = new WSClient({
13132
13385
  url: session.wsUrl,
13133
13386
  sessionId: session.sessionId,
@@ -13138,16 +13391,13 @@ var Granular = class _Granular {
13138
13391
  onReconnectError: this.onReconnectError
13139
13392
  });
13140
13393
  await client.connect();
13141
- const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
13142
- const environment = new Environment(
13394
+ const environmentSession = new EnvironmentSession(
13143
13395
  client,
13144
- envData,
13145
- clientId,
13146
- this.apiKey,
13147
- graphqlEndpoint
13396
+ environment,
13397
+ clientId
13148
13398
  );
13149
- await environment.hello();
13150
- return environment;
13399
+ await environmentSession.hello();
13400
+ return environmentSession;
13151
13401
  }
13152
13402
  async activateEnvironment(environmentId) {
13153
13403
  await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
@@ -15539,7 +15789,9 @@ function buildSessionTranscript(input) {
15539
15789
  }
15540
15790
 
15541
15791
  exports.Environment = Environment;
15792
+ exports.EnvironmentSession = EnvironmentSession;
15542
15793
  exports.Granular = Granular;
15794
+ exports.OntologyHandle = OntologyHandle;
15543
15795
  exports.Session = Session;
15544
15796
  exports.WSClient = WSClient;
15545
15797
  exports.buildContinuationInstruction = buildContinuationInstruction;