@letta-ai/letta-agent-sdk 0.3.1 → 0.3.3

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.
Files changed (64) hide show
  1. package/AGENTS.md +47 -0
  2. package/README.md +17 -0
  3. package/dist/app-server-management.d.ts +58 -0
  4. package/dist/app-server-management.d.ts.map +1 -1
  5. package/dist/app-server-session.d.ts +5 -0
  6. package/dist/app-server-session.d.ts.map +1 -1
  7. package/dist/client-base.d.ts +7 -0
  8. package/dist/client-base.d.ts.map +1 -1
  9. package/dist/client-entry.js +1123 -776
  10. package/dist/client-entry.js.map +13 -9
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/cloud-sandbox.d.ts +33 -0
  13. package/dist/cloud-sandbox.d.ts.map +1 -0
  14. package/dist/cloud-session.d.ts.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1176 -791
  18. package/dist/index.js.map +17 -13
  19. package/dist/local-app-server-session.d.ts +1 -1
  20. package/dist/local-app-server-session.d.ts.map +1 -1
  21. package/dist/management.d.ts +7 -0
  22. package/dist/management.d.ts.map +1 -1
  23. package/dist/remote-client-session-core.d.ts +24 -93
  24. package/dist/remote-client-session-core.d.ts.map +1 -1
  25. package/dist/remote-session-protocol.d.ts +130 -0
  26. package/dist/remote-session-protocol.d.ts.map +1 -0
  27. package/dist/remote-turn-coordinator.d.ts +49 -0
  28. package/dist/remote-turn-coordinator.d.ts.map +1 -0
  29. package/dist/request-ids.d.ts +24 -0
  30. package/dist/request-ids.d.ts.map +1 -0
  31. package/dist/session.d.ts +10 -1
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/types.d.ts +6 -20
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +5 -2
  36. package/src/app-server-management.ts +641 -0
  37. package/src/app-server-session.ts +948 -0
  38. package/src/cli-resolver.ts +46 -0
  39. package/src/client-base.ts +482 -0
  40. package/src/client-entry.ts +31 -0
  41. package/src/client.ts +138 -0
  42. package/src/cloud-management.ts +360 -0
  43. package/src/cloud-sandbox.ts +117 -0
  44. package/src/cloud-session.ts +1313 -0
  45. package/src/index.ts +440 -0
  46. package/src/interactiveToolPolicy.ts +62 -0
  47. package/src/local-app-server-session.ts +39 -0
  48. package/src/local-app-server.ts +137 -0
  49. package/src/management-types.ts +133 -0
  50. package/src/management.ts +206 -0
  51. package/src/protocol.ts +249 -0
  52. package/src/remote-client-session-core.ts +786 -0
  53. package/src/remote-session-protocol.ts +660 -0
  54. package/src/remote-turn-coordinator.ts +505 -0
  55. package/src/remote.ts +177 -0
  56. package/src/repositories.ts +340 -0
  57. package/src/request-ids.ts +33 -0
  58. package/src/session.ts +1638 -0
  59. package/src/stream-events.ts +88 -0
  60. package/src/tool-helpers.ts +147 -0
  61. package/src/transport.ts +484 -0
  62. package/src/types.ts +1328 -0
  63. package/src/validation.ts +223 -0
  64. package/src/websocket.ts +22 -0
@@ -1,3 +1,5 @@
1
+ // Generated bundle. Canonical modules are published under src/ and maintained in the letta-agent-sdk repository.
2
+
1
3
  // src/repositories.ts
2
4
  var DEFAULT_CLOUD_API_BASE_URL = "https://api.letta.com";
