@letta-ai/letta-agent-sdk 0.8.73 → 0.8.75

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 (47) hide show
  1. package/README.md +69 -0
  2. package/dist/app-server-session.d.ts +28 -7
  3. package/dist/app-server-session.d.ts.map +1 -1
  4. package/dist/client-base.d.ts +17 -0
  5. package/dist/client-base.d.ts.map +1 -1
  6. package/dist/client-entry.d.ts +1 -0
  7. package/dist/client-entry.d.ts.map +1 -1
  8. package/dist/client-entry.js +488 -100
  9. package/dist/client-entry.js.map +16 -13
  10. package/dist/client.d.ts +9 -0
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/cloud-session.d.ts.map +1 -1
  13. package/dist/cloud-status-transport.d.ts.map +1 -1
  14. package/dist/external-tools.d.ts +11 -0
  15. package/dist/external-tools.d.ts.map +1 -0
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +517 -126
  19. package/dist/index.js.map +18 -15
  20. package/dist/remote-client-session-core.d.ts.map +1 -1
  21. package/dist/remote-session-protocol.d.ts +3 -1
  22. package/dist/remote-session-protocol.d.ts.map +1 -1
  23. package/dist/remote-turn-coordinator.d.ts +14 -0
  24. package/dist/remote-turn-coordinator.d.ts.map +1 -1
  25. package/dist/runtime-tool-policy.d.ts +57 -0
  26. package/dist/runtime-tool-policy.d.ts.map +1 -0
  27. package/dist/structured-output-session.d.ts.map +1 -1
  28. package/dist/tool-registration.d.ts +67 -0
  29. package/dist/tool-registration.d.ts.map +1 -0
  30. package/dist/types.d.ts +30 -2
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +3 -3
  33. package/src/app-server-session.ts +104 -60
  34. package/src/client-base.ts +57 -14
  35. package/src/client-entry.ts +1 -0
  36. package/src/client.ts +6 -2
  37. package/src/cloud-session.ts +12 -3
  38. package/src/cloud-status-transport.ts +4 -0
  39. package/src/external-tools.ts +69 -0
  40. package/src/index.ts +3 -0
  41. package/src/remote-client-session-core.ts +4 -4
  42. package/src/remote-session-protocol.ts +3 -0
  43. package/src/remote-turn-coordinator.ts +80 -15
  44. package/src/runtime-tool-policy.ts +114 -0
  45. package/src/structured-output-session.ts +2 -1
  46. package/src/tool-registration.ts +311 -0
  47. package/src/types.ts +39 -1
@@ -10143,14 +10143,6 @@ function buildCreatedAgentTags(options = {}) {
10143
10143
  }
10144
10144
  return Array.from(new Set(tags));
10145
10145
  }
10146
- var DEFAULT_SUMMARIZATION_MODEL = "letta/auto";
10147
- var SYSTEM_REMINDER_TAG = "system-reminder";
10148
- var SYSTEM_REMINDER_OPEN = `<${SYSTEM_REMINDER_TAG}>`;
10149
- var SYSTEM_REMINDER_CLOSE = `</${SYSTEM_REMINDER_TAG}>`;
10150
- var SYSTEM_ALERT_TAG = "system-alert";
10151
- var SYSTEM_ALERT_OPEN = `<${SYSTEM_ALERT_TAG}>`;
10152
- var SYSTEM_ALERT_CLOSE = `</${SYSTEM_ALERT_TAG}>`;
10153
- var ELAPSED_DISPLAY_THRESHOLD_MS = 60 * 1000;
10154
10146
  var READ_ONLY_BLOCK_LABELS = ["memory_filesystem"];
