@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
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ // Generated bundle. Canonical modules are published under src/ and maintained in the letta-agent-sdk repository.
1
2
  import { createRequire } from "node:module";
2
3
  var __create = Object.create;
3
4
  var __getProtoOf = Object.getPrototypeOf;
@@ -701,7 +702,56 @@ function createAppServerClient(options) {
701
702
  return new AppServerClient(options);
702
703
  }
703
704
 
705
+ // src/request-ids.ts
706
+ var processRequestCounter = 0;
707
+ function createRequestIdGenerator() {
708
+ const nonce = Math.random().toString(36).slice(2, 10);
709
+ return (prefix = "req") => `${prefix}-${nonce}-${++processRequestCounter}`;
710
+ }
711
+ function applyUniqueRequestIds(client) {
712
+ client.nextRequestId = createRequestIdGenerator();
713
+ return client;
714
+ }
715
+
704
716
  // src/app-server-management.ts
717
+ var DEFAULT_IDLE_LINGER_MS = 250;
718
+ var registeredTransportsByUrl = new Map;
719
+ function appServerUrlKey(value) {
720
+ try {
721
+ const url = new URL(value);
722
+ if (url.protocol === "http:")
723
+ url.protocol = "ws:";
724
+ if (url.protocol === "https:")
725
+ url.protocol = "wss:";
726
+ url.hash = "";
727
+ url.searchParams.delete("channel");
728
+ return url.toString();
729
+ } catch {
730
+ return value;
731
+ }
732
+ }
733
+ function registerManagementTransport(url, transport) {
734
+ const key = appServerUrlKey(url);
735
+ const registration = { transport };
736
+ const transports = registeredTransportsByUrl.get(key) ?? new Set;
737
+ transports.add(registration);
738
+ registeredTransportsByUrl.set(key, transports);
739
+ return () => {
740
+ transports.delete(registration);
741
+ if (transports.size === 0) {
742
+ registeredTransportsByUrl.delete(key);
743
+ }
744
+ };
745
+ }
746
+ async function releaseAppServerManagementConnections(url) {
747
+ const key = appServerUrlKey(url);
748
+ while (true) {
749
+ const transports = [...registeredTransportsByUrl.get(key) ?? []];
750
+ if (transports.length === 0)
751
+ return;
752
+ await Promise.all(transports.map(({ transport }) => transport.releaseIdleConnection()));
753
+ }
754
+ }
705
755
  function ensureResponse(response, value, fallback) {
706
756
  if (!response.success || value == null) {
707
757
  throw new Error(response.error ?? fallback);
@@ -737,8 +787,16 @@ function stringRecord(value) {
737
787
 
738
788
  class AppServerManagementTransport {
739
789
  options;
790
+ connectionPromise = null;
791
+ releasePromise = null;
792
+ closingConnections = new Set;
793
+ inFlightRequests = 0;
794
+ idleWaiters = new Set;
795
+ idleTimer = null;
796
+ idleLingerMs;
740
797
  constructor(options) {
741
798
  this.options = options;
799
+ this.idleLingerMs = options.idleLingerMs ?? DEFAULT_IDLE_LINGER_MS;
742
800
  }
743
801
  async listAgents(query) {
744
802
  const response = await this.request("agent_list", { query }, "agent_list_response");
@@ -805,38 +863,206 @@ class AppServerManagementTransport {
805
863
  }
806
864
  return { messages: response.messages };
807
865
  }
866
+ releaseIdleConnection() {
867
+ this.clearIdleTimer();
868
+ if (this.releasePromise)
869
+ return this.releasePromise;
870
+ const release = this.releaseConnectionWhenIdle();
871
+ this.releasePromise = release;
872
+ release.then(() => {
873
+ if (this.releasePromise === release) {
874
+ this.releasePromise = null;
875
+ }
876
+ }, () => {
877
+ if (this.releasePromise === release) {
878
+ this.releasePromise = null;
879
+ }
880
+ });
881
+ return release;
882
+ }
808
883
  async request(type, body, responseType) {
809
- const ownedConnection = this.options.url ? null : await this.options.connect?.();
884
+ if (this.releasePromise) {
885
+ await this.releasePromise;
886
+ }
887
+ if (this.closingConnections.size > 0) {
888
+ await Promise.all([...this.closingConnections]);
889
+ }
890
+ this.clearIdleTimer();
891
+ this.inFlightRequests += 1;
892
+ try {
893
+ const { client } = await this.acquireConnection();
894
+ const command = {
895
+ type,
896
+ request_id: client.nextRequestId(type),
897
+ ...body
898
+ };
899
+ const response = await client.request(command, {
900
+ predicate: (message) => message.type === responseType
901
+ });
902
+ return response;
903
+ } finally {
904
+ this.inFlightRequests -= 1;
905
+ if (this.inFlightRequests === 0) {
906
+ const waiters = [...this.idleWaiters];
907
+ this.idleWaiters.clear();
908
+ for (const resolve of waiters)
909
+ resolve();
910
+ if (!this.releasePromise) {
911
+ this.scheduleIdleRelease();
912
+ }
913
+ }
914
+ }
915
+ }
916
+ async releaseConnectionWhenIdle() {
917
+ if (this.inFlightRequests > 0) {
918
+ await new Promise((resolve) => {
919
+ this.idleWaiters.add(resolve);
920
+ });
921
+ }
922
+ this.clearIdleTimer();
923
+ const promise = this.connectionPromise;
924
+ try {
925
+ if (promise) {
926
+ this.connectionPromise = null;
927
+ await this.trackClosingConnection(await promise);
928
+ }
929
+ if (this.closingConnections.size > 0) {
930
+ await Promise.all([...this.closingConnections]);
931
+ }
932
+ } catch {}
933
+ }
934
+ acquireConnection() {
935
+ if (this.connectionPromise)
936
+ return this.connectionPromise;
937
+ const promise = this.openConnection().then((connection) => {
938
+ connection.detachDisconnect = connection.client.onDisconnect(() => {
939
+ this.discardConnection(promise, connection);
940
+ });
941
+ return connection;
942
+ }, (error) => {
943
+ if (this.connectionPromise === promise) {
944
+ this.connectionPromise = null;
945
+ }
946
+ throw error;
947
+ });
948
+ this.connectionPromise = promise;
949
+ return promise;
950
+ }
951
+ async openConnection() {
952
+ const ownedConnection = this.options.url ? null : await this.options.connect?.() ?? null;
810
953
  const url = this.options.url ?? ownedConnection?.url;
811
954
  if (!url) {
812
955
  throw new Error("App-server management requires a url or connect hook.");
813
956
  }
957
+ const unregister = registerManagementTransport(url, this);
814
958
  let client = null;
815
959
  try {
816
- client = createAppServerClient({
960
+ client = applyUniqueRequestIds(createAppServerClient({
817
961
  url,
818
962
  ...this.options.authToken !== undefined ? { authToken: this.options.authToken } : {},
819
963
  ...this.options.WebSocket ? {
820
964
  WebSocket: this.options.WebSocket
821
965
  } : {},
822
966
  ...this.options.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.options.requestTimeoutMs } : {}
823
- });
967
+ }));
824
968
  await client.connect();
825
- const command = {
826
- type,
827
- request_id: client.nextRequestId(type),
828
- ...body
829
- };
830
- const response = await client.request(command, {
831
- predicate: (message) => message.type === responseType
832
- });
833
- return response;
834
- } finally {
835
- client?.close();
836
- ownedConnection?.close();
969
+ } catch (error) {
970
+ try {
971
+ if (client) {
972
+ const controlClosed = waitForSocketClose(client.control);
973
+ client.close();
974
+ ownedConnection?.close();
975
+ await controlClosed;
976
+ } else {
977
+ ownedConnection?.close();
978
+ }
979
+ } catch {} finally {
980
+ unregister();
981
+ }
982
+ throw error;
837
983
  }
984
+ return {
985
+ client,
986
+ ownedConnection,
987
+ detachDisconnect: () => {},
988
+ unregister
989
+ };
990
+ }
991
+ discardConnection(promise, connection) {
992
+ if (this.connectionPromise === promise) {
993
+ this.connectionPromise = null;
994
+ this.clearIdleTimer();
995
+ }
996
+ this.trackClosingConnection(connection);
997
+ }
998
+ trackClosingConnection(connection) {
999
+ const closing = closeConnection(connection);
1000
+ this.closingConnections.add(closing);
1001
+ closing.then(() => this.closingConnections.delete(closing), () => this.closingConnections.delete(closing));
1002
+ return closing;
1003
+ }
1004
+ scheduleIdleRelease() {
1005
+ if (!this.connectionPromise)
1006
+ return;
1007
+ this.clearIdleTimer();
1008
+ const timer = setTimeout(() => {
1009
+ this.idleTimer = null;
1010
+ this.releaseIdleConnection();
1011
+ }, this.idleLingerMs);
1012
+ this.idleTimer = timer;
1013
+ timer.unref?.();
1014
+ }
1015
+ clearIdleTimer() {
1016
+ if (this.idleTimer === null)
1017
+ return;
1018
+ clearTimeout(this.idleTimer);
1019
+ this.idleTimer = null;
1020
+ }
1021
+ }
1022
+ async function closeConnection(connection) {
1023
+ connection.detachDisconnect();
1024
+ const controlClosed = waitForSocketClose(connection.client.control);
1025
+ try {
1026
+ connection.client.close();
1027
+ connection.ownedConnection?.close();
1028
+ await controlClosed;
1029
+ } finally {
1030
+ connection.unregister();
838
1031
  }
839
1032
  }
1033
+ function waitForSocketClose(socket) {
1034
+ if (socket.readyState === 3)
1035
+ return Promise.resolve();
1036
+ return new Promise((resolve) => {
1037
+ let settled = false;
1038
+ let detach = () => {};
1039
+ const finish = () => {
1040
+ if (settled)
1041
+ return;
1042
+ settled = true;
1043
+ clearTimeout(timeout);
1044
+ detach();
1045
+ resolve();
1046
+ };
1047
+ if (socket.addEventListener) {
1048
+ socket.addEventListener("close", finish);
1049
+ detach = () => socket.removeEventListener?.("close", finish);
1050
+ } else if (socket.once) {
1051
+ socket.once("close", finish);
1052
+ detach = () => socket.off?.("close", finish);
1053
+ } else if (socket.on) {
1054
+ socket.on("close", finish);
1055
+ detach = () => socket.off?.("close", finish);
1056
+ } else {
1057
+ resolve();
1058
+ return;
1059
+ }
1060
+ const timeout = setTimeout(finish, 1000);
1061
+ timeout.unref?.();
1062
+ if (socket.readyState === 3)
1063
+ finish();
1064
+ });
1065
+ }
840
1066
 
841
1067
  // node_modules/@letta-ai/letta-code/dist/agent-presets.js
842
1068
  var LETTA_CODE_ORIGIN_TAG = "origin:letta-code";
@@ -5505,7 +5731,7 @@ function buildCanUseToolContext(request, requestId) {
5505
5731
  return context;
5506
5732
  }
5507
5733
 
5508
- // src/remote-client-session-core.ts
5734
+ // src/remote-session-protocol.ts
5509
5735
  var FAILURE_STOP_REASONS = new Set([
5510
5736
  "error",
5511
5737
  "llm_api_error",
@@ -5900,856 +6126,930 @@ function normalizeSendMessage(message) {
5900
6126
  return message;
5901
6127
  }
5902
6128
 
5903
- class RemoteClientSessionCore {
5904
- mode;
5905
- controller = null;
5906
- runtime = null;
5907
- initialized = false;
5908
- closed = false;
5909
- activeTurnStartedAt = 0;
5910
- _agentId = null;
5911
- _sessionId = null;
5912
- _conversationId = null;
5913
- _model = "";
5914
- _modelSettings = null;
6129
+ // src/remote-turn-coordinator.ts
6130
+ class RemoteTurnCoordinator {
5915
6131
  label;
5916
6132
  requestTimeoutMs;
6133
+ onDeviceStatus;
5917
6134
  streamQueue = [];
5918
6135
  streamResolvers = [];
5919
- removeMessageHandler = null;
5920
6136
  activeTurn = null;
5921
6137
  pendingTurns = [];
5922
6138
  nextTurnId = 0;
5923
6139
  messageCounter = 0;
5924
6140
  clientMessageCounter = 0;
5925
- toolNames;
5926
- deviceStatusListeners = new Set;
5927
- deviceStatusRefreshCancels = new Set;
5928
- constructor(mode, config) {
5929
- this.mode = mode;
6141
+ closed = false;
6142
+ _activeTurnStartedAt = 0;
6143
+ constructor(config) {
5930
6144
  this.label = config.label;
5931
6145
  this.requestTimeoutMs = config.requestTimeoutMs;
6146
+ this.onDeviceStatus = config.onDeviceStatus;
5932
6147
  }
5933
- async initialize() {
5934
- if (this.initialized) {
5935
- throw new Error("Session already initialized");
5936
- }
5937
- if (this.closed) {
5938
- throw new Error("Session is closed");
5939
- }
5940
- try {
5941
- const init = await this.initializeRuntimeController();
5942
- this.controller = init.controller;
5943
- this.runtime = init.runtime;
5944
- this._agentId = init.runtime.agent_id;
5945
- this._conversationId = init.runtime.conversation_id;
5946
- this._sessionId = `${init.runtime.agent_id}:${init.runtime.conversation_id}`;
5947
- this._modelSettings = init.modelSettings ?? null;
5948
- this._model = typeof init.model === "string" ? init.model : typeof this._modelSettings?.model === "string" ? this._modelSettings.model : "";
5949
- this.toolNames = init.tools;
5950
- this.removeMessageHandler = this.controller.onMessage(this.handleProtocolMessage);
5951
- this.initialized = true;
5952
- await this.afterRuntimeInitialized();
5953
- await this.applyPostInitializeOptions();
5954
- const initMessage = {
5955
- type: "init",
5956
- agentId: init.runtime.agent_id,
5957
- sessionId: this._sessionId,
5958
- conversationId: init.runtime.conversation_id,
5959
- model: this._model
5960
- };
5961
- if (this.toolNames !== undefined)
5962
- initMessage.tools = this.toolNames;
5963
- if (init.skillSources !== undefined) {
5964
- initMessage.skillSources = init.skillSources;
5965
- }
5966
- return initMessage;
5967
- } catch (error) {
5968
- this.close();
5969
- throw error;
5970
- }
6148
+ get activeTurnStartedAt() {
6149
+ return this._activeTurnStartedAt;
5971
6150
  }
5972
- async send(message) {
5973
- if (!this.initialized) {
5974
- await this.initialize();
5975
- }
5976
- if (!this.controller || !this.runtime) {
5977
- throw new Error("Session is not initialized");
5978
- }
5979
- await this.beforeTurn();
5980
- const turn = this.trackSentTurn(this.runtime);
5981
- try {
5982
- this.controller.sendTurnMessage(this.runtime, message, {
5983
- clientMessageId: turn.clientMessageId
5984
- });
5985
- } catch (error) {
5986
- this.removeTrackedTurn(turn);
5987
- throw error;
5988
- }
6151
+ hasInFlightTurn() {
6152
+ return this.activeTurn !== null || this.pendingTurns.length > 0;
5989
6153
  }
5990
- async runTurn(message, _options = {}) {
5991
- if (this.activeTurn || this.pendingTurns.length > 0) {
5992
- throw new Error(`A turn is already in flight for this ${this.label} session. Use send() and stream() to let the listener queue messages.`);
5993
- }
5994
- await this.send(message);
5995
- for await (const msg of this.stream()) {
5996
- if (msg.type === "result") {
5997
- return msg;
5998
- }
5999
- }
6000
- return {
6001
- type: "result",
6002
- success: false,
6003
- error: "stream_closed",
6004
- errorCode: "stream_closed",
6005
- recoverable: false,
6006
- errorDetail: "Stream ended before terminal result",
6007
- durationMs: Date.now() - this.activeTurnStartedAt,
6008
- conversationId: this._conversationId
6154
+ trackSentTurn(runtime) {
6155
+ const turn = {
6156
+ id: ++this.nextTurnId,
6157
+ runtime,
6158
+ clientMessageId: `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
6159
+ queuedAt: Date.now(),
6160
+ startedAt: 0,
6161
+ assistantText: "",
6162
+ runIds: new Set,
6163
+ observedTurnEvidence: false,
6164
+ observedRequiresApprovalStop: false,
6165
+ abortRequested: false,
6166
+ timeout: null
6009
6167
  };
6010
- }
6011
- async* stream() {
6012
- while (true) {
6013
- const msg = await this.nextMessage();
6014
- if (!msg)
6015
- break;
6016
- yield msg;
6017
- if (msg.type === "result")
6018
- break;
6168
+ if (this.activeTurn) {
6169
+ this.pendingTurns.push(turn);
6170
+ } else {
6171
+ this.activateTurn(turn);
6019
6172
  }
6173
+ return turn;
6020
6174
  }
6021
- async abort() {
6022
- if (!this.initialized)
6023
- return;
6024
- if (!this.controller || !this.runtime)
6175
+ removeTrackedTurn(turn) {
6176
+ if (turn.timeout)
6177
+ clearTimeout(turn.timeout);
6178
+ if (this.activeTurn === turn) {
6179
+ this.activeTurn = null;
6025
6180
  return;
6181
+ }
6182
+ const index = this.pendingTurns.indexOf(turn);
6183
+ if (index !== -1)
6184
+ this.pendingTurns.splice(index, 1);
6185
+ }
6186
+ markAbortRequested() {
6026
6187
  if (this.activeTurn)
6027
6188
  this.activeTurn.abortRequested = true;
6028
- await this.controller.abort(this.runtime);
6029
6189
  }
6030
- async sendCommand(command, options) {
6031
- if (!this.initialized) {
6032
- await this.initialize();
6033
- }
6034
- if (!this.controller) {
6035
- throw new Error("Session is not initialized");
6190
+ handleProtocolMessage(message, runtime) {
6191
+ if (!sameRuntime2(message, runtime))
6192
+ return;
6193
+ const statusRecord = deviceStatusRecord(message);
6194
+ if (statusRecord) {
6195
+ const status = toSessionDeviceStatus(statusRecord);
6196
+ if (status)
6197
+ this.onDeviceStatus(status);
6198
+ return;
6036
6199
  }
6037
- if (!command || typeof command !== "object" || Array.isArray(command)) {
6038
- throw new Error("Invalid command. Expected a protocol command object.");
6200
+ if (message.type === "update_queue") {
6201
+ const sdkMessage2 = {
6202
+ type: "queue_update",
6203
+ queue: queueItems(message)
6204
+ };
6205
+ this.enqueue(sdkMessage2);
6206
+ return;
6039
6207
  }
6040
- if (typeof command.type !== "string" || command.type.length === 0) {
6041
- throw new Error("Invalid command. Expected a non-empty type.");
6208
+ if (message.type === "update_loop_status") {
6209
+ this.handleLoopStatusMessage(message);
6210
+ return;
6042
6211
  }
6043
- if (!options || !options.responseType && !options.predicate && options.timeoutMs === undefined) {
6044
- this.controller.send(command);
6212
+ const delta = streamDeltaRecord(message);
6213
+ if (!delta)
6045
6214
  return;
6215
+ const active = this.activateNextTurnFromProtocol();
6216
+ if (active) {
6217
+ active.observedTurnEvidence = true;
6218
+ const runId = streamDeltaRunId2(delta);
6219
+ if (runId)
6220
+ active.runIds.add(runId);
6046
6221
  }
6047
- const { type, ...body } = command;
6048
- const response = await this.controller.request(type, body, {
6049
- ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {},
6050
- predicate: options.predicate ? (message) => options.predicate?.(message) === true : options.responseType ? (message) => message.type === options.responseType : undefined
6051
- });
6052
- return response;
6222
+ const sdkMessage = this.transformStreamDelta(delta);
6223
+ if (sdkMessage)
6224
+ this.enqueue(sdkMessage);
6225
+ this.handleTurnTerminalDelta(delta, sdkMessage);
6053
6226
  }
6054
- async listModels() {
6055
- if (!this.initialized) {
6056
- await this.initialize();
6057
- }
6058
- if (!this.controller) {
6059
- throw new Error("Session is not initialized");
6060
- }
6061
- return this.controller.listModels();
6227
+ nextMessage() {
6228
+ const next = this.streamQueue.shift();
6229
+ if (next)
6230
+ return Promise.resolve(next);
6231
+ if (this.closed)
6232
+ return Promise.resolve(null);
6233
+ return new Promise((resolve) => {
6234
+ this.streamResolvers.push(resolve);
6235
+ });
6062
6236
  }
6063
- async updateModel(update) {
6064
- if (!this.initialized) {
6065
- await this.initialize();
6237
+ close() {
6238
+ if (this.closed)
6239
+ return;
6240
+ this.closed = true;
6241
+ if (this.activeTurn?.timeout)
6242
+ clearTimeout(this.activeTurn.timeout);
6243
+ for (const turn of this.pendingTurns) {
6244
+ if (turn.timeout)
6245
+ clearTimeout(turn.timeout);
6246
+ }
6247
+ this.activeTurn = null;
6248
+ this.pendingTurns.length = 0;
6249
+ this.resolveAll(null);
6250
+ }
6251
+ activateTurn(turn) {
6252
+ this.activeTurn = turn;
6253
+ turn.startedAt = Date.now();
6254
+ this._activeTurnStartedAt = turn.startedAt;
6255
+ if (this.requestTimeoutMs !== undefined) {
6256
+ turn.timeout = setTimeout(() => {
6257
+ this.failTurn(turn, `Timed out waiting for ${this.label} turn`);
6258
+ }, this.requestTimeoutMs);
6259
+ turn.timeout.unref?.();
6260
+ }
6261
+ }
6262
+ activateNextTurnFromProtocol() {
6263
+ if (this.activeTurn)
6264
+ return this.activeTurn;
6265
+ const next = this.pendingTurns.shift();
6266
+ if (!next)
6267
+ return null;
6268
+ this.activateTurn(next);
6269
+ return next;
6270
+ }
6271
+ failTurn(turn, detail) {
6272
+ if (this.activeTurn !== turn)
6273
+ return;
6274
+ this.enqueue({
6275
+ type: "error",
6276
+ message: detail,
6277
+ errorCode: "error",
6278
+ stopReason: "error",
6279
+ errorDetail: detail,
6280
+ recoverable: false
6281
+ });
6282
+ this.completeActiveTurn({
6283
+ runtime: turn.runtime,
6284
+ stopReason: "error",
6285
+ runIds: [...turn.runIds],
6286
+ success: false,
6287
+ detail,
6288
+ errorCode: "error"
6289
+ });
6290
+ }
6291
+ completeActiveTurn(turn) {
6292
+ const active = this.activeTurn;
6293
+ if (!active)
6294
+ return;
6295
+ if (active.timeout) {
6296
+ clearTimeout(active.timeout);
6297
+ active.timeout = null;
6298
+ }
6299
+ this.enqueue(this.resultFromTurn(turn, active));
6300
+ this.activeTurn = null;
6301
+ }
6302
+ handleLoopStatusMessage(message) {
6303
+ const status = loopStatusValue(message);
6304
+ if (!status)
6305
+ return;
6306
+ const activeRunIds = loopStatusRunIds(message);
6307
+ const sdkMessage = {
6308
+ type: "loop_status",
6309
+ status,
6310
+ activeRunIds
6311
+ };
6312
+ this.enqueue(sdkMessage);
6313
+ const active = this.activeTurn;
6314
+ if (!active)
6315
+ return;
6316
+ for (const runId of activeRunIds)
6317
+ active.runIds.add(runId);
6318
+ const hadTurnEvidence = active.observedTurnEvidence || active.observedRequiresApprovalStop;
6319
+ if (!hadTurnEvidence)
6320
+ return;
6321
+ if (status === "WAITING_ON_APPROVAL") {
6322
+ this.completeActiveTurn({
6323
+ runtime: active.runtime,
6324
+ stopReason: "requires_approval",
6325
+ runIds: [...active.runIds]
6326
+ });
6327
+ return;
6328
+ }
6329
+ if (status === "WAITING_ON_INPUT" && active.abortRequested) {
6330
+ this.completeActiveTurn({
6331
+ runtime: active.runtime,
6332
+ stopReason: "interrupted",
6333
+ runIds: [...active.runIds],
6334
+ success: false,
6335
+ detail: "Interrupted",
6336
+ errorCode: "interrupted"
6337
+ });
6338
+ return;
6339
+ }
6340
+ if (status === "WAITING_ON_INPUT" && active.observedTurnEvidence) {
6341
+ this.completeActiveTurn({
6342
+ runtime: active.runtime,
6343
+ stopReason: null,
6344
+ runIds: [...active.runIds]
6345
+ });
6346
+ }
6347
+ }
6348
+ handleTurnTerminalDelta(delta, sdkMessage) {
6349
+ const active = this.activeTurn;
6350
+ if (!active)
6351
+ return;
6352
+ const messageType = streamDeltaMessageType2(delta);
6353
+ if (messageType === "stop_reason") {
6354
+ const stopReason = streamDeltaStopReason2(delta) ?? null;
6355
+ if (stopReason === "requires_approval") {
6356
+ active.observedRequiresApprovalStop = true;
6357
+ return;
6358
+ }
6359
+ this.completeActiveTurn({
6360
+ runtime: active.runtime,
6361
+ stopReason,
6362
+ runIds: [...active.runIds]
6363
+ });
6364
+ return;
6365
+ }
6366
+ if (sdkMessage?.type === "error") {
6367
+ this.completeActiveTurn({
6368
+ runtime: active.runtime,
6369
+ stopReason: sdkMessage.stopReason,
6370
+ runIds: [...active.runIds],
6371
+ success: false,
6372
+ detail: sdkMessage.errorDetail ?? sdkMessage.message,
6373
+ errorCode: sdkMessage.errorCode
6374
+ });
6375
+ }
6376
+ }
6377
+ transformStreamDelta(delta) {
6378
+ const messageType = typeof delta.message_type === "string" ? delta.message_type : undefined;
6379
+ const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
6380
+ const uuid = typeof delta.id === "string" ? delta.id : `${this.label}-${++this.messageCounter}`;
6381
+ if (messageType === "assistant_message") {
6382
+ const content = extractTextFromContent(delta.content);
6383
+ if (!content)
6384
+ return null;
6385
+ if (this.activeTurn)
6386
+ this.activeTurn.assistantText += content;
6387
+ return { type: "assistant", content, uuid, runId };
6388
+ }
6389
+ if (messageType === "reasoning_message") {
6390
+ const content = typeof delta.reasoning === "string" ? delta.reasoning : extractTextFromContent(delta.content);
6391
+ if (!content)
6392
+ return null;
6393
+ return { type: "reasoning", content, uuid, runId };
6394
+ }
6395
+ if (messageType === "tool_call_message" || messageType === "approval_request_message") {
6396
+ const toolCall = firstToolCall(delta);
6397
+ if (!toolCall)
6398
+ return null;
6399
+ const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
6400
+ const toolCallId = (typeof toolCall.tool_call_id === "string" ? toolCall.tool_call_id : undefined) ?? (typeof toolCall.id === "string" ? toolCall.id : undefined);
6401
+ if (!toolCallId) {
6402
+ const detail = `Missing tool_call_id in ${messageType} (uuid=${uuid})`;
6403
+ return {
6404
+ type: "error",
6405
+ message: detail,
6406
+ errorCode: "protocol_error",
6407
+ stopReason: "protocol_error",
6408
+ runId,
6409
+ recoverable: false,
6410
+ errorDetail: detail
6411
+ };
6412
+ }
6413
+ const toolName = (typeof toolCall.name === "string" ? toolCall.name : undefined) ?? (typeof fn?.name === "string" ? fn.name : undefined) ?? "?";
6414
+ const { input, raw } = toolInputFromArguments(toolCall.arguments ?? fn?.arguments);
6415
+ return {
6416
+ type: "tool_call",
6417
+ toolCallId,
6418
+ toolName,
6419
+ toolInput: input,
6420
+ rawArguments: raw,
6421
+ uuid,
6422
+ runId
6423
+ };
6424
+ }
6425
+ if (messageType === "tool_return_message") {
6426
+ const toolReturn = firstToolReturn(delta) ?? delta;
6427
+ const toolCallId = (typeof delta.tool_call_id === "string" ? delta.tool_call_id : undefined) ?? (typeof toolReturn.tool_call_id === "string" ? toolReturn.tool_call_id : undefined);
6428
+ if (!toolCallId)
6429
+ return null;
6430
+ const content = extractTextFromContent(delta.tool_return ?? toolReturn.tool_return ?? toolReturn.content) ?? "";
6431
+ const status = typeof delta.status === "string" ? delta.status : toolReturn.status;
6432
+ return {
6433
+ type: "tool_result",
6434
+ toolCallId,
6435
+ content,
6436
+ isError: status === "error",
6437
+ uuid,
6438
+ runId
6439
+ };
6440
+ }
6441
+ if (messageType === "error_message" || messageType === "loop_error") {
6442
+ const detail = (typeof delta.detail === "string" ? delta.detail : undefined) ?? (typeof delta.message === "string" ? delta.message : undefined) ?? `${this.label} turn failed`;
6443
+ const stopReason = (typeof delta.stop_reason === "string" ? delta.stop_reason : undefined) ?? (typeof delta.error_type === "string" ? delta.error_type : undefined) ?? "error";
6444
+ const approvalConflict = isApprovalConflictSignal({
6445
+ detail,
6446
+ message: typeof delta.message === "string" ? delta.message : undefined,
6447
+ stopReason
6448
+ });
6449
+ return {
6450
+ type: "error",
6451
+ message: detail,
6452
+ errorCode: approvalConflict ? "approval_conflict" : toSdkErrorCode(stopReason),
6453
+ approvalConflict: approvalConflict || undefined,
6454
+ recoverable: approvalConflict ? true : false,
6455
+ errorDetail: detail,
6456
+ stopReason,
6457
+ runId
6458
+ };
6459
+ }
6460
+ if (messageType === "retry") {
6461
+ return {
6462
+ type: "retry",
6463
+ reason: typeof delta.reason === "string" ? delta.reason : "error",
6464
+ attempt: typeof delta.attempt === "number" ? delta.attempt : 0,
6465
+ maxAttempts: typeof delta.max_attempts === "number" ? delta.max_attempts : 0,
6466
+ delayMs: typeof delta.delay_ms === "number" ? delta.delay_ms : 0,
6467
+ runId
6468
+ };
6469
+ }
6470
+ if (messageType === "stop_reason" || messageType === "ping") {
6471
+ return null;
6472
+ }
6473
+ return {
6474
+ type: "stream_event",
6475
+ event: delta,
6476
+ uuid
6477
+ };
6478
+ }
6479
+ resultFromTurn(turn, tracker) {
6480
+ const stopReason = turn.stopReason ?? (turn.success === false ? "error" : undefined);
6481
+ const approvalConflict = isApprovalConflictSignal({
6482
+ detail: turn.detail,
6483
+ stopReason
6484
+ });
6485
+ const success = turn.success !== undefined ? turn.success && !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "") : !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "");
6486
+ const errorCode = approvalConflict ? "approval_conflict" : turn.errorCode ?? toSdkErrorCode(stopReason);
6487
+ return {
6488
+ type: "result",
6489
+ success,
6490
+ result: success ? tracker?.assistantText || undefined : undefined,
6491
+ error: success ? undefined : errorCode ?? stopReason ?? "error",
6492
+ errorCode: success ? undefined : errorCode ?? "error",
6493
+ approvalConflict: approvalConflict || undefined,
6494
+ recoverable: approvalConflict ? true : success ? undefined : false,
6495
+ errorDetail: success ? undefined : turn.detail,
6496
+ stopReason,
6497
+ durationMs: Date.now() - (tracker?.startedAt || this._activeTurnStartedAt),
6498
+ conversationId: turn.runtime.conversation_id,
6499
+ runIds: turn.runIds.length > 0 ? turn.runIds : undefined
6500
+ };
6501
+ }
6502
+ enqueue(message) {
6503
+ const resolver = this.streamResolvers.shift();
6504
+ if (resolver) {
6505
+ resolver(message);
6506
+ return;
6507
+ }
6508
+ this.streamQueue.push(message);
6509
+ }
6510
+ resolveAll(value) {
6511
+ for (const resolve of this.streamResolvers.splice(0)) {
6512
+ resolve(value);
6513
+ }
6514
+ }
6515
+ }
6516
+ // src/remote-client-session-core.ts
6517
+ class RemoteClientSessionCore {
6518
+ mode;
6519
+ controller = null;
6520
+ runtime = null;
6521
+ initialized = false;
6522
+ closed = false;
6523
+ _agentId = null;
6524
+ _sessionId = null;
6525
+ _conversationId = null;
6526
+ _model = "";
6527
+ _modelSettings = null;
6528
+ label;
6529
+ requestTimeoutMs;
6530
+ initializePromise = null;
6531
+ removeMessageHandler = null;
6532
+ turns;
6533
+ toolNames;
6534
+ deviceStatusListeners = new Set;
6535
+ deviceStatusRefreshCancels = new Set;
6536
+ constructor(mode, config) {
6537
+ this.mode = mode;
6538
+ this.label = config.label;
6539
+ this.requestTimeoutMs = config.requestTimeoutMs;
6540
+ this.turns = new RemoteTurnCoordinator({
6541
+ label: config.label,
6542
+ requestTimeoutMs: config.requestTimeoutMs,
6543
+ onDeviceStatus: (status) => this.emitDeviceStatus(status)
6544
+ });
6545
+ }
6546
+ async initialize() {
6547
+ if (this.closed) {
6548
+ throw new Error("Session is closed");
6066
6549
  }
6067
- if (!this.controller || !this.runtime) {
6068
- throw new Error("Session is not initialized");
6550
+ if (this.initializePromise) {
6551
+ return this.initializePromise;
6069
6552
  }
6070
- const normalized = normalizeUpdateModelInput(update);
6071
- const payload = await this.resolveUpdateModelPayload(normalized);
6072
- const result = await this.controller.updateModel(this.runtime, payload);
6073
- if (result.modelHandle !== undefined) {
6074
- this._model = result.modelHandle;
6075
- } else if (payload.model_handle !== undefined) {
6076
- this._model = payload.model_handle;
6077
- } else if (typeof result.modelSettings?.model === "string") {
6078
- this._model = result.modelSettings.model;
6553
+ if (this.initialized) {
6554
+ throw new Error("Session already initialized");
6079
6555
  }
6080
- if ("modelSettings" in result) {
6081
- this._modelSettings = result.modelSettings ?? null;
6556
+ const attempt = this.performInitialize();
6557
+ const memo = attempt.catch((error) => {
6558
+ this.cleanupFailedInitialize();
6559
+ throw error;
6560
+ }).finally(() => {
6561
+ if (this.initializePromise === memo) {
6562
+ this.initializePromise = null;
6563
+ }
6564
+ });
6565
+ this.initializePromise = memo;
6566
+ return memo;
6567
+ }
6568
+ async performInitialize() {
6569
+ const init = await this.initializeRuntimeController();
6570
+ this.controller = init.controller;
6571
+ this.runtime = init.runtime;
6572
+ this._agentId = init.runtime.agent_id;
6573
+ this._conversationId = init.runtime.conversation_id;
6574
+ this._sessionId = `${init.runtime.agent_id}:${init.runtime.conversation_id}`;
6575
+ this._modelSettings = init.modelSettings ?? null;
6576
+ this._model = typeof init.model === "string" ? init.model : typeof this._modelSettings?.model === "string" ? this._modelSettings.model : "";
6577
+ this.toolNames = init.tools;
6578
+ this.removeMessageHandler = this.controller.onMessage((message) => {
6579
+ if (this.runtime)
6580
+ this.turns.handleProtocolMessage(message, this.runtime);
6581
+ });
6582
+ await this.afterRuntimeInitialized();
6583
+ await this.applyPostInitializeOptions();
6584
+ if (this.closed) {
6585
+ throw new Error("Session is closed");
6082
6586
  }
6083
- return result;
6587
+ this.initialized = true;
6588
+ const initMessage = {
6589
+ type: "init",
6590
+ agentId: init.runtime.agent_id,
6591
+ sessionId: this._sessionId,
6592
+ conversationId: init.runtime.conversation_id,
6593
+ model: this._model
6594
+ };
6595
+ if (this.toolNames !== undefined)
6596
+ initMessage.tools = this.toolNames;
6597
+ if (init.skillSources !== undefined) {
6598
+ initMessage.skillSources = init.skillSources;
6599
+ }
6600
+ return initMessage;
6084
6601
  }
6085
- async recoverPendingApprovals(options = {}) {
6602
+ cleanupFailedInitialize() {
6603
+ this.removeMessageHandler?.();
6604
+ this.removeMessageHandler = null;
6605
+ this.controller?.close();
6606
+ this.controller = null;
6607
+ this.onCoreClose();
6608
+ this.runtime = null;
6609
+ this._agentId = null;
6610
+ this._conversationId = null;
6611
+ this._sessionId = null;
6612
+ this._modelSettings = null;
6613
+ this._model = "";
6614
+ this.toolNames = undefined;
6615
+ this.initialized = false;
6616
+ }
6617
+ async send(message) {
6086
6618
  if (!this.initialized) {
6087
6619
  await this.initialize();
6088
6620
  }
6089
6621
  if (!this.controller || !this.runtime) {
6090
6622
  throw new Error("Session is not initialized");
6091
6623
  }
6092
- return this.controller.recoverPendingApprovals(this.runtime, options);
6093
- }
6094
- async removeQueuedMessage(itemId) {
6095
- if (typeof itemId !== "string" || itemId.trim().length === 0) {
6096
- throw new Error("Invalid queue item id. Expected a non-empty string.");
6097
- }
6098
- if (!this.initialized) {
6099
- await this.initialize();
6624
+ await this.beforeTurn();
6625
+ const turn = this.turns.trackSentTurn(this.runtime);
6626
+ try {
6627
+ this.controller.sendTurnMessage(this.runtime, message, {
6628
+ clientMessageId: turn.clientMessageId
6629
+ });
6630
+ } catch (error) {
6631
+ this.turns.removeTrackedTurn(turn);
6632
+ throw error;
6100
6633
  }
6101
- if (!this.controller || !this.runtime) {
6102
- throw new Error("Session is not initialized");
6634
+ }
6635
+ async runTurn(message, _options = {}) {
6636
+ if (this.turns.hasInFlightTurn()) {
6637
+ throw new Error(`A turn is already in flight for this ${this.label} session. Use send() and stream() to let the listener queue messages.`);
6103
6638
  }
6104
- const response = await this.controller.request("remove_queue_item", {
6105
- runtime: this.runtime,
6106
- item_id: itemId
6107
- }, {
6108
- predicate: (message) => message.type === "remove_queue_item_response" && message.item_id === itemId
6109
- });
6110
- if (typeof response.success !== "boolean") {
6111
- throw new Error("Invalid remove_queue_item_response from runtime");
6639
+ await this.send(message);
6640
+ for await (const msg of this.stream()) {
6641
+ if (msg.type === "result") {
6642
+ return msg;
6643
+ }
6112
6644
  }
6113
6645
  return {
6114
- itemId: typeof response.item_id === "string" ? response.item_id : itemId,
6115
- removed: response.success
6646
+ type: "result",
6647
+ success: false,
6648
+ error: "stream_closed",
6649
+ errorCode: "stream_closed",
6650
+ recoverable: false,
6651
+ errorDetail: "Stream ended before terminal result",
6652
+ durationMs: Date.now() - this.turns.activeTurnStartedAt,
6653
+ conversationId: this._conversationId
6116
6654
  };
6117
6655
  }
6118
- async getDeviceStatus(options = {}) {
6656
+ async* stream() {
6657
+ while (true) {
6658
+ const msg = await this.turns.nextMessage();
6659
+ if (!msg)
6660
+ break;
6661
+ yield msg;
6662
+ if (msg.type === "result")
6663
+ break;
6664
+ }
6665
+ }
6666
+ async abort() {
6667
+ if (!this.initialized)
6668
+ return;
6669
+ if (!this.controller || !this.runtime)
6670
+ return;
6671
+ this.turns.markAbortRequested();
6672
+ await this.controller.abort(this.runtime);
6673
+ }
6674
+ async sendCommand(command, options) {
6119
6675
  if (!this.initialized) {
6120
6676
  await this.initialize();
6121
6677
  }
6122
- if (!this.controller || !this.runtime) {
6678
+ if (!this.controller) {
6123
6679
  throw new Error("Session is not initialized");
6124
6680
  }
6125
- const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs ?? 30000;
6126
- if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
6127
- throw new Error("Invalid device status timeout. Expected a positive integer.");
6681
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
6682
+ throw new Error("Invalid command. Expected a protocol command object.");
6128
6683
  }
6129
- return this.refreshDeviceStatus(timeoutMs);
6130
- }
6131
- onDeviceStatus(listener) {
6132
- if (typeof listener !== "function") {
6133
- throw new Error("Invalid device status listener. Expected a function.");
6684
+ if (typeof command.type !== "string" || command.type.length === 0) {
6685
+ throw new Error("Invalid command. Expected a non-empty type.");
6134
6686
  }
6135
- this.deviceStatusListeners.add(listener);
6136
- return () => {
6137
- this.deviceStatusListeners.delete(listener);
6138
- };
6139
- }
6140
- refreshDeviceStatus(timeoutMs) {
6141
- const controller = this.controller;
6142
- const runtime = this.runtime;
6143
- if (!controller || !runtime) {
6144
- return Promise.reject(new Error("Session is not initialized"));
6687
+ if (!options || !options.responseType && !options.predicate && options.timeoutMs === undefined) {
6688
+ this.controller.send(command);
6689
+ return;
6145
6690
  }
6146
- return new Promise((resolve, reject) => {
6147
- let status = null;
6148
- let syncAcknowledged = false;
6149
- let settled = false;
6150
- const cleanup = () => {
6151
- clearTimeout(timer);
6152
- unsubscribe();
6153
- this.deviceStatusRefreshCancels.delete(cancel);
6154
- };
6155
- const rejectOnce = (error) => {
6156
- if (settled)
6157
- return;
6158
- settled = true;
6159
- cleanup();
6160
- reject(error);
6161
- };
6162
- const resolveIfComplete = () => {
6163
- if (settled || !syncAcknowledged || !status)
6164
- return;
6165
- settled = true;
6166
- cleanup();
6167
- resolve(status);
6168
- };
6169
- const cancel = (error) => rejectOnce(error);
6170
- const timer = setTimeout(() => {
6171
- rejectOnce(new Error(`Timed out waiting for ${this.label} device status`));
6172
- }, timeoutMs);
6173
- timer.unref?.();
6174
- const unsubscribe = this.onDeviceStatus((nextStatus) => {
6175
- status = nextStatus;
6176
- resolveIfComplete();
6177
- });
6178
- this.deviceStatusRefreshCancels.add(cancel);
6179
- controller.request("sync", {
6180
- runtime,
6181
- recover_approvals: false,
6182
- force_device_status: true
6183
- }, {
6184
- timeoutMs,
6185
- predicate: (message) => message.type === "sync_response"
6186
- }).then((response) => {
6187
- if (response.success === false) {
6188
- rejectOnce(new Error(typeof response.error === "string" ? response.error : `Failed to refresh ${this.label} device status`));
6189
- return;
6190
- }
6191
- syncAcknowledged = true;
6192
- resolveIfComplete();
6193
- }, (error) => {
6194
- rejectOnce(error instanceof Error ? error : new Error(String(error)));
6195
- });
6691
+ const { type, ...body } = command;
6692
+ const response = await this.controller.request(type, body, {
6693
+ ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {},
6694
+ predicate: options.predicate ? (message) => options.predicate?.(message) === true : options.responseType ? (message) => message.type === options.responseType : undefined
6196
6695
  });
6696
+ return response;
6197
6697
  }
6198
- async listMessages(options = {}) {
6698
+ async listModels() {
6199
6699
  if (!this.initialized) {
6200
6700
  await this.initialize();
6201
6701
  }
6202
6702
  if (!this.controller) {
6203
6703
  throw new Error("Session is not initialized");
6204
6704
  }
6205
- const conversationId = options.conversationId ?? this._conversationId;
6206
- if (!conversationId) {
6207
- throw new Error("No conversation id available for listMessages()");
6208
- }
6209
- return this.controller.listMessages(conversationId, options);
6705
+ return this.controller.listModels();
6210
6706
  }
6211
- async bootstrapState(options = {}) {
6707
+ async updateModel(update) {
6212
6708
  if (!this.initialized) {
6213
6709
  await this.initialize();
6214
6710
  }
6215
- const page = await this.listMessages({
6216
- limit: options.limit,
6217
- order: options.order
6218
- });
6219
- const state = {
6220
- agentId: this._agentId ?? "",
6221
- conversationId: this._conversationId ?? "",
6222
- model: this._model,
6223
- messages: page.messages
6224
- };
6225
- if (this.toolNames !== undefined) {
6226
- state.tools = this.toolNames;
6227
- }
6228
- if (page.nextBefore !== undefined) {
6229
- state.nextBefore = page.nextBefore;
6230
- }
6231
- if (page.hasMore !== undefined) {
6232
- state.hasMore = page.hasMore;
6711
+ if (!this.controller || !this.runtime) {
6712
+ throw new Error("Session is not initialized");
6233
6713
  }
6234
- return state;
6714
+ return this.applyModelUpdate(update);
6235
6715
  }
6236
- close() {
6237
- if (this.closed)
6238
- return;
6239
- this.closed = true;
6240
- this.removeMessageHandler?.();
6241
- this.removeMessageHandler = null;
6242
- if (this.activeTurn?.timeout)
6243
- clearTimeout(this.activeTurn.timeout);
6244
- for (const turn of this.pendingTurns) {
6245
- if (turn.timeout)
6246
- clearTimeout(turn.timeout);
6247
- }
6248
- this.activeTurn = null;
6249
- this.pendingTurns.length = 0;
6250
- for (const cancel of [...this.deviceStatusRefreshCancels]) {
6251
- cancel(new Error(`Session closed while waiting for ${this.label} device status`));
6716
+ async applyModelUpdate(update) {
6717
+ if (!this.controller || !this.runtime) {
6718
+ throw new Error("Session is not initialized");
6252
6719
  }
6253
- this.deviceStatusRefreshCancels.clear();
6254
- this.deviceStatusListeners.clear();
6255
- this.controller?.close();
6256
- this.controller = null;
6257
- this.onCoreClose();
6258
- this.resolveAll(null);
6259
- }
6260
- get agentId() {
6261
- return this._agentId;
6262
- }
6263
- get sessionId() {
6264
- return this._sessionId;
6265
- }
6266
- get conversationId() {
6267
- return this._conversationId;
6268
- }
6269
- async[Symbol.asyncDispose]() {
6270
- this.close();
6720
+ const normalized = normalizeUpdateModelInput(update);
6721
+ const payload = await this.resolveUpdateModelPayload(normalized);
6722
+ const result = await this.controller.updateModel(this.runtime, payload);
6723
+ if (result.modelHandle !== undefined) {
6724
+ this._model = result.modelHandle;
6725
+ } else if (payload.model_handle !== undefined) {
6726
+ this._model = payload.model_handle;
6727
+ } else if (typeof result.modelSettings?.model === "string") {
6728
+ this._model = result.modelSettings.model;
6729
+ }
6730
+ if ("modelSettings" in result) {
6731
+ this._modelSettings = result.modelSettings ?? null;
6732
+ }
6733
+ return result;
6271
6734
  }
6272
- async changeDeviceState(updates) {
6735
+ async recoverPendingApprovals(options = {}) {
6273
6736
  if (!this.initialized) {
6274
6737
  await this.initialize();
6275
6738
  }
6276
6739
  if (!this.controller || !this.runtime) {
6277
6740
  throw new Error("Session is not initialized");
6278
6741
  }
6279
- const payload = {};
6280
- if (updates.cwd !== undefined)
6281
- payload.cwd = updates.cwd;
6282
- if (updates.permissionMode !== undefined) {
6283
- const mode = mapPermissionMode(updates.permissionMode);
6284
- if (mode !== undefined)
6285
- payload.mode = mode;
6286
- }
6287
- if (Object.keys(payload).length === 0) {
6288
- throw new Error("Invalid device state update. Expected cwd or permissionMode.");
6289
- }
6290
- this.controller.send({
6291
- type: "change_device_state",
6292
- runtime: this.runtime,
6293
- payload
6294
- });
6742
+ return this.controller.recoverPendingApprovals(this.runtime, options);
6295
6743
  }
6296
- async updateToolset(toolsetPreference) {
6744
+ async removeQueuedMessage(itemId) {
6745
+ if (typeof itemId !== "string" || itemId.trim().length === 0) {
6746
+ throw new Error("Invalid queue item id. Expected a non-empty string.");
6747
+ }
6297
6748
  if (!this.initialized) {
6298
6749
  await this.initialize();
6299
6750
  }
6300
6751
  if (!this.controller || !this.runtime) {
6301
6752
  throw new Error("Session is not initialized");
6302
6753
  }
6303
- const response = await this.controller.request("update_toolset", {
6754
+ const response = await this.controller.request("remove_queue_item", {
6304
6755
  runtime: this.runtime,
6305
- toolset_preference: toolsetPreference
6306
- }, { predicate: (message) => message.type === "update_toolset_response" });
6307
- ensureSuccess(response, "Failed to update toolset");
6308
- }
6309
- async afterRuntimeInitialized() {}
6310
- async beforeTurn() {}
6311
- onCoreClose() {}
6312
- currentOptions() {
6313
- return this.mode.options;
6314
- }
6315
- shouldEnableMemfs(options) {
6316
- return false;
6317
- }
6318
- enableMemfsBody() {
6319
- if (!this.runtime)
6320
- return {};
6321
- return { agent_id: this.runtime.agent_id };
6322
- }
6323
- setModel(model) {
6324
- this._model = model;
6325
- }
6326
- trackSentTurn(runtime) {
6327
- const turn = {
6328
- id: ++this.nextTurnId,
6329
- runtime,
6330
- clientMessageId: `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
6331
- queuedAt: Date.now(),
6332
- startedAt: 0,
6333
- assistantText: "",
6334
- runIds: new Set,
6335
- observedTurnEvidence: false,
6336
- observedRequiresApprovalStop: false,
6337
- abortRequested: false,
6338
- timeout: null
6339
- };
6340
- if (this.activeTurn) {
6341
- this.pendingTurns.push(turn);
6342
- } else {
6343
- this.activateTurn(turn);
6344
- }
6345
- return turn;
6346
- }
6347
- activateTurn(turn) {
6348
- this.activeTurn = turn;
6349
- turn.startedAt = Date.now();
6350
- this.activeTurnStartedAt = turn.startedAt;
6351
- if (this.requestTimeoutMs !== undefined) {
6352
- turn.timeout = setTimeout(() => {
6353
- this.failTurn(turn, `Timed out waiting for ${this.label} turn`);
6354
- }, this.requestTimeoutMs);
6355
- turn.timeout.unref?.();
6356
- }
6357
- }
6358
- activateNextTurnFromProtocol() {
6359
- if (this.activeTurn)
6360
- return this.activeTurn;
6361
- const next = this.pendingTurns.shift();
6362
- if (!next)
6363
- return null;
6364
- this.activateTurn(next);
6365
- return next;
6366
- }
6367
- removeTrackedTurn(turn) {
6368
- if (turn.timeout)
6369
- clearTimeout(turn.timeout);
6370
- if (this.activeTurn === turn) {
6371
- this.activeTurn = null;
6372
- return;
6373
- }
6374
- const index = this.pendingTurns.indexOf(turn);
6375
- if (index !== -1)
6376
- this.pendingTurns.splice(index, 1);
6377
- }
6378
- failTurn(turn, detail) {
6379
- if (this.activeTurn !== turn)
6380
- return;
6381
- this.enqueue({
6382
- type: "error",
6383
- message: detail,
6384
- errorCode: "error",
6385
- stopReason: "error",
6386
- errorDetail: detail,
6387
- recoverable: false
6388
- });
6389
- this.completeActiveTurn({
6390
- runtime: turn.runtime,
6391
- stopReason: "error",
6392
- runIds: [...turn.runIds],
6393
- success: false,
6394
- detail,
6395
- errorCode: "error"
6756
+ item_id: itemId
6757
+ }, {
6758
+ predicate: (message) => message.type === "remove_queue_item_response" && message.item_id === itemId
6396
6759
  });
6397
- }
6398
- completeActiveTurn(turn) {
6399
- const active = this.activeTurn;
6400
- if (!active)
6401
- return;
6402
- if (active.timeout) {
6403
- clearTimeout(active.timeout);
6404
- active.timeout = null;
6760
+ if (typeof response.success !== "boolean") {
6761
+ throw new Error("Invalid remove_queue_item_response from runtime");
6405
6762
  }
6406
- this.enqueue(this.resultFromTurn(turn, active));
6407
- this.activeTurn = null;
6763
+ return {
6764
+ itemId: typeof response.item_id === "string" ? response.item_id : itemId,
6765
+ removed: response.success
6766
+ };
6408
6767
  }
6409
- async resolveUpdateModelPayload(input) {
6410
- if (input.reasoningEffort === undefined) {
6411
- return modelPayloadWithoutReasoning(input);
6768
+ async getDeviceStatus(options = {}) {
6769
+ if (!this.initialized) {
6770
+ await this.initialize();
6412
6771
  }
6413
- if (!this.controller) {
6772
+ if (!this.controller || !this.runtime) {
6414
6773
  throw new Error("Session is not initialized");
6415
6774
  }
6416
- const catalog = await this.controller.listModels();
6417
- const byId = new Map(catalog.entries.map((entry) => [entry.id, entry]));
6418
- const aliases = catalog.byokProviderAliases;
6419
- let baseEntry;
6420
- let explicitHandle;
6421
- let targetHandle;
6422
- if (input.modelId !== undefined) {
6423
- baseEntry = byId.get(input.modelId);
6424
- explicitHandle = input.modelHandle;
6425
- targetHandle = baseEntry?.handle ?? toBaseModelHandle(input.modelHandle, aliases);
6426
- } else if (input.modelHandle !== undefined) {
6427
- explicitHandle = input.modelHandle;
6428
- targetHandle = toBaseModelHandle(input.modelHandle, aliases);
6429
- } else if (input.model !== undefined) {
6430
- baseEntry = byId.get(input.model);
6431
- if (baseEntry) {
6432
- targetHandle = baseEntry.handle;
6433
- } else {
6434
- explicitHandle = input.model;
6435
- targetHandle = toBaseModelHandle(input.model, aliases);
6436
- }
6437
- } else {
6438
- explicitHandle = this._model || undefined;
6439
- targetHandle = toBaseModelHandle(this._model || undefined, aliases);
6440
- }
6441
- if (!targetHandle) {
6442
- throw new Error("reasoningEffort requires a current model or explicit model/modelId/modelHandle.");
6443
- }
6444
- const candidates = catalog.entries.filter((entry) => entry.handle === targetHandle || entry.handle === explicitHandle);
6445
- if (candidates.length === 0) {
6446
- throw new Error(`reasoningEffort requires a model from listModels(); no catalog entry found for ${targetHandle}.`);
6447
- }
6448
- const contextWindow = getContextWindow(baseEntry?.updateArgs) ?? getContextWindow(this._modelSettings);
6449
- const scopedCandidates = sameContextCandidates(candidates, contextWindow);
6450
- const matchingEntry = scopedCandidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort) ?? candidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort);
6451
- if (!matchingEntry) {
6452
- throw new Error(`No ${input.reasoningEffort} reasoning tier found for model ${targetHandle}.`);
6775
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs ?? 30000;
6776
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
6777
+ throw new Error("Invalid device status timeout. Expected a positive integer.");
6453
6778
  }
6454
- const payload = { model_id: matchingEntry.id };
6455
- if (explicitHandle !== undefined) {
6456
- payload.model_handle = explicitHandle;
6779
+ return this.refreshDeviceStatus(timeoutMs);
6780
+ }
6781
+ onDeviceStatus(listener) {
6782
+ if (typeof listener !== "function") {
6783
+ throw new Error("Invalid device status listener. Expected a function.");
6457
6784
  }
6458
- return payload;
6785
+ this.deviceStatusListeners.add(listener);
6786
+ return () => {
6787
+ this.deviceStatusListeners.delete(listener);
6788
+ };
6459
6789
  }
6460
- async applyPostInitializeOptions() {
6461
- if (!this.controller || !this.runtime)
6462
- return;
6463
- const options = this.currentOptions();
6464
- if (this.shouldEnableMemfs(options)) {
6465
- const response = await this.controller.request("enable_memfs", this.enableMemfsBody(), {
6466
- predicate: (message) => message.type === "enable_memfs_response",
6467
- timeoutMs: 180000
6790
+ refreshDeviceStatus(timeoutMs) {
6791
+ const controller = this.controller;
6792
+ const runtime = this.runtime;
6793
+ if (!controller || !runtime) {
6794
+ return Promise.reject(new Error("Session is not initialized"));
6795
+ }
6796
+ return new Promise((resolve, reject) => {
6797
+ let status = null;
6798
+ let syncAcknowledged = false;
6799
+ let settled = false;
6800
+ const cleanup = () => {
6801
+ clearTimeout(timer);
6802
+ unsubscribe();
6803
+ this.deviceStatusRefreshCancels.delete(cancel);
6804
+ };
6805
+ const rejectOnce = (error) => {
6806
+ if (settled)
6807
+ return;
6808
+ settled = true;
6809
+ cleanup();
6810
+ reject(error);
6811
+ };
6812
+ const resolveIfComplete = () => {
6813
+ if (settled || !syncAcknowledged || !status)
6814
+ return;
6815
+ settled = true;
6816
+ cleanup();
6817
+ resolve(status);
6818
+ };
6819
+ const cancel = (error) => rejectOnce(error);
6820
+ const timer = setTimeout(() => {
6821
+ rejectOnce(new Error(`Timed out waiting for ${this.label} device status`));
6822
+ }, timeoutMs);
6823
+ timer.unref?.();
6824
+ const unsubscribe = this.onDeviceStatus((nextStatus) => {
6825
+ status = nextStatus;
6826
+ resolveIfComplete();
6827
+ });
6828
+ this.deviceStatusRefreshCancels.add(cancel);
6829
+ controller.request("sync", {
6830
+ runtime,
6831
+ recover_approvals: false,
6832
+ force_device_status: true
6833
+ }, {
6834
+ timeoutMs,
6835
+ predicate: (message) => message.type === "sync_response"
6836
+ }).then((response) => {
6837
+ if (response.success === false) {
6838
+ rejectOnce(new Error(typeof response.error === "string" ? response.error : `Failed to refresh ${this.label} device status`));
6839
+ return;
6840
+ }
6841
+ syncAcknowledged = true;
6842
+ resolveIfComplete();
6843
+ }, (error) => {
6844
+ rejectOnce(error instanceof Error ? error : new Error(String(error)));
6468
6845
  });
6469
- ensureSuccess(response, "Failed to enable memfs");
6846
+ });
6847
+ }
6848
+ async listMessages(options = {}) {
6849
+ if (!this.initialized) {
6850
+ await this.initialize();
6470
6851
  }
6471
- const dreamingSettings = resolveDreamingSettings(options.dreaming);
6472
- if (dreamingSettings) {
6473
- const response = await this.controller.request("set_reflection_settings", {
6474
- runtime: this.runtime,
6475
- settings: dreamingSettings,
6476
- scope: "both"
6477
- }, { predicate: (message) => message.type === "set_reflection_settings_response" });
6478
- ensureSuccess(response, "Failed to update dreaming settings");
6852
+ if (!this.controller) {
6853
+ throw new Error("Session is not initialized");
6479
6854
  }
6480
- if (this.mode.kind !== "session")
6481
- return;
6482
- if (this.mode.options.model !== undefined || this.mode.options.reasoningEffort !== undefined) {
6483
- await this.updateModel({
6484
- ...this.mode.options.model !== undefined ? { model: this.mode.options.model } : {},
6485
- ...this.mode.options.reasoningEffort !== undefined ? { reasoningEffort: this.mode.options.reasoningEffort } : {}
6486
- });
6855
+ const conversationId = options.conversationId ?? this._conversationId;
6856
+ if (!conversationId) {
6857
+ throw new Error("No conversation id available for listMessages()");
6487
6858
  }
6859
+ return this.controller.listMessages(conversationId, options);
6488
6860
  }
6489
- handleProtocolMessage = (message) => {
6490
- if (!this.runtime || !sameRuntime2(message, this.runtime))
6491
- return;
6492
- const deviceStatus = deviceStatusRecord(message);
6493
- if (deviceStatus) {
6494
- const status = toSessionDeviceStatus(deviceStatus);
6495
- if (!status)
6496
- return;
6497
- for (const listener of [...this.deviceStatusListeners]) {
6498
- try {
6499
- listener(status);
6500
- } catch {}
6501
- }
6502
- return;
6503
- }
6504
- if (message.type === "update_queue") {
6505
- const sdkMessage2 = {
6506
- type: "queue_update",
6507
- queue: queueItems(message)
6508
- };
6509
- this.enqueue(sdkMessage2);
6510
- return;
6861
+ async bootstrapState(options = {}) {
6862
+ if (!this.initialized) {
6863
+ await this.initialize();
6511
6864
  }
6512
- if (message.type === "update_loop_status") {
6513
- this.handleLoopStatusMessage(message);
6514
- return;
6865
+ const page = await this.listMessages({
6866
+ limit: options.limit,
6867
+ order: options.order
6868
+ });
6869
+ const state = {
6870
+ agentId: this._agentId ?? "",
6871
+ conversationId: this._conversationId ?? "",
6872
+ model: this._model,
6873
+ messages: page.messages
6874
+ };
6875
+ if (this.toolNames !== undefined) {
6876
+ state.tools = this.toolNames;
6515
6877
  }
6516
- const delta = streamDeltaRecord(message);
6517
- if (!delta)
6518
- return;
6519
- const active = this.activateNextTurnFromProtocol();
6520
- if (active) {
6521
- active.observedTurnEvidence = true;
6522
- const runId = streamDeltaRunId2(delta);
6523
- if (runId)
6524
- active.runIds.add(runId);
6878
+ if (page.nextBefore !== undefined) {
6879
+ state.nextBefore = page.nextBefore;
6525
6880
  }
6526
- const sdkMessage = this.transformStreamDelta(delta);
6527
- if (sdkMessage) {
6528
- this.enqueue(sdkMessage);
6881
+ if (page.hasMore !== undefined) {
6882
+ state.hasMore = page.hasMore;
6529
6883
  }
6530
- this.handleTurnTerminalDelta(delta, sdkMessage);
6531
- };
6532
- handleLoopStatusMessage(message) {
6533
- const status = loopStatusValue(message);
6534
- if (!status)
6535
- return;
6536
- const activeRunIds = loopStatusRunIds(message);
6537
- const sdkMessage = {
6538
- type: "loop_status",
6539
- status,
6540
- activeRunIds
6541
- };
6542
- this.enqueue(sdkMessage);
6543
- const active = this.activeTurn;
6544
- if (!active)
6545
- return;
6546
- for (const runId of activeRunIds)
6547
- active.runIds.add(runId);
6548
- const hadTurnEvidence = active.observedTurnEvidence || active.observedRequiresApprovalStop;
6549
- if (!hadTurnEvidence)
6550
- return;
6551
- if (status === "WAITING_ON_APPROVAL") {
6552
- this.completeActiveTurn({
6553
- runtime: active.runtime,
6554
- stopReason: "requires_approval",
6555
- runIds: [...active.runIds]
6556
- });
6884
+ return state;
6885
+ }
6886
+ close() {
6887
+ if (this.closed)
6557
6888
  return;
6889
+ this.closed = true;
6890
+ this.removeMessageHandler?.();
6891
+ this.removeMessageHandler = null;
6892
+ this.turns.close();
6893
+ for (const cancel of [...this.deviceStatusRefreshCancels]) {
6894
+ cancel(new Error(`Session closed while waiting for ${this.label} device status`));
6558
6895
  }
6559
- if (status === "WAITING_ON_INPUT" && active.abortRequested) {
6560
- this.completeActiveTurn({
6561
- runtime: active.runtime,
6562
- stopReason: "interrupted",
6563
- runIds: [...active.runIds],
6564
- success: false,
6565
- detail: "Interrupted",
6566
- errorCode: "interrupted"
6567
- });
6568
- return;
6896
+ this.deviceStatusRefreshCancels.clear();
6897
+ this.deviceStatusListeners.clear();
6898
+ this.controller?.close();
6899
+ this.controller = null;
6900
+ this.onCoreClose();
6901
+ }
6902
+ get agentId() {
6903
+ return this._agentId;
6904
+ }
6905
+ get sessionId() {
6906
+ return this._sessionId;
6907
+ }
6908
+ get conversationId() {
6909
+ return this._conversationId;
6910
+ }
6911
+ async[Symbol.asyncDispose]() {
6912
+ this.close();
6913
+ }
6914
+ async changeDeviceState(updates) {
6915
+ if (!this.initialized) {
6916
+ await this.initialize();
6569
6917
  }
6570
- if (status === "WAITING_ON_INPUT" && active.observedTurnEvidence) {
6571
- this.completeActiveTurn({
6572
- runtime: active.runtime,
6573
- stopReason: null,
6574
- runIds: [...active.runIds]
6575
- });
6918
+ if (!this.controller || !this.runtime) {
6919
+ throw new Error("Session is not initialized");
6576
6920
  }
6577
- }
6578
- handleTurnTerminalDelta(delta, sdkMessage) {
6579
- const active = this.activeTurn;
6580
- if (!active)
6581
- return;
6582
- const messageType = streamDeltaMessageType2(delta);
6583
- if (messageType === "stop_reason") {
6584
- const stopReason = streamDeltaStopReason2(delta) ?? null;
6585
- if (stopReason === "requires_approval") {
6586
- active.observedRequiresApprovalStop = true;
6587
- return;
6588
- }
6589
- this.completeActiveTurn({
6590
- runtime: active.runtime,
6591
- stopReason,
6592
- runIds: [...active.runIds]
6593
- });
6594
- return;
6921
+ const payload = {};
6922
+ if (updates.cwd !== undefined)
6923
+ payload.cwd = updates.cwd;
6924
+ if (updates.permissionMode !== undefined) {
6925
+ const mode = mapPermissionMode(updates.permissionMode);
6926
+ if (mode !== undefined)
6927
+ payload.mode = mode;
6595
6928
  }
6596
- if (sdkMessage?.type === "error") {
6597
- this.completeActiveTurn({
6598
- runtime: active.runtime,
6599
- stopReason: sdkMessage.stopReason,
6600
- runIds: [...active.runIds],
6601
- success: false,
6602
- detail: sdkMessage.errorDetail ?? sdkMessage.message,
6603
- errorCode: sdkMessage.errorCode
6604
- });
6929
+ if (Object.keys(payload).length === 0) {
6930
+ throw new Error("Invalid device state update. Expected cwd or permissionMode.");
6605
6931
  }
6932
+ this.controller.send({
6933
+ type: "change_device_state",
6934
+ runtime: this.runtime,
6935
+ payload
6936
+ });
6606
6937
  }
6607
- transformStreamDelta(delta) {
6608
- const messageType = typeof delta.message_type === "string" ? delta.message_type : undefined;
6609
- const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
6610
- const uuid = typeof delta.id === "string" ? delta.id : `${this.label}-${++this.messageCounter}`;
6611
- if (messageType === "assistant_message") {
6612
- const content = extractTextFromContent(delta.content);
6613
- if (!content)
6614
- return null;
6615
- if (this.activeTurn)
6616
- this.activeTurn.assistantText += content;
6617
- return { type: "assistant", content, uuid, runId };
6938
+ async updateToolset(toolsetPreference) {
6939
+ if (!this.initialized) {
6940
+ await this.initialize();
6618
6941
  }
6619
- if (messageType === "reasoning_message") {
6620
- const content = typeof delta.reasoning === "string" ? delta.reasoning : extractTextFromContent(delta.content);
6621
- if (!content)
6622
- return null;
6623
- return { type: "reasoning", content, uuid, runId };
6942
+ if (!this.controller || !this.runtime) {
6943
+ throw new Error("Session is not initialized");
6624
6944
  }
6625
- if (messageType === "tool_call_message" || messageType === "approval_request_message") {
6626
- const toolCall = firstToolCall(delta);
6627
- if (!toolCall)
6628
- return null;
6629
- const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
6630
- const toolCallId = (typeof toolCall.tool_call_id === "string" ? toolCall.tool_call_id : undefined) ?? (typeof toolCall.id === "string" ? toolCall.id : undefined);
6631
- if (!toolCallId) {
6632
- const detail = `Missing tool_call_id in ${messageType} (uuid=${uuid})`;
6633
- return {
6634
- type: "error",
6635
- message: detail,
6636
- errorCode: "protocol_error",
6637
- stopReason: "protocol_error",
6638
- runId,
6639
- recoverable: false,
6640
- errorDetail: detail
6641
- };
6945
+ const response = await this.controller.request("update_toolset", {
6946
+ runtime: this.runtime,
6947
+ toolset_preference: toolsetPreference
6948
+ }, { predicate: (message) => message.type === "update_toolset_response" });
6949
+ ensureSuccess(response, "Failed to update toolset");
6950
+ }
6951
+ async afterRuntimeInitialized() {}
6952
+ async beforeTurn() {}
6953
+ onCoreClose() {}
6954
+ currentOptions() {
6955
+ return this.mode.options;
6956
+ }
6957
+ shouldEnableMemfs(options) {
6958
+ return false;
6959
+ }
6960
+ enableMemfsBody() {
6961
+ if (!this.runtime)
6962
+ return {};
6963
+ return { agent_id: this.runtime.agent_id };
6964
+ }
6965
+ setModel(model) {
6966
+ this._model = model;
6967
+ }
6968
+ async resolveUpdateModelPayload(input) {
6969
+ if (input.reasoningEffort === undefined) {
6970
+ return modelPayloadWithoutReasoning(input);
6971
+ }
6972
+ if (!this.controller) {
6973
+ throw new Error("Session is not initialized");
6974
+ }
6975
+ const catalog = await this.controller.listModels();
6976
+ const byId = new Map(catalog.entries.map((entry) => [entry.id, entry]));
6977
+ const aliases = catalog.byokProviderAliases;
6978
+ let baseEntry;
6979
+ let explicitHandle;
6980
+ let targetHandle;
6981
+ if (input.modelId !== undefined) {
6982
+ baseEntry = byId.get(input.modelId);
6983
+ explicitHandle = input.modelHandle;
6984
+ targetHandle = baseEntry?.handle ?? toBaseModelHandle(input.modelHandle, aliases);
6985
+ } else if (input.modelHandle !== undefined) {
6986
+ explicitHandle = input.modelHandle;
6987
+ targetHandle = toBaseModelHandle(input.modelHandle, aliases);
6988
+ } else if (input.model !== undefined) {
6989
+ baseEntry = byId.get(input.model);
6990
+ if (baseEntry) {
6991
+ targetHandle = baseEntry.handle;
6992
+ } else {
6993
+ explicitHandle = input.model;
6994
+ targetHandle = toBaseModelHandle(input.model, aliases);
6642
6995
  }
6643
- const toolName = (typeof toolCall.name === "string" ? toolCall.name : undefined) ?? (typeof fn?.name === "string" ? fn.name : undefined) ?? "?";
6644
- const { input, raw } = toolInputFromArguments(toolCall.arguments ?? fn?.arguments);
6645
- return {
6646
- type: "tool_call",
6647
- toolCallId,
6648
- toolName,
6649
- toolInput: input,
6650
- rawArguments: raw,
6651
- uuid,
6652
- runId
6653
- };
6996
+ } else {
6997
+ explicitHandle = this._model || undefined;
6998
+ targetHandle = toBaseModelHandle(this._model || undefined, aliases);
6654
6999
  }
6655
- if (messageType === "tool_return_message") {
6656
- const toolReturn = firstToolReturn(delta) ?? delta;
6657
- const toolCallId = (typeof delta.tool_call_id === "string" ? delta.tool_call_id : undefined) ?? (typeof toolReturn.tool_call_id === "string" ? toolReturn.tool_call_id : undefined);
6658
- if (!toolCallId)
6659
- return null;
6660
- const content = extractTextFromContent(delta.tool_return ?? toolReturn.tool_return ?? toolReturn.content) ?? "";
6661
- const status = typeof delta.status === "string" ? delta.status : toolReturn.status;
6662
- return {
6663
- type: "tool_result",
6664
- toolCallId,
6665
- content,
6666
- isError: status === "error",
6667
- uuid,
6668
- runId
6669
- };
7000
+ if (!targetHandle) {
7001
+ throw new Error("reasoningEffort requires a current model or explicit model/modelId/modelHandle.");
6670
7002
  }
6671
- if (messageType === "error_message" || messageType === "loop_error") {
6672
- const detail = (typeof delta.detail === "string" ? delta.detail : undefined) ?? (typeof delta.message === "string" ? delta.message : undefined) ?? `${this.label} turn failed`;
6673
- const stopReason = (typeof delta.stop_reason === "string" ? delta.stop_reason : undefined) ?? (typeof delta.error_type === "string" ? delta.error_type : undefined) ?? "error";
6674
- const approvalConflict = isApprovalConflictSignal({
6675
- detail,
6676
- message: typeof delta.message === "string" ? delta.message : undefined,
6677
- stopReason
6678
- });
6679
- return {
6680
- type: "error",
6681
- message: detail,
6682
- errorCode: approvalConflict ? "approval_conflict" : toSdkErrorCode(stopReason),
6683
- approvalConflict: approvalConflict || undefined,
6684
- recoverable: approvalConflict ? true : false,
6685
- errorDetail: detail,
6686
- stopReason,
6687
- runId
6688
- };
7003
+ const candidates = catalog.entries.filter((entry) => entry.handle === targetHandle || entry.handle === explicitHandle);
7004
+ if (candidates.length === 0) {
7005
+ throw new Error(`reasoningEffort requires a model from listModels(); no catalog entry found for ${targetHandle}.`);
6689
7006
  }
6690
- if (messageType === "retry") {
6691
- return {
6692
- type: "retry",
6693
- reason: typeof delta.reason === "string" ? delta.reason : "error",
6694
- attempt: typeof delta.attempt === "number" ? delta.attempt : 0,
6695
- maxAttempts: typeof delta.max_attempts === "number" ? delta.max_attempts : 0,
6696
- delayMs: typeof delta.delay_ms === "number" ? delta.delay_ms : 0,
6697
- runId
6698
- };
7007
+ const contextWindow = getContextWindow(baseEntry?.updateArgs) ?? getContextWindow(this._modelSettings);
7008
+ const scopedCandidates = sameContextCandidates(candidates, contextWindow);
7009
+ const matchingEntry = scopedCandidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort) ?? candidates.find((entry) => getReasoningEffort(entry) === input.reasoningEffort);
7010
+ if (!matchingEntry) {
7011
+ throw new Error(`No ${input.reasoningEffort} reasoning tier found for model ${targetHandle}.`);
6699
7012
  }
6700
- if (messageType === "stop_reason" || messageType === "ping") {
6701
- return null;
7013
+ const payload = { model_id: matchingEntry.id };
7014
+ if (explicitHandle !== undefined) {
7015
+ payload.model_handle = explicitHandle;
6702
7016
  }
6703
- return {
6704
- type: "stream_event",
6705
- event: delta,
6706
- uuid
6707
- };
6708
- }
6709
- resultFromTurn(turn, tracker) {
6710
- const stopReason = turn.stopReason ?? (turn.success === false ? "error" : undefined);
6711
- const approvalConflict = isApprovalConflictSignal({
6712
- detail: turn.detail,
6713
- stopReason
6714
- });
6715
- const success = turn.success !== undefined ? turn.success && !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "") : !approvalConflict && !FAILURE_STOP_REASONS.has(stopReason ?? "");
6716
- const errorCode = approvalConflict ? "approval_conflict" : turn.errorCode ?? toSdkErrorCode(stopReason);
6717
- return {
6718
- type: "result",
6719
- success,
6720
- result: success ? tracker?.assistantText || undefined : undefined,
6721
- error: success ? undefined : errorCode ?? stopReason ?? "error",
6722
- errorCode: success ? undefined : errorCode ?? "error",
6723
- approvalConflict: approvalConflict || undefined,
6724
- recoverable: approvalConflict ? true : success ? undefined : false,
6725
- errorDetail: success ? undefined : turn.detail,
6726
- stopReason,
6727
- durationMs: Date.now() - (tracker?.startedAt || this.activeTurnStartedAt),
6728
- conversationId: turn.runtime.conversation_id,
6729
- runIds: turn.runIds.length > 0 ? turn.runIds : undefined
6730
- };
7017
+ return payload;
6731
7018
  }
6732
- enqueue(message) {
6733
- const resolver = this.streamResolvers.shift();
6734
- if (resolver) {
6735
- resolver(message);
7019
+ async applyPostInitializeOptions() {
7020
+ if (!this.controller || !this.runtime)
6736
7021
  return;
7022
+ const options = this.currentOptions();
7023
+ if (this.shouldEnableMemfs(options)) {
7024
+ const response = await this.controller.request("enable_memfs", this.enableMemfsBody(), {
7025
+ predicate: (message) => message.type === "enable_memfs_response",
7026
+ timeoutMs: 180000
7027
+ });
7028
+ ensureSuccess(response, "Failed to enable memfs");
7029
+ }
7030
+ const dreamingSettings = resolveDreamingSettings(options.dreaming);
7031
+ if (dreamingSettings) {
7032
+ const response = await this.controller.request("set_reflection_settings", {
7033
+ runtime: this.runtime,
7034
+ settings: dreamingSettings,
7035
+ scope: "both"
7036
+ }, { predicate: (message) => message.type === "set_reflection_settings_response" });
7037
+ ensureSuccess(response, "Failed to update dreaming settings");
7038
+ }
7039
+ if (this.mode.kind !== "session")
7040
+ return;
7041
+ if (this.mode.options.model !== undefined || this.mode.options.reasoningEffort !== undefined) {
7042
+ await this.applyModelUpdate({
7043
+ ...this.mode.options.model !== undefined ? { model: this.mode.options.model } : {},
7044
+ ...this.mode.options.reasoningEffort !== undefined ? { reasoningEffort: this.mode.options.reasoningEffort } : {}
7045
+ });
6737
7046
  }
6738
- this.streamQueue.push(message);
6739
- }
6740
- nextMessage() {
6741
- const next = this.streamQueue.shift();
6742
- if (next)
6743
- return Promise.resolve(next);
6744
- if (this.closed)
6745
- return Promise.resolve(null);
6746
- return new Promise((resolve) => {
6747
- this.streamResolvers.push(resolve);
6748
- });
6749
7047
  }
6750
- resolveAll(value) {
6751
- for (const resolve of this.streamResolvers.splice(0)) {
6752
- resolve(value);
7048
+ emitDeviceStatus(status) {
7049
+ for (const listener of [...this.deviceStatusListeners]) {
7050
+ try {
7051
+ listener(status);
7052
+ } catch {}
6753
7053
  }
6754
7054
  }
6755
7055
  }
@@ -7243,13 +7543,15 @@ class AppServerSession extends RemoteClientSessionCore {
7243
7543
  return options.memfs !== false;
7244
7544
  }
7245
7545
  async initializeRuntimeController() {
7546
+ await this.remoteOptions.beforeConnect?.();
7246
7547
  const url = await this.resolveAppServerUrl();
7247
- const client = createAppServerClient({
7548
+ await releaseAppServerManagementConnections(url);
7549
+ const client = applyUniqueRequestIds(createAppServerClient({
7248
7550
  url,
7249
7551
  ...this.remoteOptions.authToken !== undefined ? { authToken: this.remoteOptions.authToken } : {},
7250
7552
  ...this.remoteOptions.WebSocket ? { WebSocket: this.remoteOptions.WebSocket } : {},
7251
7553
  ...this.remoteOptions.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.remoteOptions.requestTimeoutMs } : {}
7252
- });
7554
+ }));
7253
7555
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
7254
7556
  client,
7255
7557
  getRuntime: () => this.runtime,
@@ -7648,13 +7950,58 @@ class RemoteEnvironmentClient {
7648
7950
  }
7649
7951
  }
7650
7952
 
7953
+ // src/cloud-sandbox.ts
7954
+ var MIN_TTL_MINUTES = 1;
7955
+ var MAX_TTL_MINUTES = 60;
7956
+ var MAX_GITHUB_REPOSITORIES = 10;
7957
+ var GITHUB_OWNER_PATTERN = /^[A-Za-z0-9-]+$/;
7958
+ var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]+$/;
7959
+ function validatePositiveInteger(value, name) {
7960
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
7961
+ throw new Error(`Invalid ${name}. Expected a positive integer.`);
7962
+ }
7963
+ }
7964
+ function validateCloudSandboxOptions(options, name) {
7965
+ if (options === undefined)
7966
+ return;
7967
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
7968
+ throw new Error(`Invalid ${name}. Expected an object.`);
7969
+ }
7970
+ if (options.ttlMinutes !== undefined && (!Number.isInteger(options.ttlMinutes) || options.ttlMinutes < MIN_TTL_MINUTES || options.ttlMinutes > MAX_TTL_MINUTES)) {
7971
+ throw new Error(`Invalid ${name}.ttlMinutes. Expected an integer between ${MIN_TTL_MINUTES} and ${MAX_TTL_MINUTES}.`);
7972
+ }
7973
+ validatePositiveInteger(options.readyTimeoutMs, `${name}.readyTimeoutMs`);
7974
+ validatePositiveInteger(options.readyPollIntervalMs, `${name}.readyPollIntervalMs`);
7975
+ validatePositiveInteger(options.refreshIntervalMs, `${name}.refreshIntervalMs`);
7976
+ if (options.githubRepositories !== undefined) {
7977
+ if (!Array.isArray(options.githubRepositories)) {
7978
+ throw new Error(`Invalid ${name}.githubRepositories. Expected an array.`);
7979
+ }
7980
+ if (options.githubRepositories.length > MAX_GITHUB_REPOSITORIES) {
7981
+ throw new Error(`Invalid ${name}.githubRepositories. Expected at most ${MAX_GITHUB_REPOSITORIES} repositories.`);
7982
+ }
7983
+ for (const [index, repository] of options.githubRepositories.entries()) {
7984
+ if (repository === null || typeof repository !== "object" || Array.isArray(repository)) {
7985
+ throw new Error(`Invalid ${name}.githubRepositories[${index}]. Expected an object.`);
7986
+ }
7987
+ if (typeof repository.owner !== "string" || !GITHUB_OWNER_PATTERN.test(repository.owner)) {
7988
+ throw new Error(`Invalid ${name}.githubRepositories[${index}].owner.`);
7989
+ }
7990
+ if (typeof repository.repo !== "string" || !GITHUB_REPOSITORY_PATTERN.test(repository.repo)) {
7991
+ throw new Error(`Invalid ${name}.githubRepositories[${index}].repo.`);
7992
+ }
7993
+ }
7994
+ }
7995
+ if (options.terminateOnClose !== undefined && typeof options.terminateOnClose !== "boolean") {
7996
+ throw new Error(`Invalid ${name}.terminateOnClose. Expected a boolean.`);
7997
+ }
7998
+ }
7999
+
7651
8000
  // src/cloud-session.ts
7652
8001
  var DEFAULT_CLOUD_API_BASE_URL3 = "https://api.letta.com";
7653
8002
  var DEFAULT_TURN_TIMEOUT_MS = 120000;
7654
8003
  var DEFAULT_PING_INTERVAL_MS = 30000;
7655
8004
  var DEFAULT_SANDBOX_TTL_MINUTES = 5;
7656
- var MIN_SANDBOX_TTL_MINUTES = 1;
7657
- var MAX_SANDBOX_TTL_MINUTES = 60;
7658
8005
  var DEFAULT_SANDBOX_READY_TIMEOUT_MS = 120000;
7659
8006
  var DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS = 1000;
7660
8007
  var DEFAULT_REPOSITORY_ATTACH_TIMEOUT_MS = 1e4;
@@ -7765,34 +8112,13 @@ function assertOkResponse2(response, body, action, url) {
7765
8112
  throw new Error(parts.join(" — "));
7766
8113
  }
7767
8114
  }
7768
- function validatePositiveInteger(value, name) {
8115
+ function validatePositiveInteger2(value, name) {
7769
8116
  if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
7770
8117
  throw new Error(`Invalid ${name}. Expected a positive integer.`);
7771
8118
  }
7772
8119
  }
7773
- function validateIntegerRange(value, name, min, max) {
7774
- if (value === undefined)
7775
- return;
7776
- if (!Number.isInteger(value) || value < min || value > max) {
7777
- throw new Error(`Invalid ${name}. Expected an integer between ${min} and ${max}.`);
7778
- }
7779
- }
7780
- function validateCloudSandboxOptions(options, name) {
7781
- if (options === undefined)
7782
- return;
7783
- if (options === null || typeof options !== "object" || Array.isArray(options)) {
7784
- throw new Error(`Invalid ${name}. Expected an object.`);
7785
- }
7786
- validateIntegerRange(options.ttlMinutes, `${name}.ttlMinutes`, MIN_SANDBOX_TTL_MINUTES, MAX_SANDBOX_TTL_MINUTES);
7787
- validatePositiveInteger(options.readyTimeoutMs, `${name}.readyTimeoutMs`);
7788
- validatePositiveInteger(options.readyPollIntervalMs, `${name}.readyPollIntervalMs`);
7789
- validatePositiveInteger(options.refreshIntervalMs, `${name}.refreshIntervalMs`);
7790
- if (options.terminateOnClose !== undefined && typeof options.terminateOnClose !== "boolean") {
7791
- throw new Error(`Invalid ${name}.terminateOnClose. Expected a boolean.`);
7792
- }
7793
- }
7794
8120
  function validateCloudClientOptions(options) {
7795
- validatePositiveInteger(options.requestTimeoutMs, "requestTimeoutMs");
8121
+ validatePositiveInteger2(options.requestTimeoutMs, "requestTimeoutMs");
7796
8122
  validateCloudSandboxOptions(options.sandbox, "sandbox");
7797
8123
  if (options.environment !== undefined && options.sandbox !== undefined) {
7798
8124
  throw new Error("Letta Cloud sessions cannot specify both environment and sandbox options.");
@@ -8120,14 +8446,14 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8120
8446
  apiKey,
8121
8447
  authMode: this.cloudOptions.webSocketAuth ?? "header"
8122
8448
  });
8123
- const client = createAppServerClient({
8449
+ const client = applyUniqueRequestIds(createAppServerClient({
8124
8450
  url,
8125
8451
  WebSocket: createCloudStatusWebSocketConstructor({
8126
8452
  cloudOptions: this.cloudOptions,
8127
8453
  runtime: resolved.runtime
8128
8454
  }),
8129
8455
  requestTimeoutMs: this.cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS
8130
- });
8456
+ }));
8131
8457
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
8132
8458
  client,
8133
8459
  getRuntime: () => this.runtime,
@@ -8395,10 +8721,15 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8395
8721
  async createManagedSandbox(agentId, conversationId) {
8396
8722
  const fetchImpl = getFetch3(this.cloudOptions.fetch);
8397
8723
  const baseUrl = normalizeCloudApiBaseUrl2(this.cloudOptions.apiBaseUrl);
8724
+ const sandboxOptions = this.resolvedSandboxOptions();
8725
+ const githubRepositories = sandboxOptions.githubRepositories;
8398
8726
  const response = await fetchImpl(`${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sandboxes`, {
8399
8727
  method: "POST",
8400
8728
  headers: cloudHeaders2(this.cloudOptions),
8401
- body: JSON.stringify(conversationId ? { conversationId } : {})
8729
+ body: JSON.stringify({
8730
+ ...conversationId ? { conversationId } : {},
8731
+ ...githubRepositories && githubRepositories.length > 0 ? { githubRepositories } : {}
8732
+ })
8402
8733
  });
8403
8734
  const body = await parseJsonResponse3(response);
8404
8735
  assertOkResponse2(response, body, "Cloud create managed sandbox", `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/sandboxes`);
@@ -8409,7 +8740,6 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8409
8740
  if (responseConversationId !== null && responseConversationId !== conversationId) {
8410
8741
  throw new Error(`Cloud managed sandbox response conversation mismatch: expected ${conversationId ?? "none"}, got ${responseConversationId}.`);
8411
8742
  }
8412
- const sandboxOptions = this.resolvedSandboxOptions();
8413
8743
  const ttlMinutes = sandboxOptions.ttlMinutes ?? DEFAULT_SANDBOX_TTL_MINUTES;
8414
8744
  const readyTimeoutMs = sandboxOptions.readyTimeoutMs ?? DEFAULT_SANDBOX_READY_TIMEOUT_MS;
8415
8745
  const readyPollIntervalMs = sandboxOptions.readyPollIntervalMs ?? DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS;
@@ -8865,6 +9195,10 @@ class LettaAgentClientBase {
8865
9195
  if (requestTimeoutMs !== undefined && (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0)) {
8866
9196
  throw new Error("Invalid appServer.requestTimeoutMs. Expected a positive integer.");
8867
9197
  }
9198
+ const idleLingerMs = localOptions.appServer?.idleLingerMs;
9199
+ if (idleLingerMs !== undefined && (!Number.isInteger(idleLingerMs) || idleLingerMs < 0)) {
9200
+ throw new Error("Invalid appServer.idleLingerMs. Expected a non-negative integer.");
9201
+ }
8868
9202
  const startupTimeoutMs = localOptions.appServer?.startupTimeoutMs;
8869
9203
  if (startupTimeoutMs !== undefined && (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs <= 0)) {
8870
9204
  throw new Error("Invalid appServer.startupTimeoutMs. Expected a positive integer.");
@@ -8877,6 +9211,9 @@ class LettaAgentClientBase {
8877
9211
  if (options.requestTimeoutMs !== undefined && (!Number.isInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
8878
9212
  throw new Error("Invalid requestTimeoutMs. Expected a positive integer.");
8879
9213
  }
9214
+ if (options.idleLingerMs !== undefined && (!Number.isInteger(options.idleLingerMs) || options.idleLingerMs < 0)) {
9215
+ throw new Error("Invalid idleLingerMs. Expected a non-negative integer.");
9216
+ }
8880
9217
  }
8881
9218
  if (this.backend === "cloud") {
8882
9219
  validateCloudClientOptions(options);
@@ -8891,7 +9228,7 @@ class LettaAgentClientBase {
8891
9228
  }
8892
9229
  validateCreateAgentOptions(options);
8893
9230
  if (this.backend === "remote") {
8894
- const session = new AppServerSession(this.remoteOptions(), {
9231
+ const session = new AppServerSession(this.appServerSessionOptions(), {
8895
9232
  kind: "create-agent",
8896
9233
  options
8897
9234
  });
@@ -8914,7 +9251,7 @@ class LettaAgentClientBase {
8914
9251
  if (!agentId) {
8915
9252
  throw new Error("App-server createSession() requires an agent id. Call createAgent() first or pass an agent id.");
8916
9253
  }
8917
- return new AppServerSession(this.remoteOptions(), {
9254
+ return new AppServerSession(this.appServerSessionOptions(), {
8918
9255
  kind: "session",
8919
9256
  agentId,
8920
9257
  newConversation: true,
@@ -8940,13 +9277,13 @@ class LettaAgentClientBase {
8940
9277
  validateCreateSessionOptions(sessionOptions);
8941
9278
  if (this.backend === "remote") {
8942
9279
  if (looksLikeConversationId(id)) {
8943
- return new AppServerSession(this.remoteOptions(), {
9280
+ return new AppServerSession(this.appServerSessionOptions(), {
8944
9281
  kind: "session",
8945
9282
  conversationId: id,
8946
9283
  options
8947
9284
  });
8948
9285
  }
8949
- return new AppServerSession(this.remoteOptions(), {
9286
+ return new AppServerSession(this.appServerSessionOptions(), {
8950
9287
  kind: "session",
8951
9288
  agentId: id,
8952
9289
  defaultConversation: true,
@@ -9045,6 +9382,12 @@ class LettaAgentClientBase {
9045
9382
  }
9046
9383
  return this.options;
9047
9384
  }
9385
+ appServerSessionOptions() {
9386
+ return {
9387
+ ...this.remoteOptions(),
9388
+ beforeConnect: () => this.releaseIdleManagementConnection()
9389
+ };
9390
+ }
9048
9391
  getRepositoriesClient() {
9049
9392
  if (this.backend !== "cloud") {
9050
9393
  throw new Error('client.repositories is only available with backend: "cloud".');
@@ -9052,6 +9395,9 @@ class LettaAgentClientBase {
9052
9395
  this.repositoriesClient ??= new RepositoriesClient(this.cloudOptions());
9053
9396
  return this.repositoriesClient;
9054
9397
  }
9398
+ async releaseIdleManagementConnection() {
9399
+ await this.managementTransport?.releaseIdleConnection?.();
9400
+ }
9055
9401
  getManagementTransport() {
9056
9402
  if (this.managementTransport)
9057
9403
  return this.managementTransport;
@@ -9201,7 +9547,7 @@ ${output.trim()}` : ""}`));
9201
9547
  }
9202
9548
 
9203
9549
  // src/local-app-server-session.ts
9204
- function createLocalAppServerSession(options, mode) {
9550
+ function createLocalAppServerSession(options, mode, beforeConnect) {
9205
9551
  const appServer = options ?? {};
9206
9552
  const sessionOptions = {
9207
9553
  ...appServer.url !== undefined ? { url: appServer.url } : {
@@ -9214,7 +9560,8 @@ function createLocalAppServerSession(options, mode) {
9214
9560
  },
9215
9561
  ...appServer.WebSocket !== undefined ? { WebSocket: appServer.WebSocket } : {},
9216
9562
  ...appServer.requestTimeoutMs !== undefined ? { requestTimeoutMs: appServer.requestTimeoutMs } : {},
9217
- ...appServer.pinGlobalAgent !== undefined ? { pinGlobalAgent: appServer.pinGlobalAgent } : {}
9563
+ ...appServer.pinGlobalAgent !== undefined ? { pinGlobalAgent: appServer.pinGlobalAgent } : {},
9564
+ ...beforeConnect ? { beforeConnect } : {}
9218
9565
  };
9219
9566
  return new AppServerSession(sessionOptions, mode);
9220
9567
  }
@@ -9614,6 +9961,8 @@ class Session {
9614
9961
  _sessionId = null;
9615
9962
  _conversationId = null;
9616
9963
  initialized = false;
9964
+ closed = false;
9965
+ initializePromise = null;
9617
9966
  externalTools = new Map;
9618
9967
  streamQueue = [];
9619
9968
  streamResolvers = [];
@@ -9636,9 +9985,28 @@ class Session {
9636
9985
  }
9637
9986
  }
9638
9987
  async initialize() {
9988
+ if (this.closed) {
9989
+ throw new Error("Session is closed");
9990
+ }
9991
+ if (this.initializePromise) {
9992
+ return this.initializePromise;
9993
+ }
9639
9994
  if (this.initialized) {
9640
9995
  throw new Error("Session already initialized");
9641
9996
  }
9997
+ const attempt = this.performInitialize();
9998
+ const memo = attempt.catch((error) => {
9999
+ this.cleanupFailedInitialize();
10000
+ throw error;
10001
+ }).finally(() => {
10002
+ if (this.initializePromise === memo) {
10003
+ this.initializePromise = null;
10004
+ }
10005
+ });
10006
+ this.initializePromise = memo;
10007
+ return memo;
10008
+ }
10009
+ async performInitialize() {
9642
10010
  sessionLog("init", "connecting transport...");
9643
10011
  await this.transport.connect();
9644
10012
  sessionLog("init", "transport connected, sending initialize request");
@@ -9663,11 +10031,14 @@ class Session {
9663
10031
  this._agentId = initMsg.agent_id;
9664
10032
  this._sessionId = initMsg.session_id;
9665
10033
  this._conversationId = initMsg.conversation_id;
9666
- this.initialized = true;
9667
- this.startBackgroundPump();
9668
10034
  if (this.externalTools.size > 0) {
9669
10035
  await this.registerExternalTools();
9670
10036
  }
10037
+ if (this.closed) {
10038
+ throw new Error("Session is closed");
10039
+ }
10040
+ this.initialized = true;
10041
+ this.startBackgroundPump();
9671
10042
  const allTools = [
9672
10043
  ...initMsg.tools,
9673
10044
  ...Array.from(this.externalTools.keys())
@@ -9698,6 +10069,16 @@ ${stderr}` : "";
9698
10069
  sessionLog("init", `ERROR: transport closed before init message received${detail}`);
9699
10070
  throw new Error(`Failed to initialize session - no init message received${detail}`);
9700
10071
  }
10072
+ cleanupFailedInitialize() {
10073
+ this.transport.close();
10074
+ this.closed = true;
10075
+ this.initialized = false;
10076
+ this._agentId = null;
10077
+ this._sessionId = null;
10078
+ this._conversationId = null;
10079
+ this.pumpClosed = true;
10080
+ this.resolveAllStreamWaiters(null);
10081
+ }
9701
10082
  async send(message) {
9702
10083
  if (!this.initialized) {
9703
10084
  sessionLog("send", "auto-initializing (not yet initialized)");
@@ -10374,6 +10755,9 @@ ${stderr}` : "";
10374
10755
  throw new Error("updateToolset() is not supported by this session. Use a Cloud, Remote, or local agent session.");
10375
10756
  }
10376
10757
  close() {
10758
+ if (this.closed)
10759
+ return;
10760
+ this.closed = true;
10377
10761
  sessionLog("close", `closing session (agent=${this._agentId}, conversation=${this._conversationId})`);
10378
10762
  this.transport.close();
10379
10763
  this.pumpClosed = true;
@@ -10571,7 +10955,8 @@ class LettaAgentClient extends LettaAgentClientBase {
10571
10955
  })
10572
10956
  },
10573
10957
  ...localOptions?.WebSocket !== undefined ? { WebSocket: localOptions.WebSocket } : {},
10574
- ...localOptions?.requestTimeoutMs !== undefined ? { requestTimeoutMs: localOptions.requestTimeoutMs } : {}
10958
+ ...localOptions?.requestTimeoutMs !== undefined ? { requestTimeoutMs: localOptions.requestTimeoutMs } : {},
10959
+ ...localOptions?.idleLingerMs !== undefined ? { idleLingerMs: localOptions.idleLingerMs } : {}
10575
10960
  });
10576
10961
  }
10577
10962
  async createLocalAgent(options) {
@@ -10580,7 +10965,7 @@ class LettaAgentClient extends LettaAgentClientBase {
10580
10965
  const session2 = createLocalAppServerSession(localOptions.appServer, {
10581
10966
  kind: "create-agent",
10582
10967
  options
10583
- });
10968
+ }, () => this.releaseIdleManagementConnection());
10584
10969
  const initMsg2 = await session2.initialize();
10585
10970
  session2.close();
10586
10971
  return initMsg2.agentId;
@@ -10598,7 +10983,7 @@ class LettaAgentClient extends LettaAgentClientBase {
10598
10983
  agentId,
10599
10984
  newConversation: true,
10600
10985
  options
10601
- });
10986
+ }, () => this.releaseIdleManagementConnection());
10602
10987
  }
10603
10988
  if (agentId) {
10604
10989
  return new Session({ ...sessionOptions, agentId, newConversation: true });
@@ -10613,14 +10998,14 @@ class LettaAgentClient extends LettaAgentClientBase {
10613
10998
  kind: "session",
10614
10999
  conversationId: id,
10615
11000
  options
10616
- });
11001
+ }, () => this.releaseIdleManagementConnection());
10617
11002
  }
10618
11003
  return createLocalAppServerSession(localOptions.appServer, {
10619
11004
  kind: "session",
10620
11005
  agentId: id,
10621
11006
  defaultConversation: true,
10622
11007
  options
10623
- });
11008
+ }, () => this.releaseIdleManagementConnection());
10624
11009
  }
10625
11010
  if (looksLikeConversationId2(id)) {
10626
11011
  return new Session({ ...sessionOptions, conversationId: id });
@@ -10888,4 +11273,4 @@ export {
10888
11273
  CloudManagedSandboxExpiredError
10889
11274
  };
10890
11275
 
10891
- //# debugId=D44861B7D6BE1C4C64756E2164756E21
11276
+ //# debugId=01C64A8CE3FD2BF964756E2164756E21