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

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.
@@ -682,7 +682,56 @@ function createAppServerClient(options) {
682
682
  return new AppServerClient(options);
683
683
  }
684
684
 
685
+ // src/request-ids.ts
686
+ var processRequestCounter = 0;
687
+ function createRequestIdGenerator() {
688
+ const nonce = Math.random().toString(36).slice(2, 10);
689
+ return (prefix = "req") => `${prefix}-${nonce}-${++processRequestCounter}`;
690
+ }
691
+ function applyUniqueRequestIds(client) {
692
+ client.nextRequestId = createRequestIdGenerator();
693
+ return client;
694
+ }
695
+
685
696
  // src/app-server-management.ts
697
+ var DEFAULT_IDLE_LINGER_MS = 250;
698
+ var registeredTransportsByUrl = new Map;
699
+ function appServerUrlKey(value) {
700
+ try {
701
+ const url = new URL(value);
702
+ if (url.protocol === "http:")
703
+ url.protocol = "ws:";
704
+ if (url.protocol === "https:")
705
+ url.protocol = "wss:";
706
+ url.hash = "";
707
+ url.searchParams.delete("channel");
708
+ return url.toString();
709
+ } catch {
710
+ return value;
711
+ }
712
+ }
713
+ function registerManagementTransport(url, transport) {
714
+ const key = appServerUrlKey(url);
715
+ const registration = { transport };
716
+ const transports = registeredTransportsByUrl.get(key) ?? new Set;
717
+ transports.add(registration);
718
+ registeredTransportsByUrl.set(key, transports);
719
+ return () => {
720
+ transports.delete(registration);
721
+ if (transports.size === 0) {
722
+ registeredTransportsByUrl.delete(key);
723
+ }
724
+ };
725
+ }
726
+ async function releaseAppServerManagementConnections(url) {
727
+ const key = appServerUrlKey(url);
728
+ while (true) {
729
+ const transports = [...registeredTransportsByUrl.get(key) ?? []];
730
+ if (transports.length === 0)
731
+ return;
732
+ await Promise.all(transports.map(({ transport }) => transport.releaseIdleConnection()));
733
+ }
734
+ }
686
735
  function ensureResponse(response, value, fallback) {
687
736
  if (!response.success || value == null) {
688
737
  throw new Error(response.error ?? fallback);
@@ -718,8 +767,16 @@ function stringRecord(value) {
718
767
 
719
768
  class AppServerManagementTransport {
720
769
  options;
770
+ connectionPromise = null;
771
+ releasePromise = null;
772
+ closingConnections = new Set;
773
+ inFlightRequests = 0;
774
+ idleWaiters = new Set;
775
+ idleTimer = null;
776
+ idleLingerMs;
721
777
  constructor(options) {
722
778
  this.options = options;
779
+ this.idleLingerMs = options.idleLingerMs ?? DEFAULT_IDLE_LINGER_MS;
723
780
  }
724
781
  async listAgents(query) {
725
782
  const response = await this.request("agent_list", { query }, "agent_list_response");
@@ -786,38 +843,206 @@ class AppServerManagementTransport {
786
843
  }
787
844
  return { messages: response.messages };
788
845
  }
846
+ releaseIdleConnection() {
847
+ this.clearIdleTimer();
848
+ if (this.releasePromise)
849
+ return this.releasePromise;
850
+ const release = this.releaseConnectionWhenIdle();
851
+ this.releasePromise = release;
852
+ release.then(() => {
853
+ if (this.releasePromise === release) {
854
+ this.releasePromise = null;
855
+ }
856
+ }, () => {
857
+ if (this.releasePromise === release) {
858
+ this.releasePromise = null;
859
+ }
860
+ });
861
+ return release;
862
+ }
789
863
  async request(type, body, responseType) {
790
- const ownedConnection = this.options.url ? null : await this.options.connect?.();
864
+ if (this.releasePromise) {
865
+ await this.releasePromise;
866
+ }
867
+ if (this.closingConnections.size > 0) {
868
+ await Promise.all([...this.closingConnections]);
869
+ }
870
+ this.clearIdleTimer();
871
+ this.inFlightRequests += 1;
872
+ try {
873
+ const { client } = await this.acquireConnection();
874
+ const command = {
875
+ type,
876
+ request_id: client.nextRequestId(type),
877
+ ...body
878
+ };
879
+ const response = await client.request(command, {
880
+ predicate: (message) => message.type === responseType
881
+ });
882
+ return response;
883
+ } finally {
884
+ this.inFlightRequests -= 1;
885
+ if (this.inFlightRequests === 0) {
886
+ const waiters = [...this.idleWaiters];
887
+ this.idleWaiters.clear();
888
+ for (const resolve of waiters)
889
+ resolve();
890
+ if (!this.releasePromise) {
891
+ this.scheduleIdleRelease();
892
+ }
893
+ }
894
+ }
895
+ }
896
+ async releaseConnectionWhenIdle() {
897
+ if (this.inFlightRequests > 0) {
898
+ await new Promise((resolve) => {
899
+ this.idleWaiters.add(resolve);
900
+ });
901
+ }
902
+ this.clearIdleTimer();
903
+ const promise = this.connectionPromise;
904
+ try {
905
+ if (promise) {
906
+ this.connectionPromise = null;
907
+ await this.trackClosingConnection(await promise);
908
+ }
909
+ if (this.closingConnections.size > 0) {
910
+ await Promise.all([...this.closingConnections]);
911
+ }
912
+ } catch {}
913
+ }
914
+ acquireConnection() {
915
+ if (this.connectionPromise)
916
+ return this.connectionPromise;
917
+ const promise = this.openConnection().then((connection) => {
918
+ connection.detachDisconnect = connection.client.onDisconnect(() => {
919
+ this.discardConnection(promise, connection);
920
+ });
921
+ return connection;
922
+ }, (error) => {
923
+ if (this.connectionPromise === promise) {
924
+ this.connectionPromise = null;
925
+ }
926
+ throw error;
927
+ });
928
+ this.connectionPromise = promise;
929
+ return promise;
930
+ }
931
+ async openConnection() {
932
+ const ownedConnection = this.options.url ? null : await this.options.connect?.() ?? null;
791
933
  const url = this.options.url ?? ownedConnection?.url;
792
934
  if (!url) {
793
935
  throw new Error("App-server management requires a url or connect hook.");
794
936
  }
937
+ const unregister = registerManagementTransport(url, this);
795
938
  let client = null;
796
939
  try {
797
- client = createAppServerClient({
940
+ client = applyUniqueRequestIds(createAppServerClient({
798
941
  url,
799
942
  ...this.options.authToken !== undefined ? { authToken: this.options.authToken } : {},
800
943
  ...this.options.WebSocket ? {
801
944
  WebSocket: this.options.WebSocket
802
945
  } : {},
803
946
  ...this.options.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.options.requestTimeoutMs } : {}
804
- });
947
+ }));
805
948
  await client.connect();
806
- const command = {
807
- type,
808
- request_id: client.nextRequestId(type),
809
- ...body
810
- };
811
- const response = await client.request(command, {
812
- predicate: (message) => message.type === responseType
813
- });
814
- return response;
815
- } finally {
816
- client?.close();
817
- ownedConnection?.close();
949
+ } catch (error) {
950
+ try {
951
+ if (client) {
952
+ const controlClosed = waitForSocketClose(client.control);
953
+ client.close();
954
+ ownedConnection?.close();
955
+ await controlClosed;
956
+ } else {
957
+ ownedConnection?.close();
958
+ }
959
+ } catch {} finally {
960
+ unregister();
961
+ }
962
+ throw error;
818
963
  }
964
+ return {
965
+ client,
966
+ ownedConnection,
967
+ detachDisconnect: () => {},
968
+ unregister
969
+ };
970
+ }
971
+ discardConnection(promise, connection) {
972
+ if (this.connectionPromise === promise) {
973
+ this.connectionPromise = null;
974
+ this.clearIdleTimer();
975
+ }
976
+ this.trackClosingConnection(connection);
977
+ }
978
+ trackClosingConnection(connection) {
979
+ const closing = closeConnection(connection);
980
+ this.closingConnections.add(closing);
981
+ closing.then(() => this.closingConnections.delete(closing), () => this.closingConnections.delete(closing));
982
+ return closing;
983
+ }
984
+ scheduleIdleRelease() {
985
+ if (!this.connectionPromise)
986
+ return;
987
+ this.clearIdleTimer();
988
+ const timer = setTimeout(() => {
989
+ this.idleTimer = null;
990
+ this.releaseIdleConnection();
991
+ }, this.idleLingerMs);
992
+ this.idleTimer = timer;
993
+ timer.unref?.();
994
+ }
995
+ clearIdleTimer() {
996
+ if (this.idleTimer === null)
997
+ return;
998
+ clearTimeout(this.idleTimer);
999
+ this.idleTimer = null;
1000
+ }
1001
+ }
1002
+ async function closeConnection(connection) {
1003
+ connection.detachDisconnect();
1004
+ const controlClosed = waitForSocketClose(connection.client.control);
1005
+ try {
1006
+ connection.client.close();
1007
+ connection.ownedConnection?.close();
1008
+ await controlClosed;
1009
+ } finally {
1010
+ connection.unregister();
819
1011
  }
820
1012
  }
1013
+ function waitForSocketClose(socket) {
1014
+ if (socket.readyState === 3)
1015
+ return Promise.resolve();
1016
+ return new Promise((resolve) => {
1017
+ let settled = false;
1018
+ let detach = () => {};
1019
+ const finish = () => {
1020
+ if (settled)
1021
+ return;
1022
+ settled = true;
1023
+ clearTimeout(timeout);
1024
+ detach();
1025
+ resolve();
1026
+ };
1027
+ if (socket.addEventListener) {
1028
+ socket.addEventListener("close", finish);
1029
+ detach = () => socket.removeEventListener?.("close", finish);
1030
+ } else if (socket.once) {
1031
+ socket.once("close", finish);
1032
+ detach = () => socket.off?.("close", finish);
1033
+ } else if (socket.on) {
1034
+ socket.on("close", finish);
1035
+ detach = () => socket.off?.("close", finish);
1036
+ } else {
1037
+ resolve();
1038
+ return;
1039
+ }
1040
+ const timeout = setTimeout(finish, 1000);
1041
+ timeout.unref?.();
1042
+ if (socket.readyState === 3)
1043
+ finish();
1044
+ });
1045
+ }
821
1046
 
822
1047
  // node_modules/@letta-ai/letta-code/dist/agent-presets.js
823
1048
  var LETTA_CODE_ORIGIN_TAG = "origin:letta-code";
@@ -5895,6 +6120,7 @@ class RemoteClientSessionCore {
5895
6120
  _modelSettings = null;
5896
6121
  label;
5897
6122
  requestTimeoutMs;
6123
+ initializePromise = null;
5898
6124
  streamQueue = [];
5899
6125
  streamResolvers = [];
5900
6126
  removeMessageHandler = null;
@@ -5912,43 +6138,72 @@ class RemoteClientSessionCore {
5912
6138
  this.requestTimeoutMs = config.requestTimeoutMs;
5913
6139
  }
5914
6140
  async initialize() {
6141
+ if (this.closed) {
6142
+ throw new Error("Session is closed");
6143
+ }
6144
+ if (this.initializePromise) {
6145
+ return this.initializePromise;
6146
+ }
5915
6147
  if (this.initialized) {
5916
6148
  throw new Error("Session already initialized");
5917
6149
  }
6150
+ const attempt = this.performInitialize();
6151
+ const memo = attempt.catch((error) => {
6152
+ this.cleanupFailedInitialize();
6153
+ throw error;
6154
+ }).finally(() => {
6155
+ if (this.initializePromise === memo) {
6156
+ this.initializePromise = null;
6157
+ }
6158
+ });
6159
+ this.initializePromise = memo;
6160
+ return memo;
6161
+ }
6162
+ async performInitialize() {
6163
+ const init = await this.initializeRuntimeController();
6164
+ this.controller = init.controller;
6165
+ this.runtime = init.runtime;
6166
+ this._agentId = init.runtime.agent_id;
6167
+ this._conversationId = init.runtime.conversation_id;
6168
+ this._sessionId = `${init.runtime.agent_id}:${init.runtime.conversation_id}`;
6169
+ this._modelSettings = init.modelSettings ?? null;
6170
+ this._model = typeof init.model === "string" ? init.model : typeof this._modelSettings?.model === "string" ? this._modelSettings.model : "";
6171
+ this.toolNames = init.tools;
6172
+ this.removeMessageHandler = this.controller.onMessage(this.handleProtocolMessage);
6173
+ await this.afterRuntimeInitialized();
6174
+ await this.applyPostInitializeOptions();
5918
6175
  if (this.closed) {
5919
6176
  throw new Error("Session is closed");
5920
6177
  }
5921
- try {
5922
- const init = await this.initializeRuntimeController();
5923
- this.controller = init.controller;
5924
- this.runtime = init.runtime;
5925
- this._agentId = init.runtime.agent_id;
5926
- this._conversationId = init.runtime.conversation_id;
5927
- this._sessionId = `${init.runtime.agent_id}:${init.runtime.conversation_id}`;
5928
- this._modelSettings = init.modelSettings ?? null;
5929
- this._model = typeof init.model === "string" ? init.model : typeof this._modelSettings?.model === "string" ? this._modelSettings.model : "";
5930
- this.toolNames = init.tools;
5931
- this.removeMessageHandler = this.controller.onMessage(this.handleProtocolMessage);
5932
- this.initialized = true;
5933
- await this.afterRuntimeInitialized();
5934
- await this.applyPostInitializeOptions();
5935
- const initMessage = {
5936
- type: "init",
5937
- agentId: init.runtime.agent_id,
5938
- sessionId: this._sessionId,
5939
- conversationId: init.runtime.conversation_id,
5940
- model: this._model
5941
- };
5942
- if (this.toolNames !== undefined)
5943
- initMessage.tools = this.toolNames;
5944
- if (init.skillSources !== undefined) {
5945
- initMessage.skillSources = init.skillSources;
5946
- }
5947
- return initMessage;
5948
- } catch (error) {
5949
- this.close();
5950
- throw error;
6178
+ this.initialized = true;
6179
+ const initMessage = {
6180
+ type: "init",
6181
+ agentId: init.runtime.agent_id,
6182
+ sessionId: this._sessionId,
6183
+ conversationId: init.runtime.conversation_id,
6184
+ model: this._model
6185
+ };
6186
+ if (this.toolNames !== undefined)
6187
+ initMessage.tools = this.toolNames;
6188
+ if (init.skillSources !== undefined) {
6189
+ initMessage.skillSources = init.skillSources;
5951
6190
  }
6191
+ return initMessage;
6192
+ }
6193
+ cleanupFailedInitialize() {
6194
+ this.removeMessageHandler?.();
6195
+ this.removeMessageHandler = null;
6196
+ this.controller?.close();
6197
+ this.controller = null;
6198
+ this.onCoreClose();
6199
+ this.runtime = null;
6200
+ this._agentId = null;
6201
+ this._conversationId = null;
6202
+ this._sessionId = null;
6203
+ this._modelSettings = null;
6204
+ this._model = "";
6205
+ this.toolNames = undefined;
6206
+ this.initialized = false;
5952
6207
  }
5953
6208
  async send(message) {
5954
6209
  if (!this.initialized) {
@@ -6045,6 +6300,12 @@ class RemoteClientSessionCore {
6045
6300
  if (!this.initialized) {
6046
6301
  await this.initialize();
6047
6302
  }
6303
+ if (!this.controller || !this.runtime) {
6304
+ throw new Error("Session is not initialized");
6305
+ }
6306
+ return this.applyModelUpdate(update);
6307
+ }
6308
+ async applyModelUpdate(update) {
6048
6309
  if (!this.controller || !this.runtime) {
6049
6310
  throw new Error("Session is not initialized");
6050
6311
  }
@@ -6461,7 +6722,7 @@ class RemoteClientSessionCore {
6461
6722
  if (this.mode.kind !== "session")
6462
6723
  return;
6463
6724
  if (this.mode.options.model !== undefined || this.mode.options.reasoningEffort !== undefined) {
6464
- await this.updateModel({
6725
+ await this.applyModelUpdate({
6465
6726
  ...this.mode.options.model !== undefined ? { model: this.mode.options.model } : {},
6466
6727
  ...this.mode.options.reasoningEffort !== undefined ? { reasoningEffort: this.mode.options.reasoningEffort } : {}
6467
6728
  });
@@ -7224,13 +7485,15 @@ class AppServerSession extends RemoteClientSessionCore {
7224
7485
  return options.memfs !== false;
7225
7486
  }
7226
7487
  async initializeRuntimeController() {
7488
+ await this.remoteOptions.beforeConnect?.();
7227
7489
  const url = await this.resolveAppServerUrl();
7228
- const client = createAppServerClient({
7490
+ await releaseAppServerManagementConnections(url);
7491
+ const client = applyUniqueRequestIds(createAppServerClient({
7229
7492
  url,
7230
7493
  ...this.remoteOptions.authToken !== undefined ? { authToken: this.remoteOptions.authToken } : {},
7231
7494
  ...this.remoteOptions.WebSocket ? { WebSocket: this.remoteOptions.WebSocket } : {},
7232
7495
  ...this.remoteOptions.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.remoteOptions.requestTimeoutMs } : {}
7233
- });
7496
+ }));
7234
7497
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
7235
7498
  client,
7236
7499
  getRuntime: () => this.runtime,
@@ -8101,14 +8364,14 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
8101
8364
  apiKey,
8102
8365
  authMode: this.cloudOptions.webSocketAuth ?? "header"
8103
8366
  });
8104
- const client = createAppServerClient({
8367
+ const client = applyUniqueRequestIds(createAppServerClient({
8105
8368
  url,
8106
8369
  WebSocket: createCloudStatusWebSocketConstructor({
8107
8370
  cloudOptions: this.cloudOptions,
8108
8371
  runtime: resolved.runtime
8109
8372
  }),
8110
8373
  requestTimeoutMs: this.cloudOptions.requestTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS
8111
- });
8374
+ }));
8112
8375
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
8113
8376
  client,
8114
8377
  getRuntime: () => this.runtime,
@@ -8846,6 +9109,10 @@ class LettaAgentClientBase {
8846
9109
  if (requestTimeoutMs !== undefined && (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0)) {
8847
9110
  throw new Error("Invalid appServer.requestTimeoutMs. Expected a positive integer.");
8848
9111
  }
9112
+ const idleLingerMs = localOptions.appServer?.idleLingerMs;
9113
+ if (idleLingerMs !== undefined && (!Number.isInteger(idleLingerMs) || idleLingerMs < 0)) {
9114
+ throw new Error("Invalid appServer.idleLingerMs. Expected a non-negative integer.");
9115
+ }
8849
9116
  const startupTimeoutMs = localOptions.appServer?.startupTimeoutMs;
8850
9117
  if (startupTimeoutMs !== undefined && (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs <= 0)) {
8851
9118
  throw new Error("Invalid appServer.startupTimeoutMs. Expected a positive integer.");
@@ -8858,6 +9125,9 @@ class LettaAgentClientBase {
8858
9125
  if (options.requestTimeoutMs !== undefined && (!Number.isInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
8859
9126
  throw new Error("Invalid requestTimeoutMs. Expected a positive integer.");
8860
9127
  }
9128
+ if (options.idleLingerMs !== undefined && (!Number.isInteger(options.idleLingerMs) || options.idleLingerMs < 0)) {
9129
+ throw new Error("Invalid idleLingerMs. Expected a non-negative integer.");
9130
+ }
8861
9131
  }
8862
9132
  if (this.backend === "cloud") {
8863
9133
  validateCloudClientOptions(options);
@@ -8872,7 +9142,7 @@ class LettaAgentClientBase {
8872
9142
  }
8873
9143
  validateCreateAgentOptions(options);
8874
9144
  if (this.backend === "remote") {
8875
- const session = new AppServerSession(this.remoteOptions(), {
9145
+ const session = new AppServerSession(this.appServerSessionOptions(), {
8876
9146
  kind: "create-agent",
8877
9147
  options
8878
9148
  });
@@ -8895,7 +9165,7 @@ class LettaAgentClientBase {
8895
9165
  if (!agentId) {
8896
9166
  throw new Error("App-server createSession() requires an agent id. Call createAgent() first or pass an agent id.");
8897
9167
  }
8898
- return new AppServerSession(this.remoteOptions(), {
9168
+ return new AppServerSession(this.appServerSessionOptions(), {
8899
9169
  kind: "session",
8900
9170
  agentId,
8901
9171
  newConversation: true,
@@ -8921,13 +9191,13 @@ class LettaAgentClientBase {
8921
9191
  validateCreateSessionOptions(sessionOptions);
8922
9192
  if (this.backend === "remote") {
8923
9193
  if (looksLikeConversationId(id)) {
8924
- return new AppServerSession(this.remoteOptions(), {
9194
+ return new AppServerSession(this.appServerSessionOptions(), {
8925
9195
  kind: "session",
8926
9196
  conversationId: id,
8927
9197
  options
8928
9198
  });
8929
9199
  }
8930
- return new AppServerSession(this.remoteOptions(), {
9200
+ return new AppServerSession(this.appServerSessionOptions(), {
8931
9201
  kind: "session",
8932
9202
  agentId: id,
8933
9203
  defaultConversation: true,
@@ -9026,6 +9296,12 @@ class LettaAgentClientBase {
9026
9296
  }
9027
9297
  return this.options;
9028
9298
  }
9299
+ appServerSessionOptions() {
9300
+ return {
9301
+ ...this.remoteOptions(),
9302
+ beforeConnect: () => this.releaseIdleManagementConnection()
9303
+ };
9304
+ }
9029
9305
  getRepositoriesClient() {
9030
9306
  if (this.backend !== "cloud") {
9031
9307
  throw new Error('client.repositories is only available with backend: "cloud".');
@@ -9033,6 +9309,9 @@ class LettaAgentClientBase {
9033
9309
  this.repositoriesClient ??= new RepositoriesClient(this.cloudOptions());
9034
9310
  return this.repositoriesClient;
9035
9311
  }
9312
+ async releaseIdleManagementConnection() {
9313
+ await this.managementTransport?.releaseIdleConnection?.();
9314
+ }
9036
9315
  getManagementTransport() {
9037
9316
  if (this.managementTransport)
9038
9317
  return this.managementTransport;
@@ -9140,4 +9419,4 @@ export {
9140
9419
  CloudManagedSandboxExpiredError
9141
9420
  };
9142
9421
 
9143
- //# debugId=071CEF1290505F1D64756E2164756E21
9422
+ //# debugId=EACEDA2AA17A075B64756E2164756E21