10155
10147
  var human_default = `---
10156
10148
  label: human
@@ -12190,7 +12182,6 @@ async function getDefaultMemoryBlocks() {
12190
12182
  }
12191
12183
  var models = [];
12192
12184
  var BUILTIN_MODEL_ALIASES = new Map([
12193
- ["auto", "letta/auto"],
12194
12185
  ["auto-chat", "letta/auto-chat"],
12195
12186
  ["auto-fast", "letta/auto-fast"]
12196
12187
  ]);
@@ -12239,20 +12230,14 @@ function resolveModel(modelIdentifier) {
12239
12230
  return builtinHandle;
12240
12231
  return modelIdentifier.includes("/") ? modelIdentifier : null;
12241
12232
  }
12233
+ var UNSELECTED_MODEL_HANDLE = "local/default";
12234
+ function isLettaHostedModelHandle(handle) {
12235
+ return handle.startsWith("letta/");
12236
+ }
12242
12237
  function getDefaultModel() {
12243
- if (models.length === 0)
12244
- return "letta/auto";
12245
- const autoModel = models.find((model) => model.id === "auto");
12246
- if (autoModel)
12247
- return autoModel.handle;
12248
- const defaultModel = models.find((model) => model.isDefault);
12249
- if (defaultModel)
12250
- return defaultModel.handle;
12251
- const firstModel = models[0];
12252
- if (!firstModel) {
12253
- throw new Error("Model catalog is unavailable.");
12254
- }
12255
- return firstModel.handle;
12238
+ const candidates = models.filter((model) => !isLettaHostedModelHandle(model.handle));
12239
+ const defaultModel = candidates.find((model) => model.isDefault) ?? candidates[0];
12240
+ return defaultModel?.handle ?? UNSELECTED_MODEL_HANDLE;
12256
12241
  }
12257
12242
  var PERSONALITY_OPTIONS = [
12258
12243
  {
@@ -12514,7 +12499,7 @@ async function buildCreateAgentRequest(options = {}) {
12514
12499
  initial_message_sequence: [],
12515
12500
  parallel_tool_calls: options.parallelToolCalls ?? true,
12516
12501
  compaction_settings: {
12517
- model: options.compactionModel ?? DEFAULT_SUMMARIZATION_MODEL
12502
+ model: options.compactionModel ?? modelHandle
12518
12503
  },
12519
12504
  ...options.embedding !== undefined ? { embedding: options.embedding } : {},
12520
12505
  ...options.isSubagent ? { hidden: true } : options.hidden !== undefined ? { hidden: options.hidden } : {}
@@ -13128,7 +13113,7 @@ async function* streamStructuredTurns(config) {
13128
13113
  return;
13129
13114
  });
13130
13115
  }
13131
- if (msg.type !== "result") {
13116
+ if (msg.type !== "result" || msg.origin !== undefined) {
13132
13117
  yield msg;
13133
13118
  continue;
13134
13119
  }
@@ -13245,9 +13230,115 @@ function expandMcpToolWildcards(allowedTools, mcpTools) {
13245
13230
  return [...new Set(expanded)];
13246
13231
  }
13247
13232
 
13233
+ // src/external-tools.ts
13234
+ function externalToolGroups(tools) {
13235
+ if (!tools || tools.length === 0)
13236
+ return;
13237
+ return [
13238
+ {
13239
+ tools: tools.map((tool) => ({
13240
+ name: tool.name,
13241
+ label: tool.label,
13242
+ description: tool.description,
13243
+ parameters: tool.parameters
13244
+ }))
13245
+ }
13246
+ ];
13247
+ }
13248
+ var MAX_REMEMBERED_EXTERNAL_TOOL_CALLS = 200;
13249
+ function createExternalToolCallHandler(externalTools, fallbackRuntime) {
13250
+ const calls = new Map;
13251
+ const execute = async (request) => {
13252
+ const tool = externalTools.get(request.tool_name);
13253
+ if (!tool) {
13254
+ throw new Error(`Unknown external tool: ${request.tool_name}`);
13255
+ }
13256
+ const runtime = request.runtime ?? fallbackRuntime?.() ?? null;
13257
+ const context = runtime ? { toolCallId: request.tool_call_id, agentId: runtime.agent_id, conversationId: runtime.conversation_id } : undefined;
13258
+ const result = await tool.execute(request.tool_call_id, request.input, undefined, undefined, context);
13259
+ return {
13260
+ content: result.content.map((part) => ({
13261
+ type: part.type,
13262
+ ...part.text !== undefined ? { text: part.text } : {},
13263
+ ...part.data !== undefined ? { data: part.data } : {},
13264
+ ...part.mimeType !== undefined ? { mimeType: part.mimeType } : {}
13265
+ })),
13266
+ ...result.isError === true ? { is_error: true } : {}
13267
+ };
13268
+ };
13269
+ return (request) => {
13270
+ const requestId = request.request_id;
13271
+ if (typeof requestId !== "string" || requestId.length === 0) {
13272
+ return execute(request);
13273
+ }
13274
+ const existing = calls.get(requestId);
13275
+ if (existing)
13276
+ return existing;
13277
+ const call = execute(request);
13278
+ calls.set(requestId, call);
13279
+ while (calls.size > MAX_REMEMBERED_EXTERNAL_TOOL_CALLS) {
13280
+ const oldest = calls.keys().next().value;
13281
+ if (oldest === undefined)
13282
+ break;
13283
+ calls.delete(oldest);
13284
+ }
13285
+ return call;
13286
+ };
13287
+ }
13288
+
13289
+ // src/runtime-tool-policy.ts
13290
+ var RUNTIME_TOOL_POLICY_CAPABILITY = "runtime_tool_policy";
13291
+ var CAPABILITY_PROBE_TIMEOUT_MS = 5000;
13292
+ function clientToolsetPayload(toolset) {
13293
+ return {
13294
+ ...toolset.base !== undefined ? { base: toolset.base } : {},
13295
+ ...toolset.include !== undefined ? { include: [...new Set(toolset.include)] } : {}
13296
+ };
13297
+ }
13298
+ function runtimeToolPolicy(allowedTools, toolset) {
13299
+ return {
13300
+ ...toolset !== undefined ? { toolset: clientToolsetPayload(toolset) } : {},
13301
+ ...allowedTools !== undefined ? { allowlist: [...new Set(allowedTools)] } : {},
13302
+ exclude_interactive: true
13303
+ };
13304
+ }
13305
+ async function supportsRuntimeToolPolicy(client) {
13306
+ try {
13307
+ const info = await client.info({ timeoutMs: CAPABILITY_PROBE_TIMEOUT_MS });
13308
+ const capabilities = info.capabilities;
13309
+ return capabilities?.[RUNTIME_TOOL_POLICY_CAPABILITY] === true;
13310
+ } catch {
13311
+ return false;
13312
+ }
13313
+ }
13314
+ function mergeInheritedTools(inherited, own) {
13315
+ if (!inherited)
13316
+ return own;
13317
+ const tools = new Map(inherited.tools.map((tool) => [tool.name, tool]));
13318
+ for (const tool of own.tools)
13319
+ tools.set(tool.name, tool);
13320
+ const ownNames = new Set(own.tools.map((tool) => tool.name));
13321
+ const registered = inherited.allowedTools;
13322
+ const allowedTools = registered === undefined ? own.allowedTools : own.allowedTools === undefined ? [...new Set([...registered, ...ownNames])] : own.allowedTools.filter((name) => registered.includes(name) || ownNames.has(name));
13323
+ const toolset = own.toolset ?? inherited.toolset;
13324
+ return {
13325
+ tools: [...tools.values()],
13326
+ ...allowedTools !== undefined ? { allowedTools } : {},
13327
+ ...toolset !== undefined ? { toolset } : {}
13328
+ };
13329
+ }
13330
+
13248
13331
  // src/remote-turn-coordinator.ts
13249
13332
  var MAX_RECENTLY_SETTLED_RUN_IDS = 256;
13250
13333
  var TRAILING_USAGE_GRACE_MS = 100;
13334
+ var TURN_ORIGINS = new Set([
13335
+ "task_notification",
13336
+ "cron",
13337
+ "subagent",
13338
+ "system",
13339
+ "channel",
13340
+ "user"
13341
+ ]);
13251
13342
 
13252
13343
  class RemoteTurnCoordinator {
13253
13344
  label;
@@ -13259,6 +13350,8 @@ class RemoteTurnCoordinator {
13259
13350
  activeTurn = null;
13260
13351
  pendingTurns = [];
13261
13352
  settledRunIds = new Set;
13353
+ queueOrigins = new Map;
13354
+ nextServerOrigin = "unknown";
13262
13355
  nextTurnId = 0;
13263
13356
  messageCounter = 0;
13264
13357
  clientMessageCounter = 0;
@@ -13274,16 +13367,27 @@ class RemoteTurnCoordinator {
13274
13367
  return this._activeTurnStartedAt;
13275
13368
  }
13276
13369
  hasInFlightTurn() {
13277
- return this.activeTurn !== null || this.pendingTurns.length > 0;
13370
+ return this.activeTurn !== null && this.activeTurn.origin === undefined || this.pendingTurns.length > 0;
13278
13371
  }
13279
13372
  trackSentTurn(runtime, callerOtid, outputFormat) {
13280
13373
  const otid = normalizeCallerOtid(callerOtid);
13281
- const turn = {
13282
- id: ++this.nextTurnId,
13283
- runtime,
13374
+ const turn = this.newTracker(runtime, {
13284
13375
  ...otid !== undefined ? { otid } : {},
13285
13376
  ...outputFormat !== undefined ? { outputFormat } : {},
13286
- clientMessageId: otid ?? `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
13377
+ clientMessageId: otid ?? `sdk-message-${Date.now()}-${++this.clientMessageCounter}`
13378
+ });
13379
+ if (this.activeTurn) {
13380
+ this.pendingTurns.push(turn);
13381
+ } else {
13382
+ this.activateTurn(turn);
13383
+ }
13384
+ return turn;
13385
+ }
13386
+ newTracker(runtime, identity) {
13387
+ return {
13388
+ id: ++this.nextTurnId,
13389
+ runtime,
13390
+ ...identity,
13287
13391
  queuedAt: Date.now(),
13288
13392
  startedAt: 0,
13289
13393
  assistantText: "",
@@ -13300,13 +13404,32 @@ class RemoteTurnCoordinator {
13300
13404
  abortRequested: false,
13301
13405
  timeout: null
13302
13406
  };
13303
- if (this.activeTurn) {
13304
- this.pendingTurns.push(turn);
13305
- } else {
13306
- this.activateTurn(turn);
13307
- }
13407
+ }
13408
+ startServerTurn(runtime) {
13409
+ const turn = this.newTracker(runtime, { clientMessageId: "", origin: this.nextServerOrigin });
13410
+ this.nextServerOrigin = "unknown";
13411
+ this.activateTurn(turn);
13308
13412
  return turn;
13309
13413
  }
13414
+ observeQueueOrigins(message, queue) {
13415
+ for (const item of queue) {
13416
+ if (!item.clientMessageId)
13417
+ continue;
13418
+ this.queueOrigins.set(item.clientMessageId, TURN_ORIGINS.has(item.source) ? item.source : "unknown");
13419
+ if (this.queueOrigins.size > MAX_RECENTLY_SETTLED_RUN_IDS) {
13420
+ this.queueOrigins.delete(this.queueOrigins.keys().next().value);
13421
+ }
13422
+ }
13423
+ for (const removed of Array.isArray(message.removed) ? message.removed : []) {
13424
+ const { client_message_id: id, disposition, source } = removed;
13425
+ if (disposition !== "dequeued" || typeof id !== "string")
13426
+ continue;
13427
+ const own = [this.activeTurn, ...this.pendingTurns].some((turn) => turn?.clientMessageId === id);
13428
+ if (own)
13429
+ continue;
13430
+ this.nextServerOrigin = typeof source === "string" && TURN_ORIGINS.has(source) ? source : this.queueOrigins.get(id) ?? "unknown";
13431
+ }
13432
+ }
13310
13433
  removeTrackedTurn(turn) {
13311
13434
  if (turn.timeout)
13312
13435
  clearTimeout(turn.timeout);
@@ -13320,6 +13443,12 @@ class RemoteTurnCoordinator {
13320
13443
  if (index !== -1)
13321
13444
  this.pendingTurns.splice(index, 1);
13322
13445
  }
13446
+ failServerTurn(detail) {
13447
+ const active = this.activeTurn;
13448
+ if (active?.origin === undefined)
13449
+ return;
13450
+ this.failTurn(active, detail, { errorCode: "stream_closed", recoverable: true });
13451
+ }
13323
13452
  markAbortRequested() {
13324
13453
  if (this.activeTurn)
13325
13454
  this.activeTurn.abortRequested = true;
@@ -13339,6 +13468,7 @@ class RemoteTurnCoordinator {
13339
13468
  type: "queue_update",
13340
13469
  queue: queueItems(message)
13341
13470
  };
13471
+ this.observeQueueOrigins(message, sdkMessage2.queue);
13342
13472
  this.enqueue(sdkMessage2);
13343
13473
  return;
13344
13474
  }
@@ -13381,9 +13511,9 @@ class RemoteTurnCoordinator {
13381
13511
  if (!delta)
13382
13512
  return;
13383
13513
  const messageType = streamDeltaMessageType(delta);
13384
- const active = messageType === "usage_statistics" || messageType === "stop_reason" ? this.activeTurn : this.activateNextTurnFromProtocol();
13385
13514
  const runId = streamDeltaRunId(delta);
13386
13515
  const isTurnEvidence = messageType === "assistant_message" || messageType === "reasoning_message" || messageType === "tool_call_message" || messageType === "approval_request_message" || messageType === "tool_return_message" || messageType === "error_message" || messageType === "loop_error" || runId !== undefined && (messageType === "usage_statistics" || messageType === "stop_reason");
13516
+ const active = messageType === "usage_statistics" || messageType === "stop_reason" ? this.activeTurn : this.activateNextTurnFromProtocol() ?? (isTurnEvidence && runId !== undefined ? this.startServerTurn(runtime) : null);
13387
13517
  if (active && isTurnEvidence) {
13388
13518
  active.observedTurnEvidence = true;
13389
13519
  if (runId)
@@ -13841,7 +13971,8 @@ class RemoteTurnCoordinator {
13841
13971
  stopReason,
13842
13972
  durationMs: Date.now() - (tracker?.startedAt || this._activeTurnStartedAt),
13843
13973
  conversationId: turn.runtime.conversation_id,
13844
- runIds: turn.runIds.length > 0 ? turn.runIds : undefined
13974
+ runIds: turn.runIds.length > 0 ? turn.runIds : undefined,
13975
+ ...tracker?.origin !== undefined ? { origin: tracker.origin } : {}
13845
13976
  };
13846
13977
  }
13847
13978
  enqueue(message) {
@@ -14083,9 +14214,8 @@ class RemoteClientSessionCore {
14083
14214
  }
14084
14215
  await this.send(message);
14085
14216
  for await (const msg of this.stream()) {
14086
- if (msg.type === "result") {
14217
+ if (msg.type === "result" && msg.origin === undefined)
14087
14218
  return msg;
14088
- }
14089
14219
  }
14090
14220
  return {
14091
14221
  type: "result",
@@ -14121,7 +14251,7 @@ class RemoteClientSessionCore {
14121
14251
  if (!msg)
14122
14252
  break;
14123
14253
  yield msg;
14124
- if (msg.type === "result")
14254
+ if (msg.type === "result" && msg.origin === undefined)
14125
14255
  break;
14126
14256
  }
14127
14257
  }
@@ -14431,6 +14561,7 @@ class RemoteClientSessionCore {
14431
14561
  this.controller?.close();
14432
14562
  this.controller = null;
14433
14563
  const error = new Error(`${this.label} connection closed before send`);
14564
+ this.turns.failServerTurn(error.message);
14434
14565
  for (const cancel of [...this.deviceStatusRefreshCancels])
14435
14566
  cancel(error);
14436
14567
  this.deviceStatusRefreshCancels.clear();
@@ -14571,38 +14702,6 @@ function agentToolNames(agent) {
14571
14702
  return typeof name === "string" && name.length > 0 ? [name] : [];
14572
14703
  });
14573
14704
  }
14574
- function externalToolGroups(tools) {
14575
- if (!tools || tools.length === 0)
14576
- return;
14577
- return [
14578
- {
14579
- tools: tools.map((tool) => ({
14580
- name: tool.name,
14581
- label: tool.label,
14582
- description: tool.description,
14583
- parameters: tool.parameters
14584
- }))
14585
- }
14586
- ];
14587
- }
14588
- function createExternalToolCallHandler(externalTools) {
14589
- return async (request) => {
14590
- const tool = externalTools.get(request.tool_name);
14591
- if (!tool) {
14592
- throw new Error(`Unknown external tool: ${request.tool_name}`);
14593
- }
14594
- const result = await tool.execute(request.tool_call_id, request.input);
14595
- return {
14596
- content: result.content.map((part) => ({
14597
- type: part.type,
14598
- ...part.text !== undefined ? { text: part.text } : {},
14599
- ...part.data !== undefined ? { data: part.data } : {},
14600
- ...part.mimeType !== undefined ? { mimeType: part.mimeType } : {}
14601
- })),
14602
- ...result.isError === true ? { is_error: true } : {}
14603
- };
14604
- };
14605
- }
14606
14705
  async function resolveAppServerToolApproval(options, toolName, toolInput, context) {
14607
14706
  if ("outputFormat" in options && options.outputFormat !== undefined && toolName === STRUCTURED_OUTPUT_TOOL) {
14608
14707
  return { behavior: "allow", updatedInput: null, updatedPermissions: [] };
@@ -14779,11 +14878,26 @@ class AppServerRuntimeController {
14779
14878
  options;
14780
14879
  clientToolAllowlist;
14781
14880
  clientToolset;
14782
- constructor(client, options, clientToolAllowlist, clientToolset) {
14881
+ toolPolicyApplied;
14882
+ constructor(client, options, clientToolAllowlist, clientToolset, toolPolicyApplied = false) {
14783
14883
  this.client = client;
14784
14884
  this.options = options;
14785
14885
  this.clientToolAllowlist = clientToolAllowlist;
14786
14886
  this.clientToolset = clientToolset;
14887
+ this.toolPolicyApplied = toolPolicyApplied;
14888
+ }
14889
+ async updateToolSelection(runtime, selection) {
14890
+ this.clientToolAllowlist = selection.allowedTools;
14891
+ this.clientToolset = selection.toolset;
14892
+ const response = await this.client.runtimeExternalToolsUpdate({
14893
+ updates: [{
14894
+ runtimes: [runtime],
14895
+ external_tools: externalToolGroups(selection.tools) ?? [],
14896
+ ...this.toolPolicyApplied ? { tool_policy: runtimeToolPolicy(selection.allowedTools, selection.toolset) } : {}
14897
+ }]
14898
+ });
14899
+ if (!response.success)
14900
+ throw new Error(response.error ?? "Failed to update session tools");
14787
14901
  }
14788
14902
  onMessage(handler) {
14789
14903
  return this.client.onMessage((message, channel) => {
@@ -14805,14 +14919,11 @@ class AppServerRuntimeController {
14805
14919
  }
14806
14920
  ]
14807
14921
  };
14808
- if (this.clientToolAllowlist !== undefined) {
14922
+ if (!this.toolPolicyApplied && this.clientToolAllowlist !== undefined) {
14809
14923
  payload.client_tool_allowlist = [...new Set(this.clientToolAllowlist)];
14810
14924
  }
14811
- if (this.clientToolset !== undefined) {
14812
- payload.client_toolset = {
14813
- ...this.clientToolset.base !== undefined ? { base: this.clientToolset.base } : {},
14814
- ...this.clientToolset.include !== undefined ? { include: [...new Set(this.clientToolset.include)] } : {}
14815
- };
14925
+ if (!this.toolPolicyApplied && this.clientToolset !== undefined) {
14926
+ payload.client_toolset = clientToolsetPayload(this.clientToolset);
14816
14927
  }
14817
14928
  if (options.outputFormat !== undefined) {
14818
14929
  payload.response_format = {
@@ -14824,7 +14935,8 @@ class AppServerRuntimeController {
14824
14935
  }
14825
14936
  };
14826
14937
  }
14827
- payload.exclude_interactive_tools = true;
14938
+ if (!this.toolPolicyApplied)
14939
+ payload.exclude_interactive_tools = true;
14828
14940
  this.client.input({
14829
14941
  runtime,
14830
14942
  payload
@@ -14916,6 +15028,9 @@ class AppServerSession extends RemoteClientSessionCore {
14916
15028
  mcpCleanup = Promise.resolve();
14917
15029
  removeExternalToolHandler = null;
14918
15030
  removeControlRequestHandler = null;
15031
+ inheritedTools = null;
15032
+ removeInheritedListener = null;
15033
+ ownSelection = null;
14919
15034
  constructor(remoteOptions, mode) {
14920
15035
  super(mode, {
14921
15036
  label: "app-server",
@@ -14927,6 +15042,27 @@ class AppServerSession extends RemoteClientSessionCore {
14927
15042
  this.externalTools.set(tool.name, tool);
14928
15043
  }
14929
15044
  }
15045
+ inheritTools(source) {
15046
+ this.inheritedTools = source;
15047
+ this.removeInheritedListener = source.onChange(() => {
15048
+ this.refreshInheritedTools().catch(() => {
15049
+ return;
15050
+ });
15051
+ });
15052
+ }
15053
+ applyToolSelection() {
15054
+ const selection = mergeInheritedTools(this.inheritedTools?.current() ?? null, this.ownSelection ?? { tools: [] });
15055
+ this.externalTools.clear();
15056
+ for (const tool of selection.tools)
15057
+ this.externalTools.set(tool.name, tool);
15058
+ return selection;
15059
+ }
15060
+ async refreshInheritedTools() {
15061
+ if (!this.ownSelection || !this.runtime || !(this.controller instanceof AppServerRuntimeController))
15062
+ return;
15063
+ const selection = this.applyToolSelection();
15064
+ await this.controller.updateToolSelection(this.runtime, selection);
15065
+ }
14930
15066
  shouldEnableMemfs(options) {
14931
15067
  if (this.mode.kind !== "create-agent")
14932
15068
  return false;
@@ -14942,6 +15078,9 @@ class AppServerSession extends RemoteClientSessionCore {
14942
15078
  ...this.remoteOptions.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.remoteOptions.requestTimeoutMs } : {}
14943
15079
  }));
14944
15080
  const options = this.currentOptions();
15081
+ this.externalTools.clear();
15082
+ for (const tool of this.mode.options.tools ?? [])
15083
+ this.externalTools.set(tool.name, tool);
14945
15084
  this.mcpBridge = await connectMcpServers("mcpServers" in options ? options.mcpServers : undefined, {
14946
15085
  cwd: options.cwd,
14947
15086
  reservedToolNames: this.externalTools.keys()
@@ -14949,13 +15088,22 @@ class AppServerSession extends RemoteClientSessionCore {
14949
15088
  for (const tool of this.mcpBridge.tools) {
14950
15089
  this.externalTools.set(tool.name, tool);
14951
15090
  }
15091
+ const mcpToolNames = this.mcpBridge.tools.map((tool) => tool.name);
15092
+ const ownToolset = "toolset" in options ? options.toolset : undefined;
15093
+ const ownAllowedTools = expandMcpToolWildcards(options.allowedTools, mcpToolNames);
15094
+ this.ownSelection = {
15095
+ tools: [...this.externalTools.values()],
15096
+ ...ownAllowedTools !== undefined ? { allowedTools: ownAllowedTools } : {},
15097
+ ...ownToolset !== undefined ? { toolset: ownToolset } : {}
15098
+ };
15099
+ const { allowedTools, toolset: clientToolset } = this.applyToolSelection();
14952
15100
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
14953
15101
  client,
14954
15102
  getRuntime: () => this.runtime,
14955
15103
  getOptions: () => this.currentOptions()
14956
15104
  });
14957
- if (this.externalTools.size > 0) {
14958
- this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools));
15105
+ if (this.externalTools.size > 0 || this.inheritedTools) {
15106
+ this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools, () => this.runtime));
14959
15107
  }
14960
15108
  try {
14961
15109
  await client.connect();
@@ -14966,19 +15114,17 @@ class AppServerSession extends RemoteClientSessionCore {
14966
15114
  throw new Error(`Local query() requires a Letta Code app-server with local conversation support (connected version: ${info.letta_code_version}). Upgrade @letta-ai/letta-code.`);
14967
15115
  }
14968
15116
  }
14969
- const response = await this.startRuntime(client);
15117
+ const response = await this.startRuntime(client, runtimeToolPolicy(allowedTools, clientToolset));
14970
15118
  if (!response.success || !response.runtime) {
14971
15119
  throw new Error(response.error ?? "Failed to start app-server runtime");
14972
15120
  }
14973
15121
  this.watchTransportDisconnect(client);
15122
+ const toolPolicyApplied = await supportsRuntimeToolPolicy(client);
14974
15123
  const tools = agentToolNames(response.agent);
14975
15124
  const skillSources = options.skillSources;
14976
- const clientToolset = "toolset" in options ? options.toolset : undefined;
14977
- const mcpToolNames = this.mcpBridge?.tools.map((tool) => tool.name) ?? [];
14978
- const allowedTools = expandMcpToolWildcards(options.allowedTools, mcpToolNames);
14979
15125
  const availableTools = tools === undefined && mcpToolNames.length === 0 ? undefined : [...tools ?? [], ...mcpToolNames];
14980
15126
  return {
14981
- controller: new AppServerRuntimeController(client, this.remoteOptions, allowedTools, clientToolset),
15127
+ controller: new AppServerRuntimeController(client, this.remoteOptions, allowedTools, clientToolset, toolPolicyApplied),
14982
15128
  runtime: response.runtime,
14983
15129
  model: typeof response.agent?.model === "string" ? response.agent.model : typeof response.conversation?.model === "string" ? response.conversation.model : "",
14984
15130
  modelSettings: objectRecord(response.agent?.model_settings) ?? objectRecord(response.conversation?.model_settings) ?? null,
@@ -14996,6 +15142,10 @@ class AppServerSession extends RemoteClientSessionCore {
14996
15142
  }
14997
15143
  }
14998
15144
  onCoreClose() {
15145
+ if (this.closed) {
15146
+ this.removeInheritedListener?.();
15147
+ this.removeInheritedListener = null;
15148
+ }
14999
15149
  this.removeExternalToolHandler?.();
15000
15150
  this.removeExternalToolHandler = null;
15001
15151
  this.removeControlRequestHandler?.();
@@ -15026,11 +15176,11 @@ class AppServerSession extends RemoteClientSessionCore {
15026
15176
  this.ownedConnection = connection;
15027
15177
  return connection.url;
15028
15178
  }
15029
- async startRuntime(client) {
15030
- const command = await this.buildRuntimeStartCommand(client);
15179
+ async startRuntime(client, toolPolicy) {
15180
+ const command = await this.buildRuntimeStartCommand(client, toolPolicy);
15031
15181
  return client.runtimeStart(command);
15032
15182
  }
15033
- async buildRuntimeStartCommand(client) {
15183
+ async buildRuntimeStartCommand(client, toolPolicy) {
15034
15184
  const options = this.mode.options;
15035
15185
  const command = {
15036
15186
  client_info: {
@@ -15061,6 +15211,7 @@ class AppServerSession extends RemoteClientSessionCore {
15061
15211
  const groups = externalToolGroups([...this.externalTools.values()]);
15062
15212
  if (groups)
15063
15213
  command.external_tools = groups;
15214
+ command.tool_policy = toolPolicy;
15064
15215
  if (this.mode.kind === "create-agent") {
15065
15216
  command.create_agent = {
15066
15217
  body: await createAgentBody(this.mode.options),
@@ -15543,6 +15694,9 @@ class CloudStatusTransport {
15543
15694
  if (channel === "control" && message.type === "stream_delta") {
15544
15695
  return false;
15545
15696
  }
15697
+ if (channel === "stream" && message.type === "external_tool_call_request") {
15698
+ return false;
15699
+ }
15546
15700
  if (this.isDuplicate(message))
15547
15701
  return false;
15548
15702
  this.trackEventSequence(message);
@@ -16039,7 +16193,7 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
16039
16193
  getOptions: () => this.currentOptions()
16040
16194
  });
16041
16195
  if (this.externalTools.size > 0) {
16042
- this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools));
16196
+ this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools, () => this.runtime));
16043
16197
  }
16044
16198
  }
16045
16199
  cleanupTransportHandlers() {
@@ -16097,6 +16251,7 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
16097
16251
  const groups = externalToolGroups([...this.externalTools.values()]);
16098
16252
  if (groups)
16099
16253
  command.external_tools = groups;
16254
+ command.tool_policy = runtimeToolPolicy(expandMcpToolWildcards(options.allowedTools, this.mcpBridge?.tools.map((tool) => tool.name) ?? []), "toolset" in options ? options.toolset : undefined);
16100
16255
  return await client.runtimeStart(command);
16101
16256
  }
16102
16257
  async afterRuntimeInitialized() {
@@ -16662,6 +16817,221 @@ function createQuery(createSession, params) {
16662
16817
  });
16663
16818
  }
16664
16819
 
16820
+ // src/tool-registration.ts
16821
+ var RECONNECT_DELAYS_MS = [250, 1000, 5000, 15000];
16822
+ var runtimeKey = (runtime) => `${runtime.agent_id}:${runtime.conversation_id}`;
16823
+
16824
+ class ToolRegistry {
16825
+ options;
16826
+ connection = null;
16827
+ entries = new Set;
16828
+ listeners = new Set;
16829
+ reconnectAttempt = 0;
16830
+ reconnectTimer = null;
16831
+ closed = false;
16832
+ constructor(options) {
16833
+ this.options = options;
16834
+ }
16835
+ async register(options) {
16836
+ if (this.closed)
16837
+ throw new Error("LettaAgentClient is closed");
16838
+ const entry = { options, runtimes: [], call: createExternalToolCallHandler(new Map) };
16839
+ await this.apply(entry, options);
16840
+ const registry = this;
16841
+ return {
16842
+ get conversations() {
16843
+ return [...entry.options.conversations];
16844
+ },
16845
+ update: async (next) => {
16846
+ if (!registry.entries.has(entry))
16847
+ throw new Error("This tool registration is closed.");
16848
+ await registry.apply(entry, { ...entry.options, ...next });
16849
+ },
16850
+ close: () => registry.remove(entry),
16851
+ [Symbol.asyncDispose]: () => registry.remove(entry)
16852
+ };
16853
+ }
16854
+ close() {
16855
+ this.closed = true;
16856
+ this.entries.clear();
16857
+ this.disconnect();
16858
+ this.notify();
16859
+ }
16860
+ inheritFor(id, own) {
16861
+ const covering = this.covering(id);
16862
+ if (!covering)
16863
+ return null;
16864
+ const registered = new Set(covering.options.tools.map((tool) => tool.name));
16865
+ const clash = own.tools?.find((tool) => registered.has(tool.name));
16866
+ if (clash) {
16867
+ throw new Error(`Tool ${clash.name} is already registered for ${id} with registerTools(); don't pass it to the session again.`);
16868
+ }
16869
+ if (own.toolset !== undefined && covering.options.toolset !== undefined && JSON.stringify(own.toolset) !== JSON.stringify(covering.options.toolset)) {
16870
+ throw new Error(`${id} already has a toolset from registerTools(); narrow it with the session's allowedTools instead.`);
16871
+ }
16872
+ return {
16873
+ current: () => this.covering(id)?.options ?? null,
16874
+ onChange: (listener) => {
16875
+ this.listeners.add(listener);
16876
+ return () => this.listeners.delete(listener);
16877
+ }
16878
+ };
16879
+ }
16880
+ covering(id) {
16881
+ return [...this.entries].find((entry) => entry.options.conversations.includes(id) || entry.runtimes.some((runtime) => runtime.conversation_id === id || runtime.conversation_id === "default" && runtime.agent_id === id));
16882
+ }
16883
+ notify() {
16884
+ for (const listener of [...this.listeners])
16885
+ listener();
16886
+ }
16887
+ async apply(entry, options) {
16888
+ if (!Array.isArray(options.conversations) || options.conversations.length === 0) {
16889
+ throw new Error("registerTools() requires at least one conversation.");
16890
+ }
16891
+ if (!Array.isArray(options.tools))
16892
+ throw new Error("registerTools() requires a tools array.");
16893
+ const connection = await this.connect();
16894
+ if (!connection.toolPolicy && (options.allowedTools || options.toolset)) {
16895
+ throw new Error("registerTools() allowedTools and toolset require a Letta Code app server that supports runtime tool policies. Upgrade @letta-ai/letta-code.");
16896
+ }
16897
+ const runtimes = await Promise.all(options.conversations.map((id) => resolveRuntime(connection.client, id)));
16898
+ const taken = new Set([...this.entries].filter((other) => other !== entry).flatMap((other) => other.runtimes.map(runtimeKey)));
16899
+ const overlap = options.conversations.find((_, index) => taken.has(runtimeKey(runtimes[index])));
16900
+ if (overlap)
16901
+ throw new Error(`Tools are already registered for ${overlap}.`);
16902
+ const keys = new Set(runtimes.map(runtimeKey));
16903
+ const dropped = entry.runtimes.filter((runtime) => !keys.has(runtimeKey(runtime)));
16904
+ const previous = { ...entry };
16905
+ const isNew = !this.entries.has(entry);
16906
+ entry.options = options;
16907
+ entry.runtimes = runtimes;
16908
+ entry.call = createExternalToolCallHandler(new Map(options.tools.map((tool) => [tool.name, tool])));
16909
+ this.entries.add(entry);
16910
+ try {
16911
+ await publish(connection.client, [
16912
+ group(runtimes, options, connection.toolPolicy),
16913
+ ...dropped.length > 0 ? [{ runtimes: dropped, external_tools: [] }] : []
16914
+ ]);
16915
+ } catch (error) {
16916
+ Object.assign(entry, previous);
16917
+ if (isNew)
16918
+ this.entries.delete(entry);
16919
+ throw error;
16920
+ }
16921
+ this.notify();
16922
+ }
16923
+ async remove(entry) {
16924
+ if (!this.entries.delete(entry))
16925
+ return;
16926
+ this.notify();
16927
+ if (this.entries.size === 0) {
16928
+ this.disconnect();
16929
+ return;
16930
+ }
16931
+ const connection = await this.connect();
16932
+ await publish(connection.client, [{ runtimes: entry.runtimes, external_tools: [] }]);
16933
+ }
16934
+ connect() {
16935
+ this.connection ??= this.open().catch((error) => {
16936
+ this.connection = null;
16937
+ throw error;
16938
+ });
16939
+ return this.connection;
16940
+ }
16941
+ async open() {
16942
+ const owned = this.options.url ? null : await this.options.connect?.();
16943
+ const url = this.options.url ?? owned?.url;
16944
+ if (!url)
16945
+ throw new Error("registerTools() requires an app-server url.");
16946
+ const client = applyUniqueRequestIds(createAppServerClient({
16947
+ url,
16948
+ ...this.options.authToken !== undefined ? { authToken: this.options.authToken } : {},
16949
+ ...this.options.WebSocket ? { WebSocket: this.options.WebSocket } : {},
16950
+ ...this.options.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.options.requestTimeoutMs } : {}
16951
+ }));
16952
+ client.onExternalToolCall(async (request) => {
16953
+ const runtime = request.runtime;
16954
+ const entry = runtime && [...this.entries].find((candidate) => candidate.runtimes.some((r) => runtimeKey(r) === runtimeKey(runtime)));
16955
+ if (!entry)
16956
+ throw new Error(`No tools registered for conversation ${runtime?.conversation_id}`);
16957
+ return entry.call(request);
16958
+ });
16959
+ try {
16960
+ await client.connect();
16961
+ } catch (error) {
16962
+ client.close();
16963
+ owned?.close();
16964
+ throw error;
16965
+ }
16966
+ client.onDisconnect(() => this.onDisconnect(client));
16967
+ return { client, owned: owned ?? null, toolPolicy: await supportsRuntimeToolPolicy(client) };
16968
+ }
16969
+ disconnect() {
16970
+ if (this.reconnectTimer)
16971
+ clearTimeout(this.reconnectTimer);
16972
+ this.reconnectTimer = null;
16973
+ const connection = this.connection;
16974
+ this.connection = null;
16975
+ connection?.then(({ client, owned }) => {
16976
+ client.close();
16977
+ owned?.close();
16978
+ }, () => {
16979
+ return;
16980
+ });
16981
+ }
16982
+ onDisconnect(client) {
16983
+ this.connection?.then((current) => {
16984
+ if (current.client !== client)
16985
+ return;
16986
+ current.owned?.close();
16987
+ this.connection = null;
16988
+ this.scheduleReconnect();
16989
+ }, () => {
16990
+ return;
16991
+ });
16992
+ }
16993
+ scheduleReconnect() {
16994
+ if (this.closed || this.entries.size === 0 || this.reconnectTimer)
16995
+ return;
16996
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
16997
+ this.reconnectAttempt += 1;
16998
+ this.reconnectTimer = setTimeout(() => {
16999
+ this.reconnectTimer = null;
17000
+ this.republish().catch(() => this.scheduleReconnect());
17001
+ }, delay);
17002
+ this.reconnectTimer.unref?.();
17003
+ }
17004
+ async republish() {
17005
+ const connection = await this.connect();
17006
+ await publish(connection.client, [...this.entries].map((entry) => group(entry.runtimes, entry.options, connection.toolPolicy)));
17007
+ this.reconnectAttempt = 0;
17008
+ }
17009
+ }
17010
+ function group(runtimes, options, toolPolicy) {
17011
+ return {
17012
+ runtimes,
17013
+ external_tools: externalToolGroups(options.tools) ?? [],
17014
+ ...toolPolicy ? { tool_policy: runtimeToolPolicy(options.allowedTools, options.toolset) } : {}
17015
+ };
17016
+ }
17017
+ async function publish(client, updates) {
17018
+ const response = await client.runtimeExternalToolsUpdate({ updates });
17019
+ if (!response.success)
17020
+ throw new Error(response.error ?? "Failed to register tools");
17021
+ }
17022
+ async function resolveRuntime(client, id) {
17023
+ if (!id.startsWith("conv-") && !id.startsWith("local-conv-")) {
17024
+ return { agent_id: id, conversation_id: "default" };
17025
+ }
17026
+ const response = await client.request({ type: "conversation_retrieve", request_id: client.nextRequestId("conversation_retrieve"), conversation_id: id }, {
17027
+ predicate: (message) => message.type === "conversation_retrieve_response"
17028
+ });
17029
+ if (!response.success || !response.conversation) {
17030
+ throw new Error(response.error ?? `Failed to retrieve conversation ${id}`);
17031
+ }
17032
+ return { agent_id: response.conversation.agent_id ?? null, conversation_id: id };
17033
+ }
17034
+
16665
17035
  // src/validation.ts
16666
17036
  var VALID_SKILL_SOURCES = [
16667
17037
  "bundled",
@@ -16938,6 +17308,7 @@ class LettaAgentClientBase {
16938
17308
  agentRepositoriesClient = null;
16939
17309
  cloudClient = null;
16940
17310
  managementTransport = null;
17311
+ toolRegistry = null;
16941
17312
  closed = false;
16942
17313
  closePromise = null;
16943
17314
  constructor(options = {}) {
@@ -17056,20 +17427,21 @@ class LettaAgentClientBase {
17056
17427
  const sessionOptions = stripCloudExecutionOptions(options);
17057
17428
  validateCreateSessionOptions(sessionOptions);
17058
17429
  this.assertSessionBackend("resumeSession", options);
17430
+ const inherited = this.backend === "cloud" ? null : this.toolRegistry?.inheritFor(id, sessionOptions) ?? null;
17059
17431
  if (this.backend === "remote") {
17060
- if (looksLikeConversationId(id)) {
17061
- return new AppServerSession(this.appServerSessionOptions(), {
17062
- kind: "session",
17063
- conversationId: id,
17064
- options
17065
- });
17066
- }
17067
- return new AppServerSession(this.appServerSessionOptions(), {
17432
+ const session2 = looksLikeConversationId(id) ? new AppServerSession(this.appServerSessionOptions(), {
17433
+ kind: "session",
17434
+ conversationId: id,
17435
+ options
17436
+ }) : new AppServerSession(this.appServerSessionOptions(), {
17068
17437
  kind: "session",
17069
17438
  agentId: id,
17070
17439
  defaultConversation: true,
17071
17440
  options
17072
17441
  });
17442
+ if (inherited)
17443
+ session2.inheritTools(inherited);
17444
+ return session2;
17073
17445
  }
17074
17446
  if (this.backend === "cloud") {
17075
17447
  if (looksLikeConversationId(id)) {
@@ -17086,7 +17458,10 @@ class LettaAgentClientBase {
17086
17458
  options
17087
17459
  }, this.getCloudClient());
17088
17460
  }
17089
- return this.resumeLocalSession(id, options);
17461
+ const session = this.resumeLocalSession(id, options);
17462
+ if (inherited && session instanceof AppServerSession)
17463
+ session.inheritTools(inherited);
17464
+ return session;
17090
17465
  }
17091
17466
  async prompt(message, agentId, options = {}) {
17092
17467
  this.assertOpen();
@@ -17294,10 +17669,23 @@ class LettaAgentClientBase {
17294
17669
  }
17295
17670
  return this.managementTransport;
17296
17671
  }
17672
+ registerTools(options) {
17673
+ this.assertOpen();
17674
+ if (this.backend === "cloud") {
17675
+ return Promise.reject(new Error('registerTools() requires backend: "local" or "remote". Cloud sessions register tools through the tools session option.'));
17676
+ }
17677
+ this.toolRegistry ??= new ToolRegistry(this.backend === "remote" ? this.remoteOptions() : this.localAppServerConnection());
17678
+ return this.toolRegistry.register(options);
17679
+ }
17680
+ localAppServerConnection() {
17681
+ throw this.localBackendUnavailableError();
17682
+ }
17297
17683
  close() {
17298
17684
  if (this.closePromise)
17299
17685
  return this.closePromise;
17300
17686
  this.closed = true;
17687
+ this.toolRegistry?.close();
17688
+ this.toolRegistry = null;
17301
17689
  const transport = this.managementTransport;
17302
17690
  this.managementTransport = null;
17303
17691
  const close = transport?.close() ?? Promise.resolve();
@@ -17842,4 +18230,4 @@ export {
17842
18230
  CloudManagedSandboxExpiredError
17843
18231
  };
17844
18232
 
17845
- //# debugId=37BAC87F328A3A1864756E2164756E21
18233
+ //# debugId=214DC938EA1FA61564756E2164756E21