3
5
  function getDefaultApiKey() {
@@ -682,7 +684,56 @@ function createAppServerClient(options) {
682
684
  return new AppServerClient(options);
683
685
  }
684
686
 
687
+ // src/request-ids.ts
688
+ var processRequestCounter = 0;
689
+ function createRequestIdGenerator() {
690
+ const nonce = Math.random().toString(36).slice(2, 10);
691
+ return (prefix = "req") => `${prefix}-${nonce}-${++processRequestCounter}`;
692
+ }
693
+ function applyUniqueRequestIds(client) {
694
+ client.nextRequestId = createRequestIdGenerator();
695
+ return client;
696
+ }
697
+
685
698
  // src/app-server-management.ts
699
+ var DEFAULT_IDLE_LINGER_MS = 250;
700
+ var registeredTransportsByUrl = new Map;
701
+ function appServerUrlKey(value) {
702
+ try {
703
+ const url = new URL(value);
704
+ if (url.protocol === "http:")
705
+ url.protocol = "ws:";
706
+ if (url.protocol === "https:")
707
+ url.protocol = "wss:";
708
+ url.hash = "";
709
+ url.searchParams.delete("channel");
710
+ return url.toString();
711
+ } catch {
712
+ return value;
713
+ }
714
+ }
715
+ function registerManagementTransport(url, transport) {
716
+ const key = appServerUrlKey(url);
717
+ const registration = { transport };
718
+ const transports = registeredTransportsByUrl.get(key) ?? new Set;
719
+ transports.add(registration);
720
+ registeredTransportsByUrl.set(key, transports);
721
+ return () => {
722
+ transports.delete(registration);
723
+ if (transports.size === 0) {
724
+ registeredTransportsByUrl.delete(key);
725
+ }
726
+ };
727
+ }
728
+ async function releaseAppServerManagementConnections(url) {
729
+ const key = appServerUrlKey(url);
730
+ while (true) {
731
+ const transports = [...registeredTransportsByUrl.get(key) ?? []];
732
+ if (transports.length === 0)
733
+ return;
734
+ await Promise.all(transports.map(({ transport }) => transport.releaseIdleConnection()));
735
+ }
736
+ }
686
737
  function ensureResponse(response, value, fallback) {
687
738
  if (!response.success || value == null) {
688
739
  throw new Error(response.error ?? fallback);
@@ -718,8 +769,16 @@ function stringRecord(value) {
718
769
 
719
770
  class AppServerManagementTransport {
720
771
  options;
772
+ connectionPromise = null;
773
+ releasePromise = null;
774
+ closingConnections = new Set;
775
+ inFlightRequests = 0;
776
+ idleWaiters = new Set;
777
+ idleTimer = null;
778
+ idleLingerMs;
721
779
  constructor(options) {
722
780
  this.options = options;
781
+ this.idleLingerMs = options.idleLingerMs ?? DEFAULT_IDLE_LINGER_MS;
723
782
  }
724
783
  async listAgents(query) {
725
784
  const response = await this.request("agent_list", { query }, "agent_list_response");
@@ -786,38 +845,206 @@ class AppServerManagementTransport {
786
845
  }
787
846
  return { messages: response.messages };
788
847
  }
848
+ releaseIdleConnection() {
849
+ this.clearIdleTimer();
850
+ if (this.releasePromise)
851
+ return this.releasePromise;
852
+ const release = this.releaseConnectionWhenIdle();
853
+ this.releasePromise = release;
854
+ release.then(() => {
855
+ if (this.releasePromise === release) {
856
+ this.releasePromise = null;
857
+ }
858
+ }, () => {
859
+ if (this.releasePromise === release) {
860
+ this.releasePromise = null;
861
+ }
862
+ });
863
+ return release;
864
+ }
789
865
  async request(type, body, responseType) {
790
- const ownedConnection = this.options.url ? null : await this.options.connect?.();
866
+ if (this.releasePromise) {
867
+ await this.releasePromise;
868
+ }
869
+ if (this.closingConnections.size > 0) {
870
+ await Promise.all([...this.closingConnections]);
871
+ }
872
+ this.clearIdleTimer();
873
+ this.inFlightRequests += 1;
874
+ try {
875
+ const { client } = await this.acquireConnection();
876
+ const command = {
877
+ type,
878
+ request_id: client.nextRequestId(type),
879
+ ...body
880
+ };
881
+ const response = await client.request(command, {
882
+ predicate: (message) => message.type === responseType
883
+ });
884
+ return response;
885
+ } finally {
886
+ this.inFlightRequests -= 1;
887
+ if (this.inFlightRequests === 0) {
888
+ const waiters = [...this.idleWaiters];
889
+ this.idleWaiters.clear();
890
+ for (const resolve of waiters)
891
+ resolve();
892
+ if (!this.releasePromise) {
893
+ this.scheduleIdleRelease();
894
+ }
895
+ }
896
+ }
897
+ }
898
+ async releaseConnectionWhenIdle() {
899
+ if (this.inFlightRequests > 0) {
900
+ await new Promise((resolve) => {
901
+ this.idleWaiters.add(resolve);
902
+ });
903
+ }
904
+ this.clearIdleTimer();
905
+ const promise = this.connectionPromise;
906
+ try {
907
+ if (promise) {
908
+ this.connectionPromise = null;
909
+ await this.trackClosingConnection(await promise);
910
+ }
911
+ if (this.closingConnections.size > 0) {
912
+ await Promise.all([...this.closingConnections]);
913
+ }
914
+ } catch {}
915
+ }
916
+ acquireConnection() {
917
+ if (this.connectionPromise)
918
+ return this.connectionPromise;
919
+ const promise = this.openConnection().then((connection) => {
920
+ connection.detachDisconnect = connection.client.onDisconnect(() => {
921
+ this.discardConnection(promise, connection);
922
+ });
923
+ return connection;
924
+ }, (error) => {
925
+ if (this.connectionPromise === promise) {
926
+ this.connectionPromise = null;
927
+ }
928
+ throw error;
929
+ });
930
+ this.connectionPromise = promise;
931
+ return promise;
932
+ }
933
+ async openConnection() {
934
+ const ownedConnection = this.options.url ? null : await this.options.connect?.() ?? null;
791
935
  const url = this.options.url ?? ownedConnection?.url;
792
936
  if (!url) {
793
937
  throw new Error("App-server management requires a url or connect hook.");
794
938
  }
939
+ const unregister = registerManagementTransport(url, this);
795
940
  let client = null;
796
941
  try {
797
- client = createAppServerClient({
942
+ client = applyUniqueRequestIds(createAppServerClient({
798
943
  url,
799
944
  ...this.options.authToken !== undefined ? { authToken: this.options.authToken } : {},
800
945
  ...this.options.WebSocket ? {
801
946
  WebSocket: this.options.WebSocket
802
947
  } : {},
803
948
  ...this.options.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.options.requestTimeoutMs } : {}
804
- });
949
+ }));
805
950
  await client.connect();
806
- const command = {
807
- type,
808
- request_id: client.nextRequestId(type),
809
- ...body
810
- };
811
- const response = await client.request(command, {
812
- predicate: (message) => message.type === responseType
813
- });
814
- return response;
815
- } finally {
816
- client?.close();
817
- ownedConnection?.close();
951
+ } catch (error) {
952
+ try {
953
+ if (client) {
954
+ const controlClosed = waitForSocketClose(client.control);
955
+ client.close();
956
+ ownedConnection?.close();
957
+ await controlClosed;
958
+ } else {
959
+ ownedConnection?.close();
960
+ }
961
+ } catch {} finally {
962
+ unregister();
963
+ }
964
+ throw error;
818
965
  }
966
+ return {
967
+ client,
968
+ ownedConnection,
969
+ detachDisconnect: () => {},
970
+ unregister
971
+ };
972
+ }
973
+ discardConnection(promise, connection) {
974
+ if (this.connectionPromise === promise) {
975
+ this.connectionPromise = null;
976
+ this.clearIdleTimer();
977
+ }
978
+ this.trackClosingConnection(connection);
979
+ }
980
+ trackClosingConnection(connection) {
981
+ const closing = closeConnection(connection);
982
+ this.closingConnections.add(closing);
983
+ closing.then(() => this.closingConnections.delete(closing), () => this.closingConnections.delete(closing));
984
+ return closing;
985
+ }
986
+ scheduleIdleRelease() {
987
+ if (!this.connectionPromise)
988
+ return;
989
+ this.clearIdleTimer();
990
+ const timer = setTimeout(() => {
991
+ this.idleTimer = null;
992
+ this.releaseIdleConnection();
993
+ }, this.idleLingerMs);
994
+ this.idleTimer = timer;
995
+ timer.unref?.();
996
+ }
997
+ clearIdleTimer() {
998
+ if (this.idleTimer === null)
999
+ return;
1000
+ clearTimeout(this.idleTimer);
1001
+ this.idleTimer = null;
1002
+ }
1003
+ }
1004
+ async function closeConnection(connection) {
1005
+ connection.detachDisconnect();
1006
+ const controlClosed = waitForSocketClose(connection.client.control);
1007
+ try {
1008
+ connection.client.close();
1009
+ connection.ownedConnection?.close();
1010
+ await controlClosed;
1011
+ } finally {
1012
+ connection.unregister();
819
1013
  }
820
1014
  }
1015
+ function waitForSocketClose(socket) {
1016
+ if (socket.readyState === 3)
1017
+ return Promise.resolve();
1018
+ return new Promise((resolve) => {
1019
+ let settled = false;
1020
+ let detach = () => {};
1021
+ const finish = () => {
1022
+ if (settled)
1023
+ return;
1024
+ settled = true;
1025
+ clearTimeout(timeout);
1026
+ detach();
1027
+ resolve();
1028
+ };
1029
+ if (socket.addEventListener) {
1030
+ socket.addEventListener("close", finish);
1031
+ detach = () => socket.removeEventListener?.("close", finish);
1032
+ } else if (socket.once) {
1033
+ socket.once("close", finish);
1034
+ detach = () => socket.off?.("close", finish);
1035
+ } else if (socket.on) {
1036
+ socket.on("close", finish);
1037
+ detach = () => socket.off?.("close", finish);
1038
+ } else {
1039
+ resolve();
1040
+ return;
1041
+ }
1042
+ const timeout = setTimeout(finish, 1000);
1043
+ timeout.unref?.();
1044
+ if (socket.readyState === 3)
1045
+ finish();
1046
+ });
1047
+ }
821
1048
 
822
1049
  // node_modules/@letta-ai/letta-code/dist/agent-presets.js
823
1050
  var LETTA_CODE_ORIGIN_TAG = "origin:letta-code";
@@ -5486,7 +5713,7 @@ function buildCanUseToolContext(request, requestId) {
5486
5713
  return context;
5487
5714
  }
5488
5715
 
5489
- // src/remote-client-session-core.ts
5716
+ // src/remote-session-protocol.ts
5490
5717
  var FAILURE_STOP_REASONS = new Set([
5491
5718
  "error",
5492
5719
  "llm_api_error",
@@ -5881,856 +6108,930 @@ function normalizeSendMessage(message) {
5881
6108
  return message;
5882
6109
  }
5883
6110
 
5884
- class RemoteClientSessionCore {
5885
- mode;
5886
- controller = null;
5887
- runtime = null;
5888
- initialized = false;
5889
- closed = false;
5890
- activeTurnStartedAt = 0;
5891
- _agentId = null;
5892
- _sessionId = null;
5893
- _conversationId = null;
5894
- _model = "";
5895
- _modelSettings = null;
6111
+ // src/remote-turn-coordinator.ts
6112
+ class RemoteTurnCoordinator {
5896
6113
  label;
5897
6114
  requestTimeoutMs;
6115
+ onDeviceStatus;
5898
6116
  streamQueue = [];
5899
6117
  streamResolvers = [];
5900
- removeMessageHandler = null;
5901
6118
  activeTurn = null;
5902
6119
  pendingTurns = [];
5903
6120
  nextTurnId = 0;
5904
6121
  messageCounter = 0;
5905
6122
  clientMessageCounter = 0;
5906
- toolNames;
5907
- deviceStatusListeners = new Set;
5908
- deviceStatusRefreshCancels = new Set;
5909
- constructor(mode, config) {
5910
- this.mode = mode;
6123
+ closed = false;
6124
+ _activeTurnStartedAt = 0;
6125
+ constructor(config) {
5911
6126
  this.label = config.label;
5912
6127
  this.requestTimeoutMs = config.requestTimeoutMs;
6128
+ this.onDeviceStatus = config.onDeviceStatus;
5913
6129
  }
5914
- async initialize() {
5915
- if (this.initialized) {
5916
- throw new Error("Session already initialized");
5917
- }
5918
- if (this.closed) {
5919
- throw new Error("Session is closed");
5920
- }
5921
- try {
5922
- const init = await this.initializeRuntimeController();
5923
- this.controller = init.controller;
5924
- this.runtime = init.runtime;
5925
- this._agentId = init.runtime.agent_id;
5926
- this._conversationId = init.runtime.conversation_id;
5927
- this._sessionId = `${init.runtime.agent_id}:${init.runtime.conversation_id}`;
5928
- this._modelSettings = init.modelSettings ?? null;
5929
- this._model = typeof init.model === "string" ? init.model : typeof this._modelSettings?.model === "string" ? this._modelSettings.model : "";
5930
- this.toolNames = init.tools;
5931
- this.removeMessageHandler = this.controller.onMessage(this.handleProtocolMessage);
5932
- this.initialized = true;
5933
- await this.afterRuntimeInitialized();
5934
- await this.applyPostInitializeOptions();
5935
- const initMessage = {
5936
- type: "init",
5937
- agentId: init.runtime.agent_id,
5938
- sessionId: this._sessionId,
5939
- conversationId: init.runtime.conversation_id,
5940
- model: this._model
5941
- };
5942
- if (this.toolNames !== undefined)
5943
- initMessage.tools = this.toolNames;
5944
- if (init.skillSources !== undefined) {
5945
- initMessage.skillSources = init.skillSources;
5946
- }
5947
- return initMessage;
5948
- } catch (error) {
5949
- this.close();
5950
- throw error;
5951
- }
6130
+ get activeTurnStartedAt() {
6131
+ return this._activeTurnStartedAt;
5952
6132
  }
5953
- async send(message) {
5954
- if (!this.initialized) {
5955
- await this.initialize();
5956
- }
5957
- if (!this.controller || !this.runtime) {
5958
- throw new Error("Session is not initialized");
5959
- }
5960
- await this.beforeTurn();
5961
- const turn = this.trackSentTurn(this.runtime);
5962
- try {
5963
- this.controller.sendTurnMessage(this.runtime, message, {
5964
- clientMessageId: turn.clientMessageId
5965
- });
5966
- } catch (error) {
5967
- this.removeTrackedTurn(turn);
5968
- throw error;
5969
- }
6133
+ hasInFlightTurn() {
6134
+ return this.activeTurn !== null || this.pendingTurns.length > 0;
5970
6135
  }
5971
- async runTurn(message, _options = {}) {
5972
- if (this.activeTurn || this.pendingTurns.length > 0) {
5973
- throw new Error(`A turn is already in flight for this ${this.label} session. Use send() and stream() to let the listener queue messages.`);
5974
- }
5975
- await this.send(message);
5976
- for await (const msg of this.stream()) {
5977
- if (msg.type === "result") {
5978
- return msg;
5979
- }
5980
- }
5981
- return {
5982
- type: "result",
5983
- success: false,
5984
- error: "stream_closed",
5985
- errorCode: "stream_closed",
5986
- recoverable: false,
5987
- errorDetail: "Stream ended before terminal result",
5988
- durationMs: Date.now() - this.activeTurnStartedAt,
5989
- conversationId: this._conversationId
6136
+ trackSentTurn(runtime) {
6137
+ const turn = {
6138
+ id: ++this.nextTurnId,
6139
+ runtime,
6140
+ clientMessageId: `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
6141
+ queuedAt: Date.now(),
6142
+ startedAt: 0,
6143
+ assistantText: "",
6144
+ runIds: new Set,
6145
+ observedTurnEvidence: false,
6146
+ observedRequiresApprovalStop: false,
6147
+ abortRequested: false,
6148
+ timeout: null
5990
6149
  };
5991
- }
5992
- async* stream() {
5993
- while (true) {
5994
- const msg = await this.nextMessage();
5995
- if (!msg)
5996
- break;
5997
- yield msg;
5998
- if (msg.type === "result")
5999
- break;
6150
+ if (this.activeTurn) {
6151
+ this.pendingTurns.push(turn);
6152
+ } else {
6153
+ this.activateTurn(turn);
6000
6154
  }
6155
+ return turn;
6001
6156
  }
6002
- async abort() {
6003
- if (!this.initialized)
6004
- return;
6005
- if (!this.controller || !this.runtime)
6157
+ removeTrackedTurn(turn) {
6158
+ if (turn.timeout)
6159
+ clearTimeout(turn.timeout);
6160
+ if (this.activeTurn === turn) {
6161
+ this.activeTurn = null;
6006
6162
  return;
6163
+ }
6164
+ const index = this.pendingTurns.indexOf(turn);
6165
+ if (index !== -1)
6166
+ this.pendingTurns.splice(index, 1);
6167
+ }
6168
+ markAbortRequested() {
6007
6169
  if (this.activeTurn)
6008
6170
  this.activeTurn.abortRequested = true;
6009
- await this.controller.abort(this.runtime);
6010
6171
  }
6011
- async sendCommand(command, options) {
6012
- if (!this.initialized) {
6013
- await this.initialize();
6014
- }
6015
- if (!this.controller) {
6016
- throw new Error("Session is not initialized");
6172
+ handleProtocolMessage(message, runtime) {
6173
+ if (!sameRuntime2(message, runtime))
6174
+ return;
6175
+ const statusRecord = deviceStatusRecord(message);
6176
+ if (statusRecord) {
6177
+ const status = toSessionDeviceStatus(statusRecord);
6178
+ if (status)
6179
+ this.onDeviceStatus(status);
6180
+ return;
6017
6181
  }
6018
- if (!command || typeof command !== "object" || Array.isArray(command)) {
6019
- throw new Error("Invalid command. Expected a protocol command object.");
6182
+ if (message.type === "update_queue") {
6183
+ const sdkMessage2 = {
6184
+ type: "queue_update",
6185
+ queue: queueItems(message)
6186
+ };
6187
+ this.enqueue(sdkMessage2);
6188
+ return;
6020
6189
  }
6021
- if (typeof command.type !== "string" || command.type.length === 0) {
6022
- throw new Error("Invalid command. Expected a non-empty type.");
6190
+ if (message.type === "update_loop_status") {
6191
+ this.handleLoopStatusMessage(message);
6192
+ return;
6023
6193
  }
6024
- if (!options || !options.responseType && !options.predicate && options.timeoutMs === undefined) {
6025
- this.controller.send(command);
6194
+ const delta = streamDeltaRecord(message);
6195
+ if (!delta)
6026
6196
  return;
6197
+ const active = this.activateNextTurnFromProtocol();
6198
+ if (active) {
6199
+ active.observedTurnEvidence = true;
6200
+ const runId = streamDeltaRunId2(delta);
6201
+ if (runId)
6202
+ active.runIds.add(runId);
6027
6203
  }
6028
- const { type, ...body } = command;
6029
- const response = await this.controller.request(type, body, {
6030
- ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {},
6031
- predicate: options.predicate ? (message) => options.predicate?.(message) === true : options.responseType ? (message) => message.type === options.responseType : undefined
6204
+ const sdkMessage = this.transformStreamDelta(delta);
6205
+ if (sdkMessage)
6206
+ this.enqueue(sdkMessage);
6207
+ this.handleTurnTerminalDelta(delta, sdkMessage);
6208
+ }
6209
+ nextMessage() {
6210
+ const next = this.streamQueue.shift();
6211
+ if (next)
6212
+ return Promise.resolve(next);
6213
+ if (this.closed)
6214
+ return Promise.resolve(null);
6215
+ return new Promise((resolve) => {
6216
+ this.streamResolvers.push(resolve);
6032
6217
  });
6033
- return response;
6034
6218
  }
6035
- async listModels() {
6036
- if (!this.initialized) {
6037
- await this.initialize();
6038
- }
6039
- if (!this.controller) {
6040
- throw new Error("Session is not initialized");
6219
+ close() {
6220
+ if (this.closed)
6221
+ return;
6222
+ this.closed = true;
6223
+ if (this.activeTurn?.timeout)
6224
+ clearTimeout(this.activeTurn.timeout);
6225
+ for (const turn of this.pendingTurns) {
6226
+ if (turn.timeout)
6227
+ clearTimeout(turn.timeout);
6041
6228
  }
6042
- return this.controller.listModels();
6229
+ this.activeTurn = null;
6230
+ this.pendingTurns.length = 0;
6231
+ this.resolveAll(null);
6043
6232
  }
6044
- async updateModel(update) {
6045
- if (!this.initialized) {
6046
- await this.initialize();
6233
+ activateTurn(turn) {
6234
+ this.activeTurn = turn;
6235
+ turn.startedAt = Date.now();
6236
+ this._activeTurnStartedAt = turn.startedAt;
6237
+ if (this.requestTimeoutMs !== undefined) {
6238
+ turn.timeout = setTimeout(() => {
6239
+ this.failTurn(turn, `Timed out waiting for ${this.label} turn`);
6240
+ }, this.requestTimeoutMs);
6241
+ turn.timeout.unref?.();
6047
6242
  }
6048
- if (!this.controller || !this.runtime) {
6049
- throw new Error("Session is not initialized");
6243
+ }
6244
+ activateNextTurnFromProtocol() {
6245
+ if (this.activeTurn)
6246
+ return this.activeTurn;
6247
+ const next = this.pendingTurns.shift();
6248
+ if (!next)
6249
+ return null;
6250
+ this.activateTurn(next);
6251
+ return next;
6252
+ }
6253
+ failTurn(turn, detail) {
6254
+ if (this.activeTurn !== turn)
6255
+ return;
6256
+ this.enqueue({
6257
+ type: "error",
6258
+ message: detail,
6259
+ errorCode: "error",
6260
+ stopReason: "error",
6261
+ errorDetail: detail,
6262
+ recoverable: false
6263
+ });
6264
+ this.completeActiveTurn({
6265
+ runtime: turn.runtime,
6266
+ stopReason: "error",
6267
+ runIds: [...turn.runIds],
6268
+ success: false,
6269
+ detail,
6270
+ errorCode: "error"
6271
+ });
6272
+ }
6273
+ completeActiveTurn(turn) {
6274
+ const active = this.activeTurn;
6275
+ if (!active)
6276
+ return;
6277
+ if (active.timeout) {
6278
+ clearTimeout(active.timeout);
6279
+ active.timeout = null;
6050
6280
  }
6051
- const normalized = normalizeUpdateModelInput(update);
6052
- const payload = await this.resolveUpdateModelPayload(normalized);
6053
- const result = await this.controller.updateModel(this.runtime, payload);
6054
- if (result.modelHandle !== undefined) {
6055
- this._model = result.modelHandle;
6056
- } else if (payload.model_handle !== undefined) {
6057
- this._model = payload.model_handle;
6058
- } else if (typeof result.modelSettings?.model === "string") {
6059
- this._model = result.modelSettings.model;
6281
+ this.enqueue(this.resultFromTurn(turn, active));
6282
+ this.activeTurn = null;
6283
+ }
6284
+ handleLoopStatusMessage(message) {
6285
+ const status = loopStatusValue(message);
6286
+ if (!status)
6287
+ return;
6288
+ const activeRunIds = loopStatusRunIds(message);
6289
+ const sdkMessage = {
6290
+ type: "loop_status",
6291
+ status,
6292
+ activeRunIds
6293
+ };
6294
+ this.enqueue(sdkMessage);
6295
+ const active = this.activeTurn;
6296
+ if (!active)
6297
+ return;
6298
+ for (const runId of activeRunIds)
6299
+ active.runIds.add(runId);
6300
+ const hadTurnEvidence = active.observedTurnEvidence || active.observedRequiresApprovalStop;
6301
+ if (!hadTurnEvidence)
6302
+ return;
6303
+ if (status === "WAITING_ON_APPROVAL") {
6304
+ this.completeActiveTurn({
6305
+ runtime: active.runtime,
6306
+ stopReason: "requires_approval",
6307
+ runIds: [...active.runIds]
6308
+ });
6309
+ return;
6060
6310
  }
6061
- if ("modelSettings" in result) {
6062
- this._modelSettings = result.modelSettings ?? null;
6311
+ if (status === "WAITING_ON_INPUT" && active.abortRequested) {
6312
+ this.completeActiveTurn({
6313
+ runtime: active.runtime,
6314
+ stopReason: "interrupted",
6315
+ runIds: [...active.runIds],
6316
+ success: false,
6317
+ detail: "Interrupted",
6318
+ errorCode: "interrupted"
6319
+ });
6320
+ return;
6321
+ }
6322
+ if (status === "WAITING_ON_INPUT" && active.observedTurnEvidence) {
6323
+ this.completeActiveTurn({
6324
+ runtime: active.runtime,
6325
+ stopReason: null,
6326
+ runIds: [...active.runIds]
6327
+ });
6063
6328
  }
6064
- return result;
6065
6329
  }
6066
- async recoverPendingApprovals(options = {}) {
6067
- if (!this.initialized) {
6068
- await this.initialize();
6330
+ handleTurnTerminalDelta(delta, sdkMessage) {
6331
+ const active = this.activeTurn;
6332
+ if (!active)
6333
+ return;
6334
+ const messageType = streamDeltaMessageType2(delta);
6335
+ if (messageType === "stop_reason") {
6336
+ const stopReason = streamDeltaStopReason2(delta) ?? null;
6337
+ if (stopReason === "requires_approval") {
6338
+ active.observedRequiresApprovalStop = true;
6339
+ return;
6340
+ }
6341
+ this.completeActiveTurn({
6342
+ runtime: active.runtime,
6343
+ stopReason,
6344
+ runIds: [...active.runIds]
6345
+ });
6346
+ return;
6069
6347
  }
6070
- if (!this.controller || !this.runtime) {
6071
- throw new Error("Session is not initialized");
6348
+ if (sdkMessage?.type === "error") {
6349
+ this.completeActiveTurn({
6350
+ runtime: active.runtime,
6351
+ stopReason: sdkMessage.stopReason,
6352
+ runIds: [...active.runIds],
6353
+ success: false,
6354
+ detail: sdkMessage.errorDetail ?? sdkMessage.message,
6355
+ errorCode: sdkMessage.errorCode
6356
+ });
6072
6357
  }
6073
- return this.controller.recoverPendingApprovals(this.runtime, options);
6074
6358
  }
6075
- async removeQueuedMessage(itemId) {
6076
- if (typeof itemId !== "string" || itemId.trim().length === 0) {
6077
- throw new Error("Invalid queue item id. Expected a non-empty string.");
6359
+ transformStreamDelta(delta) {
6360
+ const messageType = typeof delta.message_type === "string" ? delta.message_type : undefined;
6361
+ const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
6362
+ const uuid = typeof delta.id === "string" ? delta.id : `${this.label}-${++this.messageCounter}`;
6363
+ if (messageType === "assistant_message") {
6364
+ const content = extractTextFromContent(delta.content);
6365
+ if (!content)
6366
+ return null;
6367
+ if (this.activeTurn)
6368
+ this.activeTurn.assistantText += content;
6369
+ return { type: "assistant", content, uuid, runId };
6078
6370
  }
6079
- if (!this.initialized) {
6080
- await this.initialize();
6371
+ if (messageType === "reasoning_message") {
6372
+ const content = typeof delta.reasoning === "string" ? delta.reasoning : extractTextFromContent(delta.content);
6373
+ if (!content)
6374
+ return null;
6375
+ return { type: "reasoning", content, uuid, runId };
6081
6376
  }
6082
- if (!this.controller || !this.runtime) {
6083
- throw new Error("Session is not initialized");
6377
+ if (messageType === "tool_call_message" || messageType === "approval_request_message") {
6378
+ const toolCall = firstToolCall(delta);
6379
+ if (!toolCall)
6380
+ return null;
6381
+ const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
6382
+ const toolCallId = (typeof toolCall.tool_call_id === "string" ? toolCall.tool_call_id : undefined) ?? (typeof toolCall.id === "string" ? toolCall.id : undefined);
6383
+ if (!toolCallId) {
6384
+ const detail = `Missing tool_call_id in ${messageType} (uuid=${uuid})`;
6385
+ return {
6386
+ type: "error",
6387
+ message: detail,
6388
+ errorCode: "protocol_error",
6389
+ stopReason: "protocol_error",
6390
+ runId,
6391
+ recoverable: false,
6392
+ errorDetail: detail
6393
+ };
6394
+ }
6395
+ const toolName = (typeof toolCall.name === "string" ? toolCall.name : undefined) ?? (typeof fn?.name === "string" ? fn.name : undefined) ?? "?";
6396
+ const { input, raw } = toolInputFromArguments(toolCall.arguments ?? fn?.arguments);
6397
+ return {
6398
+ type: "tool_call",
6399
+ toolCallId,
6400
+ toolName,
6401
+ toolInput: input,
6402
+ rawArguments: raw,
6403
+ uuid,
6404
+ runId
6405
+ };
6084
6406
  }
6085
- const response = await this.controller.request("remove_queue_item", {
6086
- runtime: this.runtime,
6087
- item_id: itemId
6088
- }, {
6089
- predicate: (message) => message.type === "remove_queue_item_response" && message.item_id === itemId
6090
- });
6091
- if (typeof response.success !== "boolean") {
6092
- throw new Error("Invalid remove_queue_item_response from runtime");
6407
+ if (messageType === "tool_return_message") {
6408
+ const toolReturn = firstToolReturn(delta) ?? delta;
6409
+ const toolCallId = (typeof delta.tool_call_id === "string" ? delta.tool_call_id : undefined) ?? (typeof toolReturn.tool_call_id === "string" ? toolReturn.tool_call_id : undefined);
6410
+ if (!toolCallId)
6411
+ return null;
6412
+ const content = extractTextFromContent(delta.tool_return ?? toolReturn.tool_return ?? toolReturn.content) ?? "";
6413
+ const status = typeof delta.status === "string" ? delta.status : toolReturn.status;
6414
+ return {
6415
+ type: "tool_result",
6416
+ toolCallId,
6417
+ content,
6418
+ isError: status === "error",
6419
+ uuid,
6420
+ runId
6421
+ };
6093
6422
  }
6094
- return {
6095
- itemId: typeof response.item_id === "string" ? response.item_id : itemId,
6096
- removed: response.success
6097
- };
6098
- }
6099
- async getDeviceStatus(options = {}) {
6100
- if (!this.initialized) {
6101
- await this.initialize();
6423
+ if (messageType === "error_message" || messageType === "loop_error") {
6424
+ const detail = (typeof delta.detail === "string" ? delta.detail : undefined) ?? (typeof delta.message === "string" ? delta.message : undefined) ?? `${this.label} turn failed`;
6425
+ const stopReason = (typeof delta.stop_reason === "string" ? delta.stop_reason : undefined) ?? (typeof delta.error_type === "string" ? delta.error_type : undefined) ?? "error";
6426
+ const approvalConflict = isApprovalConflictSignal({
6427
+ detail,
6428
+ message: typeof delta.message === "string" ? delta.message : undefined,
6429
+ stopReason
6430
+ });
6431
+ return {
6432
+ type: "error",
6433
+ message: detail,
6434
+ errorCode: approvalConflict ? "approval_conflict" : toSdkErrorCode(stopReason),
6435
+ approvalConflict: approvalConflict || undefined,
6436
+ recoverable: approvalConflict ? true : false,
6437
+ errorDetail: detail,
6438
+ stopReason,
6439
+ runId
6440
+ };
6102
6441
  }
6103
- if (!this.controller || !this.runtime) {
6104
- throw new Error("Session is not initialized");
6442
+ if (messageType === "retry") {
6443
+ return {
6444
+ type: "retry",
6445
+ reason: typeof delta.reason === "string" ? delta.reason : "error",
6446
+ attempt: typeof delta.attempt === "number" ? delta.attempt : 0,
6447
+ maxAttempts: typeof delta.max_attempts === "number" ? delta.max_attempts : 0,
6448
+ delayMs: typeof delta.delay_ms === "number" ? delta.delay_ms : 0,
6449
+ runId
6450
+ };
6105
6451
  }
6106
- const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs ?? 30000;
6107
- if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
6108
- throw new Error("Invalid device status timeout. Expected a positive integer.");
6452
+ if (messageType === "stop_reason" || messageType === "ping") {
6453
+ return null;
6109
6454
  }
6110
- return this.refreshDeviceStatus(timeoutMs);
6455
+ return {
6456
+ type: "stream_event",
6457
+ event: delta,
6458
+ uuid
6459
+ };
6111
6460
  }
6112
- onDeviceStatus(listener) {
6113
- if (typeof listener !== "function") {
6114
- throw new Error("Invalid device status listener. Expected a function.");
6115
- }
6116
- this.deviceStatusListeners.add(listener);
6117
- return () => {
6118
- this.deviceStatusListeners.delete(listener);
6461
+ resultFromTurn(turn, tracker) {
6462
+ const stopReason = turn.stopReason ?? (turn.success === false ? "error" : undefined);
6463
+ const approvalConflict = isApprovalConflictSignal({
6464
+ detail: turn.detail,
6465
+ stopReason
6466
+ });
6467
+ const success = turn.success !== undefined ? turn.success && !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "") : !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "");
6468
+ const errorCode = approvalConflict ? "approval_conflict" : turn.errorCode ?? toSdkErrorCode(stopReason);
6469
+ return {
6470
+ type: "result",
6471
+ success,
6472
+ result: success ? tracker?.assistantText || undefined : undefined,
6473
+ error: success ? undefined : errorCode ?? stopReason ?? "error",
6474
+ errorCode: success ? undefined : errorCode ?? "error",
6475
+ approvalConflict: approvalConflict || undefined,
6476
+ recoverable: approvalConflict ? true : success ? undefined : false,
6477
+ errorDetail: success ? undefined : turn.detail,
6478
+ stopReason,
6479
+ durationMs: Date.now() - (tracker?.startedAt || this._activeTurnStartedAt),
6480
+ conversationId: turn.runtime.conversation_id,
6481
+ runIds: turn.runIds.length > 0 ? turn.runIds : undefined
6119
6482
  };
6120
6483
  }
6121
- refreshDeviceStatus(timeoutMs) {
6122
- const controller = this.controller;
6123
- const runtime = this.runtime;
6124
- if (!controller || !runtime) {
6125
- return Promise.reject(new Error("Session is not initialized"));
6484
+ enqueue(message) {
6485
+ const resolver = this.streamResolvers.shift();
6486
+ if (resolver) {
6487
+ resolver(message);
6488
+ return;
6126
6489
  }
6127
- return new Promise((resolve, reject) => {
6128
- let status = null;
6129
- let syncAcknowledged = false;
6130
- let settled = false;
6131
- const cleanup = () => {
6132
- clearTimeout(timer);
6133
- unsubscribe();
6134
- this.deviceStatusRefreshCancels.delete(cancel);
6135
- };
6136
- const rejectOnce = (error) => {
6137
- if (settled)
6138
- return;
6139
- settled = true;
6140
- cleanup();
6141
- reject(error);
6142
- };
6143
- const resolveIfComplete = () => {
6144
- if (settled || !syncAcknowledged || !status)
6145
- return;
6146
- settled = true;
6147
- cleanup();
6148
- resolve(status);
6149
- };
6150
- const cancel = (error) => rejectOnce(error);
6151
- const timer = setTimeout(() => {
6152
- rejectOnce(new Error(`Timed out waiting for ${this.label} device status`));
6153
- }, timeoutMs);
6154
- timer.unref?.();
6155
- const unsubscribe = this.onDeviceStatus((nextStatus) => {
6156
- status = nextStatus;
6157
- resolveIfComplete();
6158
- });
6159
- this.deviceStatusRefreshCancels.add(cancel);
6160
- controller.request("sync", {
6161
- runtime,
6162
- recover_approvals: false,
6163
- force_device_status: true
6164
- }, {
6165
- timeoutMs,
6166
- predicate: (message) => message.type === "sync_response"
6167
- }).then((response) => {
6168
- if (response.success === false) {
6169
- rejectOnce(new Error(typeof response.error === "string" ? response.error : `Failed to refresh ${this.label} device status`));
6170
- return;
6171
- }
6172
- syncAcknowledged = true;
6173
- resolveIfComplete();
6174
- }, (error) => {
6175
- rejectOnce(error instanceof Error ? error : new Error(String(error)));
6176
- });
6177
- });
6490
+ this.streamQueue.push(message);
6178
6491
  }
6179
- async listMessages(options = {}) {
6180
- if (!this.initialized) {
6181
- await this.initialize();
6492
+ resolveAll(value) {
6493
+ for (const resolve of this.streamResolvers.splice(0)) {
6494
+ resolve(value);
6182
6495
  }
6183
- if (!this.controller) {
6184
- throw new Error("Session is not initialized");
6496
+ }
6497
+ }
6498
+ // src/remote-client-session-core.ts
6499
+ class RemoteClientSessionCore {
6500
+ mode;
6501
+ controller = null;
6502
+ runtime = null;
6503
+ initialized = false;
6504
+ closed = false;
6505
+ _agentId = null;
6506
+ _sessionId = null;
6507
+ _conversationId = null;
6508
+ _model = "";
6509
+ _modelSettings = null;
6510
+ label;
6511
+ requestTimeoutMs;
6512
+ initializePromise = null;
6513
+ removeMessageHandler = null;
6514
+ turns;
6515
+ toolNames;
6516
+ deviceStatusListeners = new Set;
6517
+ deviceStatusRefreshCancels = new Set;
6518
+ constructor(mode, config) {
6519
+ this.mode = mode;
6520
+ this.label = config.label;
6521
+ this.requestTimeoutMs = config.requestTimeoutMs;
6522
+ this.turns = new RemoteTurnCoordinator({
6523
+ label: config.label,
6524
+ requestTimeoutMs: config.requestTimeoutMs,
6525
+ onDeviceStatus: (status) => this.emitDeviceStatus(status)
6526
+ });
6527
+ }
6528
+ async initialize() {
6529
+ if (this.closed) {
6530
+ throw new Error("Session is closed");
6185
6531
  }
6186
- const conversationId = options.conversationId ?? this._conversationId;
6187
- if (!conversationId) {
6188
- throw new Error("No conversation id available for listMessages()");
6532
+ if (this.initializePromise) {
6533
+ return this.initializePromise;
6189
6534
  }
6190
- return this.controller.listMessages(conversationId, options);
6191
- }
6192
- async bootstrapState(options = {}) {
6193
- if (!this.initialized) {
6194
- await this.initialize();
6535
+ if (this.initialized) {
6536
+ throw new Error("Session already initialized");
6195
6537
  }
6196
- const page = await this.listMessages({
6197
- limit: options.limit,
6198
- order: options.order
6538
+ const attempt = this.performInitialize();
6539
+ const memo = attempt.catch((error) => {
6540
+ this.cleanupFailedInitialize();
6541
+ throw error;
6542
+ }).finally(() => {
6543
+ if (this.initializePromise === memo) {
6544
+ this.initializePromise = null;
6545
+ }
6199
6546
  });
6200
- const state = {
6201
- agentId: this._agentId ?? "",
6202
- conversationId: this._conversationId ?? "",
6203
- model: this._model,
6204
- messages: page.messages
6205
- };
6206
- if (this.toolNames !== undefined) {
6207
- state.tools = this.toolNames;
6208
- }
6209
- if (page.nextBefore !== undefined) {
6210
- state.nextBefore = page.nextBefore;
6547
+ this.initializePromise = memo;
6548
+ return memo;
6549
+ }
6550
+ async performInitialize() {
6551
+ const init = await this.initializeRuntimeController();
6552
+ this.controller = init.controller;
6553
+ this.runtime = init.runtime;
6554
+ this._agentId = init.runtime.agent_id;
6555
+ this._conversationId = init.runtime.conversation_id;
6556
+ this._sessionId = `${init.runtime.agent_id}:${init.runtime.conversation_id}`;
6557
+ this._modelSettings = init.modelSettings ?? null;
6558
+ this._model = typeof init.model === "string" ? init.model : typeof this._modelSettings?.model === "string" ? this._modelSettings.model : "";
6559
+ this.toolNames = init.tools;
6560
+ this.removeMessageHandler = this.controller.onMessage((message) => {
6561
+ if (this.runtime)
6562
+ this.turns.handleProtocolMessage(message, this.runtime);
6563
+ });
6564
+ await this.afterRuntimeInitialized();
6565
+ await this.applyPostInitializeOptions();
6566
+ if (this.closed) {
6567
+ throw new Error("Session is closed");
6211
6568
  }
6212
- if (page.hasMore !== undefined) {
6213
- state.hasMore = page.hasMore;
6569
+ this.initialized = true;
6570
+ const initMessage = {
6571
+ type: "init",
6572
+ agentId: init.runtime.agent_id,
6573
+ sessionId: this._sessionId,
6574
+ conversationId: init.runtime.conversation_id,
6575
+ model: this._model
6576
+ };
6577
+ if (this.toolNames !== undefined)
6578
+ initMessage.tools = this.toolNames;
6579
+ if (init.skillSources !== undefined) {
6580
+ initMessage.skillSources = init.skillSources;
6214
6581
  }
6215
- return state;
6582
+ return initMessage;
6216
6583
  }
6217
- close() {
6218
- if (this.closed)
6219
- return;
6220
- this.closed = true;
6584
+ cleanupFailedInitialize() {
6221
6585
  this.removeMessageHandler?.();
6222
6586
  this.removeMessageHandler = null;
6223
- if (this.activeTurn?.timeout)
6224
- clearTimeout(this.activeTurn.timeout);
6225
- for (const turn of this.pendingTurns) {
6226
- if (turn.timeout)
6227
- clearTimeout(turn.timeout);
6228
- }
6229
- this.activeTurn = null;
6230
- this.pendingTurns.length = 0;
6231
- for (const cancel of [...this.deviceStatusRefreshCancels]) {
6232
- cancel(new Error(`Session closed while waiting for ${this.label} device status`));
6233
- }
6234
- this.deviceStatusRefreshCancels.clear();
6235
- this.deviceStatusListeners.clear();
6236
6587
  this.controller?.close();
6237
6588
  this.controller = null;
6238
6589
  this.onCoreClose();
6239
- this.resolveAll(null);
6590
+ this.runtime = null;
6591
+ this._agentId = null;
6592
+ this._conversationId = null;
6593
+ this._sessionId = null;
6594
+ this._modelSettings = null;
6595
+ this._model = "";
6596
+ this.toolNames = undefined;
6597
+ this.initialized = false;
6240
6598
  }
6241
- get agentId() {
6242
- return this._agentId;
6599
+ async send(message) {
6600
+ if (!this.initialized) {
6601
+ await this.initialize();
6602
+ }
6603
+ if (!this.controller || !this.runtime) {
6604
+ throw new Error("Session is not initialized");
6605
+ }
6606
+ await this.beforeTurn();
6607
+ const turn = this.turns.trackSentTurn(this.runtime);
6608
+ try {
6609
+ this.controller.sendTurnMessage(this.runtime, message, {
6610
+ clientMessageId: turn.clientMessageId
6611
+ });
6612
+ } catch (error) {
6613
+ this.turns.removeTrackedTurn(turn);
6614
+ throw error;
6615
+ }
6243
6616
  }
6244
- get sessionId() {
6245
- return this._sessionId;
6617
+ async runTurn(message, _options = {}) {
6618
+ if (this.turns.hasInFlightTurn()) {
6619
+ throw new Error(`A turn is already in flight for this ${this.label} session. Use send() and stream() to let the listener queue messages.`);
6620
+ }
6621
+ await this.send(message);
6622
+ for await (const msg of this.stream()) {
6623
+ if (msg.type === "result") {
6624
+ return msg;
6625
+ }
6626
+ }
6627
+ return {
6628
+ type: "result",
6629
+ success: false,
6630
+ error: "stream_closed",
6631
+ errorCode: "stream_closed",
6632
+ recoverable: false,
6633
+ errorDetail: "Stream ended before terminal result",
6634
+ durationMs: Date.now() - this.turns.activeTurnStartedAt,
6635
+ conversationId: this._conversationId
6636
+ };
6246
6637
  }
6247
- get conversationId() {
6248
- return this._conversationId;
6638
+ async* stream() {
6639
+ while (true) {
6640
+ const msg = await this.turns.nextMessage();
6641
+ if (!msg)
6642
+ break;
6643
+ yield msg;
6644
+ if (msg.type === "result")
6645
+ break;
6646
+ }
6647
+ }
6648
+ async abort() {
6649
+ if (!this.initialized)
6650
+ return;
6651
+ if (!this.controller || !this.runtime)
6652
+ return;
6653
+ this.turns.markAbortRequested();
6654
+ await this.controller.abort(this.runtime);
6655
+ }
6656
+ async sendCommand(command, options) {
6657
+ if (!this.initialized) {
6658
+ await this.initialize();
6659
+ }
6660
+ if (!this.controller) {
6661
+ throw new Error("Session is not initialized");
6662
+ }
6663
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
6664
+ throw new Error("Invalid command. Expected a protocol command object.");
6665
+ }
6666
+ if (typeof command.type !== "string" || command.type.length === 0) {
6667
+ throw new Error("Invalid command. Expected a non-empty type.");
6668
+ }
6669
+ if (!options || !options.responseType && !options.predicate && options.timeoutMs === undefined) {
6670
+ this.controller.send(command);
6671
+ return;
6672
+ }
6673
+ const { type, ...body } = command;
6674
+ const response = await this.controller.request(type, body, {
6675
+ ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {},
6676
+ predicate: options.predicate ? (message) => options.predicate?.(message) === true : options.responseType ? (message) => message.type === options.responseType : undefined
6677
+ });
6678
+ return response;
6249
6679
  }
6250
- async[Symbol.asyncDispose]() {
6251
- this.close();
6680
+ async listModels() {
6681
+ if (!this.initialized) {
6682
+ await this.initialize();
6683
+ }
6684
+ if (!this.controller) {
6685
+ throw new Error("Session is not initialized");
6686
+ }
6687
+ return this.controller.listModels();
6252
6688
  }
6253
- async changeDeviceState(updates) {
6689
+ async updateModel(update) {
6254
6690
  if (!this.initialized) {
6255
6691
  await this.initialize();
6256
6692
  }
6257
6693
  if (!this.controller || !this.runtime) {
6258
6694
  throw new Error("Session is not initialized");
6259
6695
  }
6260
- const payload = {};
6261
- if (updates.cwd !== undefined)
6262
- payload.cwd = updates.cwd;
6263
- if (updates.permissionMode !== undefined) {
6264
- const mode = mapPermissionMode(updates.permissionMode);
6265
- if (mode !== undefined)
6266
- payload.mode = mode;
6696
+ return this.applyModelUpdate(update);
6697
+ }
6698
+ async applyModelUpdate(update) {
6699
+ if (!this.controller || !this.runtime) {
6700
+ throw new Error("Session is not initialized");
6267
6701
  }
6268
- if (Object.keys(payload).length === 0) {
6269
- throw new Error("Invalid device state update. Expected cwd or permissionMode.");
6702
+ const normalized = normalizeUpdateModelInput(update);
6703
+ const payload = await this.resolveUpdateModelPayload(normalized);
6704
+ const result = await this.controller.updateModel(this.runtime, payload);
6705
+ if (result.modelHandle !== undefined) {
6706
+ this._model = result.modelHandle;
6707
+ } else if (payload.model_handle !== undefined) {
6708
+ this._model = payload.model_handle;
6709
+ } else if (typeof result.modelSettings?.model === "string") {
6710
+ this._model = result.modelSettings.model;
6270
6711
  }
6271
- this.controller.send({
6272
- type: "change_device_state",
6273
- runtime: this.runtime,
6274
- payload
6275
- });
6712
+ if ("modelSettings" in result) {
6713
+ this._modelSettings = result.modelSettings ?? null;
6714
+ }
6715
+ return result;
6276
6716
  }
6277
- async updateToolset(toolsetPreference) {
6717
+ async recoverPendingApprovals(options = {}) {
6278
6718
  if (!this.initialized) {
6279
6719
  await this.initialize();
6280
6720
  }
6281
6721
  if (!this.controller || !this.runtime) {
6282
6722
  throw new Error("Session is not initialized");
6283
6723
  }
6284
- const response = await this.controller.request("update_toolset", {
6285
- runtime: this.runtime,
6286
- toolset_preference: toolsetPreference
6287
- }, { predicate: (message) => message.type === "update_toolset_response" });
6288
- ensureSuccess(response, "Failed to update toolset");
6289
- }
6290
- async afterRuntimeInitialized() {}
6291
- async beforeTurn() {}
6292
- onCoreClose() {}
6293
- currentOptions() {
6294
- return this.mode.options;
6295
- }
6296
- shouldEnableMemfs(options) {
6297
- return false;
6298
- }
6299
- enableMemfsBody() {
6300
- if (!this.runtime)
6301
- return {};
6302
- return { agent_id: this.runtime.agent_id };
6303
- }
6304
- setModel(model) {
6305
- this._model = model;
6724
+ return this.controller.recoverPendingApprovals(this.runtime, options);
6306
6725
  }
6307
- trackSentTurn(runtime) {
6308
- const turn = {
6309
- id: ++this.nextTurnId,
6310
- runtime,
6311
- clientMessageId: `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
6312
- queuedAt: Date.now(),
6313
- startedAt: 0,
6314
- assistantText: "",
6315
- runIds: new Set,
6316
- observedTurnEvidence: false,
6317
- observedRequiresApprovalStop: false,
6318
- abortRequested: false,
6319
- timeout: null
6320
- };
6321
- if (this.activeTurn) {
6322
- this.pendingTurns.push(turn);
6323
- } else {
6324
- this.activateTurn(turn);
6726
+ async removeQueuedMessage(itemId) {
6727
+ if (typeof itemId !== "string" || itemId.trim().length === 0) {
6728
+ throw new Error("Invalid queue item id. Expected a non-empty string.");
6325
6729
  }
6326
- return turn;
6327
- }
6328
- activateTurn(turn) {
6329
- this.activeTurn = turn;
6330
- turn.startedAt = Date.now();
6331
- this.activeTurnStartedAt = turn.startedAt;
6332
- if (this.requestTimeoutMs !== undefined) {
6333
- turn.timeout = setTimeout(() => {
6334
- this.failTurn(turn, `Timed out waiting for ${this.label} turn`);
6335
- }, this.requestTimeoutMs);
6336
- turn.timeout.unref?.();
6730
+ if (!this.initialized) {
6731
+ await this.initialize();
6337
6732
  }
6338
- }
6339
- activateNextTurnFromProtocol() {
6340
- if (this.activeTurn)
6341
- return this.activeTurn;
6342
- const next = this.pendingTurns.shift();
6343
- if (!next)
6344
- return null;
6345
- this.activateTurn(next);
6346
- return next;
6347
- }
6348
- removeTrackedTurn(turn) {
6349
- if (turn.timeout)
6350
- clearTimeout(turn.timeout);
6351
- if (this.activeTurn === turn) {
6352
- this.activeTurn = null;
6353
- return;
6733
+ if (!this.controller || !this.runtime) {
6734
+ throw new Error("Session is not initialized");
6354
6735
  }
6355
- const index = this.pendingTurns.indexOf(turn);
6356
- if (index !== -1)
6357
- this.pendingTurns.splice(index, 1);
6358
- }
6359
- failTurn(turn, detail) {
6360
- if (this.activeTurn !== turn)
6361
- return;
6362
- this.enqueue({
6363
- type: "error",
6364
- message: detail,
6365
- errorCode: "error",
6366
- stopReason: "error",
6367
- errorDetail: detail,
6368
- recoverable: false
6369
- });
6370
- this.completeActiveTurn({
6371
- runtime: turn.runtime,
6372
- stopReason: "error",
6373
- runIds: [...turn.runIds],
6374
- success: false,
6375
- detail,
6376
- errorCode: "error"
6736
+ const response = await this.controller.request("remove_queue_item", {
6737
+ runtime: this.runtime,
6738
+ item_id: itemId
6739
+ }, {
6740
+ predicate: (message) => message.type === "remove_queue_item_response" && message.item_id === itemId
6377
6741
  });
6378
- }
6379
- completeActiveTurn(turn) {
6380
- const active = this.activeTurn;
6381
- if (!active)
6382
- return;
6383
- if (active.timeout) {
6384
- clearTimeout(active.timeout);
6385
- active.timeout = null;
6742
+ if (typeof response.success !== "boolean") {
6743
+ throw new Error("Invalid remove_queue_item_response from runtime");
6386
6744
  }
6387
- this.enqueue(this.resultFromTurn(turn, active));
6388
- this.activeTurn = null;
6745
+ return {
6746
+ itemId: typeof response.item_id === "string" ? response.item_id : itemId,
6747
+ removed: response.success
6748
+ };
6389
6749
  }
6390
- async resolveUpdateModelPayload(input) {
6391
- if (input.reasoningEffort === undefined) {
6392
- return modelPayloadWithoutReasoning(input);
6750
+ async getDeviceStatus(options = {}) {
6751
+ if (!this.initialized) {
6752
+ await this.initialize();
6393
6753
  }
6394
- if (!this.controller) {
6754
+ if (!this.controller || !this.runtime) {
6395
6755
  throw new Error("Session is not initialized");
6396
6756
  }
6397
- const catalog = await this.controller.listModels();
6398
- const byId = new Map(catalog.entries.map((entry) => [entry.id, entry]));
6399
- const aliases = catalog.byokProviderAliases;
6400
- let baseEntry;
6401
- let explicitHandle;
6402
- let targetHandle;
6403
- if (input.modelId !== undefined) {
6404
- baseEntry = byId.get(input.modelId);
6405
- explicitHandle = input.modelHandle;
6406
- targetHandle = baseEntry?.handle ?? toBaseModelHandle(input.modelHandle, aliases);
6407
- } else if (input.modelHandle !== undefined) {
6408
- explicitHandle = input.modelHandle;
6409
- targetHandle = toBaseModelHandle(input.modelHandle, aliases);
6410
- } else if (input.model !== undefined) {
6411
- baseEntry = byId.get(input.model);
6412
- if (baseEntry) {
6413
- targetHandle = baseEntry.handle;
6414
- } else {
6415
- explicitHandle = input.model;
6416
- targetHandle = toBaseModelHandle(input.model, aliases);
6417
- }
6418
- } else {
6419
- explicitHandle = this._model || undefined;
6420
- targetHandle = toBaseModelHandle(this._model || undefined, aliases);
6421
- }
6422
- if (!targetHandle) {
6423
- throw new Error("reasoningEffort requires a current model or explicit model/modelId/modelHandle.");
6424
- }
6425
- const candidates = catalog.entries.filter((entry) => entry.handle === targetHandle || entry.handle === explicitHandle);
6426
- if (candidates.length === 0) {
6427
- throw new Error(`reasoningEffort requires a model from listModels(); no catalog entry found for ${targetHandle}.`);
6428
- }
6429
- const contextWindow = getContextWindow(baseEntry?.updateArgs) ?? getContextWindow(this._modelSettings);
6430
- const scopedCandidates = sameContextCandidates(candidates, contextWindow);
6431
- const matchingEntry = scopedCandidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort) ?? candidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort);
6432
- if (!matchingEntry) {
6433
- throw new Error(`No ${input.reasoningEffort} reasoning tier found for model ${targetHandle}.`);
6757
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs ?? 30000;
6758
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
6759
+ throw new Error("Invalid device status timeout. Expected a positive integer.");
6434
6760
  }
6435
- const payload = { model_id: matchingEntry.id };
6436
- if (explicitHandle !== undefined) {
6437
- payload.model_handle = explicitHandle;
6761
+ return this.refreshDeviceStatus(timeoutMs);
6762
+ }
6763
+ onDeviceStatus(listener) {
6764
+ if (typeof listener !== "function") {
6765
+ throw new Error("Invalid device status listener. Expected a function.");
6438
6766
  }
6439
- return payload;
6767
+ this.deviceStatusListeners.add(listener);
6768
+ return () => {
6769
+ this.deviceStatusListeners.delete(listener);
6770
+ };
6440
6771
  }
6441
- async applyPostInitializeOptions() {
6442
- if (!this.controller || !this.runtime)
6443
- return;
6444
- const options = this.currentOptions();
6445
- if (this.shouldEnableMemfs(options)) {
6446
- const response = await this.controller.request("enable_memfs", this.enableMemfsBody(), {
6447
- predicate: (message) => message.type === "enable_memfs_response",
6448
- timeoutMs: 180000
6772
+ refreshDeviceStatus(timeoutMs) {
6773
+ const controller = this.controller;
6774
+ const runtime = this.runtime;
6775
+ if (!controller || !runtime) {
6776
+ return Promise.reject(new Error("Session is not initialized"));
6777
+ }
6778
+ return new Promise((resolve, reject) => {
6779
+ let status = null;
6780
+ let syncAcknowledged = false;
6781
+ let settled = false;
6782
+ const cleanup = () => {
6783
+ clearTimeout(timer);
6784
+ unsubscribe();
6785
+ this.deviceStatusRefreshCancels.delete(cancel);
6786
+ };
6787
+ const rejectOnce = (error) => {
6788
+ if (settled)
6789
+ return;
6790
+ settled = true;
6791
+ cleanup();
6792
+ reject(error);
6793
+ };
6794
+ const resolveIfComplete = () => {
6795
+ if (settled || !syncAcknowledged || !status)
6796
+ return;
6797
+ settled = true;
6798
+ cleanup();
6799
+ resolve(status);
6800
+ };
6801
+ const cancel = (error) => rejectOnce(error);
6802
+ const timer = setTimeout(() => {
6803
+ rejectOnce(new Error(`Timed out waiting for ${this.label} device status`));
6804
+ }, timeoutMs);
6805
+ timer.unref?.();
6806
+ const unsubscribe = this.onDeviceStatus((nextStatus) => {
6807
+ status = nextStatus;
6808
+ resolveIfComplete();
6449
6809
  });
6450
- ensureSuccess(response, "Failed to enable memfs");
6810
+ this.deviceStatusRefreshCancels.add(cancel);
6811
+ controller.request("sync", {
6812
+ runtime,
6813
+ recover_approvals: false,
6814
+ force_device_status: true
6815
+ }, {
6816
+ timeoutMs,
6817
+ predicate: (message) => message.type === "sync_response"
6818
+ }).then((response) => {
6819
+ if (response.success === false) {
6820
+ rejectOnce(new Error(typeof response.error === "string" ? response.error : `Failed to refresh ${this.label} device status`));
6821
+ return;
6822
+ }
6823
+ syncAcknowledged = true;
6824
+ resolveIfComplete();
6825
+ }, (error) => {
6826
+ rejectOnce(error instanceof Error ? error : new Error(String(error)));
6827
+ });
6828
+ });
6829
+ }
6830
+ async listMessages(options = {}) {
6831
+ if (!this.initialized) {
6832
+ await this.initialize();
6451
6833
  }
6452
- const dreamingSettings = resolveDreamingSettings(options.dreaming);
6453
- if (dreamingSettings) {
6454
- const response = await this.controller.request("set_reflection_settings", {
6455
- runtime: this.runtime,
6456
- settings: dreamingSettings,
6457
- scope: "both"
6458
- }, { predicate: (message) => message.type === "set_reflection_settings_response" });
6459
- ensureSuccess(response, "Failed to update dreaming settings");
6834
+ if (!this.controller) {
6835
+ throw new Error("Session is not initialized");
6460
6836
  }
6461
- if (this.mode.kind !== "session")
6462
- return;
6463
- if (this.mode.options.model !== undefined || this.mode.options.reasoningEffort !== undefined) {
6464
- await this.updateModel({
6465
- ...this.mode.options.model !== undefined ? { model: this.mode.options.model } : {},
6466
- ...this.mode.options.reasoningEffort !== undefined ? { reasoningEffort: this.mode.options.reasoningEffort } : {}
6467
- });
6837
+ const conversationId = options.conversationId ?? this._conversationId;
6838
+ if (!conversationId) {
6839
+ throw new Error("No conversation id available for listMessages()");
6468
6840
  }
6841
+ return this.controller.listMessages(conversationId, options);
6469
6842
  }
6470
- handleProtocolMessage = (message) => {
6471
- if (!this.runtime || !sameRuntime2(message, this.runtime))
6472
- return;
6473
- const deviceStatus = deviceStatusRecord(message);
6474
- if (deviceStatus) {
6475
- const status = toSessionDeviceStatus(deviceStatus);
6476
- if (!status)
6477
- return;
6478
- for (const listener of [...this.deviceStatusListeners]) {
6479
- try {
6480
- listener(status);
6481
- } catch {}
6482
- }
6483
- return;
6843
+ async bootstrapState(options = {}) {
6844
+ if (!this.initialized) {
6845
+ await this.initialize();
6484
6846
  }
6485
- if (message.type === "update_queue") {
6486
- const sdkMessage2 = {
6487
- type: "queue_update",
6488
- queue: queueItems(message)
6489
- };
6490
- this.enqueue(sdkMessage2);
6491
- return;
6847
+ const page = await this.listMessages({
6848
+ limit: options.limit,
6849
+ order: options.order
6850
+ });
6851
+ const state = {
6852
+ agentId: this._agentId ?? "",
6853
+ conversationId: this._conversationId ?? "",
6854
+ model: this._model,
6855
+ messages: page.messages
6856
+ };
6857
+ if (this.toolNames !== undefined) {
6858
+ state.tools = this.toolNames;
6492
6859
  }
6493
- if (message.type === "update_loop_status") {
6494
- this.handleLoopStatusMessage(message);
6495
- return;
6860
+ if (page.nextBefore !== undefined) {
6861
+ state.nextBefore = page.nextBefore;
6496
6862
  }
6497
- const delta = streamDeltaRecord(message);
6498
- if (!delta)
6863
+ if (page.hasMore !== undefined) {
6864
+ state.hasMore = page.hasMore;
6865
+ }
6866
+ return state;
6867
+ }
6868
+ close() {
6869
+ if (this.closed)
6499
6870
  return;
6500
- const active = this.activateNextTurnFromProtocol();
6501
- if (active) {
6502
- active.observedTurnEvidence = true;
6503
- const runId = streamDeltaRunId2(delta);
6504
- if (runId)
6505
- active.runIds.add(runId);
6871
+ this.closed = true;
6872
+ this.removeMessageHandler?.();
6873
+ this.removeMessageHandler = null;
6874
+ this.turns.close();
6875
+ for (const cancel of [...this.deviceStatusRefreshCancels]) {
6876
+ cancel(new Error(`Session closed while waiting for ${this.label} device status`));
6506
6877
  }
6507
- const sdkMessage = this.transformStreamDelta(delta);
6508
- if (sdkMessage) {
6509
- this.enqueue(sdkMessage);
6878
+ this.deviceStatusRefreshCancels.clear();
6879
+ this.deviceStatusListeners.clear();
6880
+ this.controller?.close();
6881
+ this.controller = null;
6882
+ this.onCoreClose();
6883
+ }
6884
+ get agentId() {
6885
+ return this._agentId;
6886
+ }
6887
+ get sessionId() {
6888
+ return this._sessionId;
6889
+ }
6890
+ get conversationId() {
6891
+ return this._conversationId;
6892
+ }
6893
+ async[Symbol.asyncDispose]() {
6894
+ this.close();
6895
+ }
6896
+ async changeDeviceState(updates) {
6897
+ if (!this.initialized) {
6898
+ await this.initialize();
6510
6899
  }
6511
- this.handleTurnTerminalDelta(delta, sdkMessage);
6512
- };
6513
- handleLoopStatusMessage(message) {
6514
- const status = loopStatusValue(message);
6515
- if (!status)
6516
- return;
6517
- const activeRunIds = loopStatusRunIds(message);
6518
- const sdkMessage = {
6519
- type: "loop_status",
6520
- status,
6521
- activeRunIds
6522
- };
6523
- this.enqueue(sdkMessage);
6524
- const active = this.activeTurn;
6525
- if (!active)
6526
- return;
6527
- for (const runId of activeRunIds)
6528
- active.runIds.add(runId);
6529
- const hadTurnEvidence = active.observedTurnEvidence || active.observedRequiresApprovalStop;
6530
- if (!hadTurnEvidence)
6531
- return;
6532
- if (status === "WAITING_ON_APPROVAL") {
6533
- this.completeActiveTurn({
6534
- runtime: active.runtime,
6535
- stopReason: "requires_approval",
6536
- runIds: [...active.runIds]
6537
- });
6538
- return;
6900
+ if (!this.controller || !this.runtime) {
6901
+ throw new Error("Session is not initialized");
6539
6902
  }
6540
- if (status === "WAITING_ON_INPUT" && active.abortRequested) {
6541
- this.completeActiveTurn({
6542
- runtime: active.runtime,
6543
- stopReason: "interrupted",
6544
- runIds: [...active.runIds],
6545
- success: false,
6546
- detail: "Interrupted",
6547
- errorCode: "interrupted"
6548
- });
6549
- return;
6903
+ const payload = {};
6904
+ if (updates.cwd !== undefined)
6905
+ payload.cwd = updates.cwd;
6906
+ if (updates.permissionMode !== undefined) {
6907
+ const mode = mapPermissionMode(updates.permissionMode);
6908
+ if (mode !== undefined)
6909
+ payload.mode = mode;
6550
6910
  }
6551
- if (status === "WAITING_ON_INPUT" && active.observedTurnEvidence) {
6552
- this.completeActiveTurn({
6553
- runtime: active.runtime,
6554
- stopReason: null,
6555
- runIds: [...active.runIds]
6556
- });
6911
+ if (Object.keys(payload).length === 0) {
6912
+ throw new Error("Invalid device state update. Expected cwd or permissionMode.");
6557
6913
  }
6914
+ this.controller.send({
6915
+ type: "change_device_state",
6916
+ runtime: this.runtime,
6917
+ payload
6918
+ });
6558
6919
  }
6559
- handleTurnTerminalDelta(delta, sdkMessage) {
6560
- const active = this.activeTurn;
6561
- if (!active)
6562
- return;
6563
- const messageType = streamDeltaMessageType2(delta);
6564
- if (messageType === "stop_reason") {
6565
- const stopReason = streamDeltaStopReason2(delta) ?? null;
6566
- if (stopReason === "requires_approval") {
6567
- active.observedRequiresApprovalStop = true;
6568
- return;
6569
- }
6570
- this.completeActiveTurn({
6571
- runtime: active.runtime,
6572
- stopReason,
6573
- runIds: [...active.runIds]
6574
- });
6575
- return;
6920
+ async updateToolset(toolsetPreference) {
6921
+ if (!this.initialized) {
6922
+ await this.initialize();
6576
6923
  }
6577
- if (sdkMessage?.type === "error") {
6578
- this.completeActiveTurn({
6579
- runtime: active.runtime,
6580
- stopReason: sdkMessage.stopReason,
6581
- runIds: [...active.runIds],
6582
- success: false,
6583
- detail: sdkMessage.errorDetail ?? sdkMessage.message,
6584
- errorCode: sdkMessage.errorCode
6585
- });
6924
+ if (!this.controller || !this.runtime) {
6925
+ throw new Error("Session is not initialized");
6586
6926
  }
6927
+ const response = await this.controller.request("update_toolset", {
6928
+ runtime: this.runtime,
6929
+ toolset_preference: toolsetPreference
6930
+ }, { predicate: (message) => message.type === "update_toolset_response" });
6931
+ ensureSuccess(response, "Failed to update toolset");
6587
6932
  }
6588
- transformStreamDelta(delta) {
6589
- const messageType = typeof delta.message_type === "string" ? delta.message_type : undefined;
6590
- const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
6591
- const uuid = typeof delta.id === "string" ? delta.id : `${this.label}-${++this.messageCounter}`;
6592
- if (messageType === "assistant_message") {
6593
- const content = extractTextFromContent(delta.content);
6594
- if (!content)
6595
- return null;
6596
- if (this.activeTurn)
6597
- this.activeTurn.assistantText += content;
6598
- return { type: "assistant", content, uuid, runId };
6933
+ async afterRuntimeInitialized() {}
6934
+ async beforeTurn() {}
6935
+ onCoreClose() {}
6936
+ currentOptions() {
6937
+ return this.mode.options;
6938
+ }
6939
+ shouldEnableMemfs(options) {
6940
+ return false;
6941
+ }
6942
+ enableMemfsBody() {
6943
+ if (!this.runtime)
6944
+ return {};
6945
+ return { agent_id: this.runtime.agent_id };
6946
+ }
6947
+ setModel(model) {
6948
+ this._model = model;
6949
+ }
6950
+ async resolveUpdateModelPayload(input) {
6951
+ if (input.reasoningEffort === undefined) {
6952
+ return modelPayloadWithoutReasoning(input);
6599
6953
  }
6600
- if (messageType === "reasoning_message") {
6601
- const content = typeof delta.reasoning === "string" ? delta.reasoning : extractTextFromContent(delta.content);
6602
- if (!content)
6603
- return null;
6604
- return { type: "reasoning", content, uuid, runId };
6954
+ if (!this.controller) {
6955
+ throw new Error("Session is not initialized");
6605
6956
  }
6606
- if (messageType === "tool_call_message" || messageType === "approval_request_message") {
6607
- const toolCall = firstToolCall(delta);
6608
- if (!toolCall)
6609
- return null;
6610
- const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
6611
- const toolCallId = (typeof toolCall.tool_call_id === "string" ? toolCall.tool_call_id : undefined) ?? (typeof toolCall.id === "string" ? toolCall.id : undefined);
6612
- if (!toolCallId) {
6613
- const detail = `Missing tool_call_id in ${messageType} (uuid=${uuid})`;
6614
- return {
6615
- type: "error",
6616
- message: detail,
6617
- errorCode: "protocol_error",
6618
- stopReason: "protocol_error",
6619
- runId,
6620
- recoverable: false,
6621
- errorDetail: detail
6622
- };
6957
+ const catalog = await this.controller.listModels();
6958
+ const byId = new Map(catalog.entries.map((entry) => [entry.id, entry]));
6959
+ const aliases = catalog.byokProviderAliases;
6960
+ let baseEntry;
6961
+ let explicitHandle;
6962
+ let targetHandle;
6963
+ if (input.modelId !== undefined) {
6964
+ baseEntry = byId.get(input.modelId);
6965
+ explicitHandle = input.modelHandle;
6966
+ targetHandle = baseEntry?.handle ?? toBaseModelHandle(input.modelHandle, aliases);
6967
+ } else if (input.modelHandle !== undefined) {
6968
+ explicitHandle = input.modelHandle;
6969
+ targetHandle = toBaseModelHandle(input.modelHandle, aliases);
6970
+ } else if (input.model !== undefined) {
6971
+ baseEntry = byId.get(input.model);
6972
+ if (baseEntry) {
6973
+ targetHandle = baseEntry.handle;
6974
+ } else {
6975
+ explicitHandle = input.model;
6976
+ targetHandle = toBaseModelHandle(input.model, aliases);
6623
6977
  }
6624
- const toolName = (typeof toolCall.name === "string" ? toolCall.name : undefined) ?? (typeof fn?.name === "string" ? fn.name : undefined) ?? "?";
6625
- const { input, raw } = toolInputFromArguments(toolCall.arguments ?? fn?.arguments);
6626
- return {
6627
- type: "tool_call",
6628
- toolCallId,
6629
- toolName,
6630
- toolInput: input,
6631
- rawArguments: raw,
6632
- uuid,
6633
- runId
6634
- };
6978
+ } else {
6979
+ explicitHandle = this._model || undefined;
6980
+ targetHandle = toBaseModelHandle(this._model || undefined, aliases);
6635
6981
  }
6636
- if (messageType === "tool_return_message") {
6637
- const toolReturn = firstToolReturn(delta) ?? delta;
6638
- const toolCallId = (typeof delta.tool_call_id === "string" ? delta.tool_call_id : undefined) ?? (typeof toolReturn.tool_call_id === "string" ? toolReturn.tool_call_id : undefined);
6639
- if (!toolCallId)
6640
- return null;
6641
- const content = extractTextFromContent(delta.tool_return ?? toolReturn.tool_return ?? toolReturn.content) ?? "";
6642
- const status = typeof delta.status === "string" ? delta.status : toolReturn.status;
6643
- return {
6644
- type: "tool_result",
6645
- toolCallId,
6646
- content,
6647
- isError: status === "error",
6648
- uuid,
6649
- runId
6650
- };
6982
+ if (!targetHandle) {
6983
+ throw new Error("reasoningEffort requires a current model or explicit model/modelId/modelHandle.");
6651
6984
  }
6652
- if (messageType === "error_message" || messageType === "loop_error") {
6653
- const detail = (typeof delta.detail === "string" ? delta.detail : undefined) ?? (typeof delta.message === "string" ? delta.message : undefined) ?? `${this.label} turn failed`;
6654
- const stopReason = (typeof delta.stop_reason === "string" ? delta.stop_reason : undefined) ?? (typeof delta.error_type === "string" ? delta.error_type : undefined) ?? "error";
6655
- const approvalConflict = isApprovalConflictSignal({
6656
- detail,
6657
- message: typeof delta.message === "string" ? delta.message : undefined,
6658
- stopReason
6659
- });
6660
- return {
6661
- type: "error",
6662
- message: detail,
6663
- errorCode: approvalConflict ? "approval_conflict" : toSdkErrorCode(stopReason),
6664
- approvalConflict: approvalConflict || undefined,
6665
- recoverable: approvalConflict ? true : false,
6666
- errorDetail: detail,
6667
- stopReason,
6668
- runId
6669
- };
6985
+ const candidates = catalog.entries.filter((entry) => entry.handle === targetHandle || entry.handle === explicitHandle);
6986
+ if (candidates.length === 0) {
6987
+ throw new Error(`reasoningEffort requires a model from listModels(); no catalog entry found for ${targetHandle}.`);
6670
6988
  }
6671
- if (messageType === "retry") {
6672
- return {
6673
- type: "retry",
6674
- reason: typeof delta.reason === "string" ? delta.reason : "error",
6675
- attempt: typeof delta.attempt === "number" ? delta.attempt : 0,
6676
- maxAttempts: typeof delta.max_attempts === "number" ? delta.max_attempts : 0,
6677
- delayMs: typeof delta.delay_ms === "number" ? delta.delay_ms : 0,
6678
- runId
6679
- };
6989
+ const contextWindow = getContextWindow(baseEntry?.updateArgs) ?? getContextWindow(this._modelSettings);
6990
+ const scopedCandidates = sameContextCandidates(candidates, contextWindow);
6991
+ const matchingEntry = scopedCandidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort) ?? candidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort);
6992
+ if (!matchingEntry) {
6993
+ throw new Error(`No ${input.reasoningEffort} reasoning tier found for model ${targetHandle}.`);
6680
6994
  }
6681
- if (messageType === "stop_reason" || messageType === "ping") {
6682
- return null;
6995
+ const payload = { model_id: matchingEntry.id };
6996
+ if (explicitHandle !== undefined) {
6997
+ payload.model_handle = explicitHandle;
6683
6998
  }
6684
- return {
6685
- type: "stream_event",
6686
- event: delta,
6687
- uuid
6688
- };
6689
- }
6690
- resultFromTurn(turn, tracker) {
6691
- const stopReason = turn.stopReason ?? (turn.success === false ? "error" : undefined);
6692
- const approvalConflict = isApprovalConflictSignal({
6693
- detail: turn.detail,
6694
- stopReason
6695
- });
6696
- const success = turn.success !== undefined ? turn.success && !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "") : !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "");
6697
- const errorCode = approvalConflict ? "approval_conflict" : turn.errorCode ?? toSdkErrorCode(stopReason);
6698
- return {
6699
- type: "result",
6700
- success,
6701
- result: success ? tracker?.assistantText || undefined : undefined,
6702
- error: success ? undefined : errorCode ?? stopReason ?? "error",
6703
- errorCode: success ? undefined : errorCode ?? "error",
6704
- approvalConflict: approvalConflict || undefined,
6705
- recoverable: approvalConflict ? true : success ? undefined : false,
6706
- errorDetail: success ? undefined : turn.detail,
6707
- stopReason,
6708
- durationMs: Date.now() - (tracker?.startedAt || this.activeTurnStartedAt),
6709
- conversationId: turn.runtime.conversation_id,
6710
- runIds: turn.runIds.length > 0 ? turn.runIds : undefined
6711
- };
6999
+ return payload;
6712
7000
  }
6713
- enqueue(message) {
6714
- const resolver = this.streamResolvers.shift();
6715
- if (resolver) {
6716
- resolver(message);
7001
+ async applyPostInitializeOptions() {
7002
+ if (!this.controller || !this.runtime)
6717
7003
  return;
7004
+ const options = this.currentOptions();
7005
+ if (this.shouldEnableMemfs(options)) {
7006
+ const response = await this.controller.request("enable_memfs", this.enableMemfsBody(), {
7007
+ predicate: (message) => message.type === "enable_memfs_response",
7008
+ timeoutMs: 180000
7009
+ });
7010
+ ensureSuccess(response, "Failed to enable memfs");
7011
+ }
7012
+ const dreamingSettings = resolveDreamingSettings(options.dreaming);
7013
+ if (dreamingSettings) {
7014
+ const response = await this.controller.request("set_reflection_settings", {
7015
+ runtime: this.runtime,
7016
+ settings: dreamingSettings,
7017
+ scope: "both"
7018
+ }, { predicate: (message) => message.type === "set_reflection_settings_response" });
7019
+ ensureSuccess(response, "Failed to update dreaming settings");
7020
+ }
7021
+ if (this.mode.kind !== "session")
7022
+ return;
7023
+ if (this.mode.options.model !== undefined || this.mode.options.reasoningEffort !== undefined) {
7024
+ await this.applyModelUpdate({
7025
+ ...this.mode.options.model !== undefined ? { model: this.mode.options.model } : {},
7026
+ ...this.mode.options.reasoningEffort !== undefined ? { reasoningEffort: this.mode.options.reasoningEffort } : {}
7027
+ });
6718
7028
  }
6719
- this.streamQueue.push(message);
6720
- }
6721
- nextMessage() {
6722
- const next = this.streamQueue.shift();
6723
- if (next)
6724
- return Promise.resolve(next);
6725
- if (this.closed)
6726
- return Promise.resolve(null);
6727
- return new Promise((resolve) => {
6728
- this.streamResolvers.push(resolve);
6729
- });
6730
7029
  }
6731
- resolveAll(value) {
6732
- for (const resolve of this.streamResolvers.splice(0)) {
6733
- resolve(value);
7030
+ emitDeviceStatus(status) {
7031
+ for (const listener of [...this.deviceStatusListeners]) {
7032
+ try {
7033
+ listener(status);
7034
+ } catch {}
6734
7035
  }
6735
7036
  }
6736
7037
  }
@@ -7224,13 +7525,15 @@ class AppServerSession extends RemoteClientSessionCore {
7224
7525
  return options.memfs !== false;
7225
7526
  }
7226
7527
  async initializeRuntimeController() {
7528
+ await this.remoteOptions.beforeConnect?.();
7227
7529
  const url = await this.resolveAppServerUrl();
7228
- const client = createAppServerClient({
7530
+ await releaseAppServerManagementConnections(url);
7531
+ const client = applyUniqueRequestIds(createAppServerClient({
7229
7532
  url,
7230
7533
  ...this.remoteOptions.authToken !== undefined ? { authToken: this.remoteOptions.authToken } : {},
7231
7534
  ...this.remoteOptions.WebSocket ? { WebSocket: this.remoteOptions.WebSocket } : {},
7232
7535
  ...this.remoteOptions.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.remoteOptions.requestTimeoutMs } : {}
7233
- });
7536
+ }));
7234
7537
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
7235
7538
  client,
7236
7539
  getRuntime: () => this.runtime,
@@ -7629,13 +7932,58 @@ class RemoteEnvironmentClient {
7629
7932
  }
7630
7933
  }
7631
7934
 
7935
+ // src/cloud-sandbox.ts
7936
+ var MIN_TTL_MINUTES = 1;
7937
+ var MAX_TTL_MINUTES = 60;
7938
+ var MAX_GITHUB_REPOSITORIES = 10;
7939
+ var GITHUB_OWNER_PATTERN = /^[A-Za-z0-9-]+$/;
7940
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]+$/;
7941
+ function validatePositiveInteger(value, name) {
7942
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
7943
+ throw new Error(`Invalid ${name}. Expected a positive integer.`);
7944
+ }
7945
+ }
7946
+ function validateCloudSandboxOptions(options, name) {
7947
+ if (options === undefined)
7948
+ return;
7949
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
7950
+ throw new Error(`Invalid ${name}. Expected an object.`);
7951
+ }
7952
+ if (options.ttlMinutes !== undefined && (!Number.isInteger(options.ttlMinutes) || options.ttlMinutes < MIN_TTL_MINUTES || options.ttlMinutes > MAX_TTL_MINUTES)) {
7953
+ throw new Error(`Invalid ${name}.ttlMinutes. Expected an integer between ${MIN_TTL_MINUTES} and ${MAX_TTL_MINUTES}.`);
7954
+ }
7955
+ validatePositiveInteger(options.readyTimeoutMs, `${name}.readyTimeoutMs`);
7956
+ validatePositiveInteger(options.readyPollIntervalMs, `${name}.readyPollIntervalMs`);
7957
+ validatePositiveInteger(options.refreshIntervalMs, `${name}.refreshIntervalMs`);
7958
+ if (options.githubRepositories !== undefined) {
7959
+ if (!Array.isArray(options.githubRepositories)) {
7960
+ throw new Error(`Invalid ${name}.githubRepositories. Expected an array.`);
7961
+ }
7962
+ if (options.githubRepositories.length > MAX_GITHUB_REPOSITORIES) {
7963
+ throw new Error(`Invalid ${name}.githubRepositories. Expected at most ${MAX_GITHUB_REPOSITORIES} repositories.`);
7964
+ }
7965
+ for (const [index, repository] of options.githubRepositories.entries()) {
7966
+ if (repository === null || typeof repository !== "object" || Array.isArray(repository)) {
7967
+ throw new Error(`Invalid ${name}.githubRepositories[${index}]. Expected an object.`);
7968
+ }
7969
+ if (typeof repository.owner !== "string" || !GITHUB_OWNER_PATTERN.test(repository.owner)) {
7970
+ throw new Error(`Invalid ${name}.githubRepositories[${index}].owner.`);
7971
+ }
7972
+ if (typeof repository.repo !== "string" || !GITHUB_REPOSITORY_PATTERN.test(repository.repo)) {
7973
+ throw new Error(`Invalid ${name}.githubRepositories[${index}].repo.`);
7974
+ }
7975
+ }
7976
+ }
7977
+ if (options.terminateOnClose !== undefined && typeof options.terminateOnClose !== "boolean") {
7978
+ throw new Error(`Invalid ${name}.terminateOnClose. Expected a boolean.`);
7979
+ }
7980
+ }
7981
+
7632
7982
  // src/cloud-session.ts
7633
7983
  var DEFAULT_CLOUD_API_BASE_URL3 = "https://api.letta.com";
7634
7984
  var DEFAULT_TURN_TIMEOUT_MS = 120000;
7635
7985
  var DEFAULT_PING_INTERVAL_MS = 30000;
7636
7986
  var DEFAULT_SANDBOX_TTL_MINUTES = 5;
7637
- var MIN_SANDBOX_TTL_MINUTES = 1;
7638
- var MAX_SANDBOX_TTL_MINUTES = 60;
7639
7987
  var DEFAULT_SANDBOX_READY_TIMEOUT_MS = 120000;
7640
7988
  var DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS = 1000;
7641
7989
  var DEFAULT_REPOSITORY_ATTACH_TIMEOUT_MS = 1e4;
@@ -7746,34 +8094,13 @@ function assertOkResponse2(response, body, action, url) {
7746
8094
  throw new Error(parts.join(" — "));
7747
8095
  }
7748
8096
  }
7749
- function validatePositiveInteger(value, name) {
8097
+ function validatePositiveInteger2(value, name) {
7750
8098
  if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
7751
8099
  throw new Error(`Invalid ${name}. Expected a positive integer.`);
7752
8100
  }
7753
8101
  }
7754
- function validateIntegerRange(value, name, min, max) {
7755
- if (value === undefined)
7756
- return;
7757
- if (!Number.isInteger(value) || value < min || value > max) {
7758
- throw new Error(`Invalid ${name}. Expected an integer between ${min} and ${max}.`);
7759
- }
7760
- }
7761
- function validateCloudSandboxOptions(options, name) {
7762
- if (options === undefined)
7763
- return;
7764
- if (options === null || typeof options !== "object" || Array.isArray(options)) {
7765
- throw new Error(`Invalid ${name}. Expected an object.`);
7766
- }
7767
- validateIntegerRange(options.ttlMinutes, `${name}.ttlMinutes`, MIN_SANDBOX_TTL_MINUTES, MAX_SANDBOX_TTL_MINUTES);
7768
- validatePositiveInteger(options.readyTimeoutMs, `${name}.readyTimeoutMs`);
7769
- validatePositiveInteger(options.readyPollIntervalMs, `${name}.readyPollIntervalMs`);
7770
- validatePositiveInteger(options.refreshIntervalMs, `${name}.refreshIntervalMs`);
7771
- if (options.terminateOnClose !== undefined && typeof options.terminateOnClose !== "boolean") {
7772
- throw new Error(`Invalid ${name}.terminateOnClose. Expected a boolean.`);
7773
- }
7774
- }
7775
8102
  function validateCloudClientOptions(options) {
7776
- validatePositiveInteger(options.requestTimeoutMs, "requestTimeoutMs");
8103
+ validatePositiveInteger2(options.requestTimeoutMs, "requestTimeoutMs");
7777
8104
  validateCloudSandboxOptions(options.sandbox, "sandbox");
7778
8105
  if (options.environment !== undefined && options.sandbox !== undefined) {
7779
8106
  throw new Error("Letta Cloud sessions cannot specify both environment and sandbox options.");
@@ -8101,14 +8428,14 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8101
8428
  apiKey,
8102
8429
  authMode: this.cloudOptions.webSocketAuth ?? "header"
8103
8430
  });
8104
- const client = createAppServerClient({
8431
+ const client = applyUniqueRequestIds(createAppServerClient({
8105
8432
  url,
8106
8433
  WebSocket: createCloudStatusWebSocketConstructor({
8107
8434
  cloudOptions: this.cloudOptions,
8108
8435
  runtime: resolved.runtime
8109
8436
  }),
8110
8437
  requestTimeoutMs: this.cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS
8111
- });
8438
+ }));
8112
8439
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
8113
8440
  client,
8114
8441
  getRuntime: () => this.runtime,
@@ -8376,10 +8703,15 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8376
8703
  async createManagedSandbox(agentId, conversationId) {
8377
8704
  const fetchImpl = getFetch3(this.cloudOptions.fetch);
8378
8705
  const baseUrl = normalizeCloudApiBaseUrl2(this.cloudOptions.apiBaseUrl);
8706
+ const sandboxOptions = this.resolvedSandboxOptions();
8707
+ const githubRepositories = sandboxOptions.githubRepositories;
8379
8708
  const response = await fetchImpl(`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sandboxes`, {
8380
8709
  method: "POST",
8381
8710
  headers: cloudHeaders2(this.cloudOptions),
8382
- body: JSON.stringify(conversationId ? { conversationId } : {})
8711
+ body: JSON.stringify({
8712
+ ...conversationId ? { conversationId } : {},
8713
+ ...githubRepositories && githubRepositories.length > 0 ? { githubRepositories } : {}
8714
+ })
8383
8715
  });
8384
8716
  const body = await parseJsonResponse3(response);
8385
8717
  assertOkResponse2(response, body, "Cloud create managed sandbox", `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sandboxes`);
@@ -8390,7 +8722,6 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8390
8722
  if (responseConversationId !== null && responseConversationId !== conversationId) {
8391
8723
  throw new Error(`Cloud managed sandbox response conversation mismatch: expected ${conversationId ?? "none"}, got ${responseConversationId}.`);
8392
8724
  }
8393
- const sandboxOptions = this.resolvedSandboxOptions();
8394
8725
  const ttlMinutes = sandboxOptions.ttlMinutes ?? DEFAULT_SANDBOX_TTL_MINUTES;
8395
8726
  const readyTimeoutMs = sandboxOptions.readyTimeoutMs ?? DEFAULT_SANDBOX_READY_TIMEOUT_MS;
8396
8727
  const readyPollIntervalMs = sandboxOptions.readyPollIntervalMs ?? DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS;
@@ -8846,6 +9177,10 @@ class LettaAgentClientBase {
8846
9177
  if (requestTimeoutMs !== undefined && (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0)) {
8847
9178
  throw new Error("Invalid appServer.requestTimeoutMs. Expected a positive integer.");
8848
9179
  }
9180
+ const idleLingerMs = localOptions.appServer?.idleLingerMs;
9181
+ if (idleLingerMs !== undefined && (!Number.isInteger(idleLingerMs) || idleLingerMs < 0)) {
9182
+ throw new Error("Invalid appServer.idleLingerMs. Expected a non-negative integer.");
9183
+ }
8849
9184
  const startupTimeoutMs = localOptions.appServer?.startupTimeoutMs;
8850
9185
  if (startupTimeoutMs !== undefined && (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs <= 0)) {
8851
9186
  throw new Error("Invalid appServer.startupTimeoutMs. Expected a positive integer.");
@@ -8858,6 +9193,9 @@ class LettaAgentClientBase {
8858
9193
  if (options.requestTimeoutMs !== undefined && (!Number.isInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
8859
9194
  throw new Error("Invalid requestTimeoutMs. Expected a positive integer.");
8860
9195
  }
9196
+ if (options.idleLingerMs !== undefined && (!Number.isInteger(options.idleLingerMs) || options.idleLingerMs < 0)) {
9197
+ throw new Error("Invalid idleLingerMs. Expected a non-negative integer.");
9198
+ }
8861
9199
  }
8862
9200
  if (this.backend === "cloud") {
8863
9201
  validateCloudClientOptions(options);
@@ -8872,7 +9210,7 @@ class LettaAgentClientBase {
8872
9210
  }
8873
9211
  validateCreateAgentOptions(options);
8874
9212
  if (this.backend === "remote") {
8875
- const session = new AppServerSession(this.remoteOptions(), {
9213
+ const session = new AppServerSession(this.appServerSessionOptions(), {
8876
9214
  kind: "create-agent",
8877
9215
  options
8878
9216
  });
@@ -8895,7 +9233,7 @@ class LettaAgentClientBase {
8895
9233
  if (!agentId) {
8896
9234
  throw new Error("App-server createSession() requires an agent id. Call createAgent() first or pass an agent id.");
8897
9235
  }
8898
- return new AppServerSession(this.remoteOptions(), {
9236
+ return new AppServerSession(this.appServerSessionOptions(), {
8899
9237
  kind: "session",
8900
9238
  agentId,
8901
9239
  newConversation: true,
@@ -8921,13 +9259,13 @@ class LettaAgentClientBase {
8921
9259
  validateCreateSessionOptions(sessionOptions);
8922
9260
  if (this.backend === "remote") {
8923
9261
  if (looksLikeConversationId(id)) {
8924
- return new AppServerSession(this.remoteOptions(), {
9262
+ return new AppServerSession(this.appServerSessionOptions(), {
8925
9263
  kind: "session",
8926
9264
  conversationId: id,
8927
9265
  options
8928
9266
  });
8929
9267
  }
8930
- return new AppServerSession(this.remoteOptions(), {
9268
+ return new AppServerSession(this.appServerSessionOptions(), {
8931
9269
  kind: "session",
8932
9270
  agentId: id,
8933
9271
  defaultConversation: true,
@@ -9026,6 +9364,12 @@ class LettaAgentClientBase {
9026
9364
  }
9027
9365
  return this.options;
9028
9366
  }
9367
+ appServerSessionOptions() {
9368
+ return {
9369
+ ...this.remoteOptions(),
9370
+ beforeConnect: () => this.releaseIdleManagementConnection()
9371
+ };
9372
+ }
9029
9373
  getRepositoriesClient() {
9030
9374
  if (this.backend !== "cloud") {
9031
9375
  throw new Error('client.repositories is only available with backend: "cloud".');
@@ -9033,6 +9377,9 @@ class LettaAgentClientBase {
9033
9377
  this.repositoriesClient ??= new RepositoriesClient(this.cloudOptions());
9034
9378
  return this.repositoriesClient;
9035
9379
  }
9380
+ async releaseIdleManagementConnection() {
9381
+ await this.managementTransport?.releaseIdleConnection?.();
9382
+ }
9036
9383
  getManagementTransport() {
9037
9384
  if (this.managementTransport)
9038
9385
  return this.managementTransport;
@@ -9140,4 +9487,4 @@ export {
9140
9487
  CloudManagedSandboxExpiredError
9141
9488
  };
9142
9489
 
9143
- //# debugId=071CEF1290505F1D64756E2164756E21
9490
+ //# debugId=0186899B952456A764756E2164756E21