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

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 +480 -77
  9. package/dist/client-entry.js.map +15 -12
  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 +508 -102
  19. package/dist/index.js.map +16 -13
  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 +1 -1
  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
@@ -13128,7 +13128,7 @@ async function* streamStructuredTurns(config) {
13128
13128
  return;
13129
13129
  });
13130
13130
  }
13131
- if (msg.type !== "result") {
13131
+ if (msg.type !== "result" || msg.origin !== undefined) {
13132
13132
  yield msg;
13133
13133
  continue;
13134
13134
  }
@@ -13245,9 +13245,115 @@ function expandMcpToolWildcards(allowedTools, mcpTools) {
13245
13245
  return [...new Set(expanded)];
13246
13246
  }
13247
13247
 
13248
+ // src/external-tools.ts
13249
+ function externalToolGroups(tools) {
13250
+ if (!tools || tools.length === 0)
13251
+ return;
13252
+ return [
13253
+ {
13254
+ tools: tools.map((tool) => ({
13255
+ name: tool.name,
13256
+ label: tool.label,
13257
+ description: tool.description,
13258
+ parameters: tool.parameters
13259
+ }))
13260
+ }
13261
+ ];
13262
+ }
13263
+ var MAX_REMEMBERED_EXTERNAL_TOOL_CALLS = 200;
13264
+ function createExternalToolCallHandler(externalTools, fallbackRuntime) {
13265
+ const calls = new Map;
13266
+ const execute = async (request) => {
13267
+ const tool = externalTools.get(request.tool_name);
13268
+ if (!tool) {
13269
+ throw new Error(`Unknown external tool: ${request.tool_name}`);
13270
+ }
13271
+ const runtime = request.runtime ?? fallbackRuntime?.() ?? null;
13272
+ const context = runtime ? { toolCallId: request.tool_call_id, agentId: runtime.agent_id, conversationId: runtime.conversation_id } : undefined;
13273
+ const result = await tool.execute(request.tool_call_id, request.input, undefined, undefined, context);
13274
+ return {
13275
+ content: result.content.map((part) => ({
13276
+ type: part.type,
13277
+ ...part.text !== undefined ? { text: part.text } : {},
13278
+ ...part.data !== undefined ? { data: part.data } : {},
13279
+ ...part.mimeType !== undefined ? { mimeType: part.mimeType } : {}
13280
+ })),
13281
+ ...result.isError === true ? { is_error: true } : {}
13282
+ };
13283
+ };
13284
+ return (request) => {
13285
+ const requestId = request.request_id;
13286
+ if (typeof requestId !== "string" || requestId.length === 0) {
13287
+ return execute(request);
13288
+ }
13289
+ const existing = calls.get(requestId);
13290
+ if (existing)
13291
+ return existing;
13292
+ const call = execute(request);
13293
+ calls.set(requestId, call);
13294
+ while (calls.size > MAX_REMEMBERED_EXTERNAL_TOOL_CALLS) {
13295
+ const oldest = calls.keys().next().value;
13296
+ if (oldest === undefined)
13297
+ break;
13298
+ calls.delete(oldest);
13299
+ }
13300
+ return call;
13301
+ };
13302
+ }
13303
+
13304
+ // src/runtime-tool-policy.ts
13305
+ var RUNTIME_TOOL_POLICY_CAPABILITY = "runtime_tool_policy";
13306
+ var CAPABILITY_PROBE_TIMEOUT_MS = 5000;
13307
+ function clientToolsetPayload(toolset) {
13308
+ return {
13309
+ ...toolset.base !== undefined ? { base: toolset.base } : {},
13310
+ ...toolset.include !== undefined ? { include: [...new Set(toolset.include)] } : {}
13311
+ };
13312
+ }
13313
+ function runtimeToolPolicy(allowedTools, toolset) {
13314
+ return {
13315
+ ...toolset !== undefined ? { toolset: clientToolsetPayload(toolset) } : {},
13316
+ ...allowedTools !== undefined ? { allowlist: [...new Set(allowedTools)] } : {},
13317
+ exclude_interactive: true
13318
+ };
13319
+ }
13320
+ async function supportsRuntimeToolPolicy(client) {
13321
+ try {
13322
+ const info = await client.info({ timeoutMs: CAPABILITY_PROBE_TIMEOUT_MS });
13323
+ const capabilities = info.capabilities;
13324
+ return capabilities?.[RUNTIME_TOOL_POLICY_CAPABILITY] === true;
13325
+ } catch {
13326
+ return false;
13327
+ }
13328
+ }
13329
+ function mergeInheritedTools(inherited, own) {
13330
+ if (!inherited)
13331
+ return own;
13332
+ const tools = new Map(inherited.tools.map((tool) => [tool.name, tool]));
13333
+ for (const tool of own.tools)
13334
+ tools.set(tool.name, tool);
13335
+ const ownNames = new Set(own.tools.map((tool) => tool.name));
13336
+ const registered = inherited.allowedTools;
13337
+ const allowedTools = registered === undefined ? own.allowedTools : own.allowedTools === undefined ? [...new Set([...registered, ...ownNames])] : own.allowedTools.filter((name) => registered.includes(name) || ownNames.has(name));
13338
+ const toolset = own.toolset ?? inherited.toolset;
13339
+ return {
13340
+ tools: [...tools.values()],
13341
+ ...allowedTools !== undefined ? { allowedTools } : {},
13342
+ ...toolset !== undefined ? { toolset } : {}
13343
+ };
13344
+ }
13345
+
13248
13346
  // src/remote-turn-coordinator.ts
13249
13347
  var MAX_RECENTLY_SETTLED_RUN_IDS = 256;
13250
13348
  var TRAILING_USAGE_GRACE_MS = 100;
13349
+ var TURN_ORIGINS = new Set([
13350
+ "task_notification",
13351
+ "cron",
13352
+ "subagent",
13353
+ "system",
13354
+ "channel",
13355
+ "user"
13356
+ ]);
13251
13357
 
13252
13358
  class RemoteTurnCoordinator {
13253
13359
  label;
@@ -13259,6 +13365,8 @@ class RemoteTurnCoordinator {
13259
13365
  activeTurn = null;
13260
13366
  pendingTurns = [];
13261
13367
  settledRunIds = new Set;
13368
+ queueOrigins = new Map;
13369
+ nextServerOrigin = "unknown";
13262
13370
  nextTurnId = 0;
13263
13371
  messageCounter = 0;
13264
13372
  clientMessageCounter = 0;
@@ -13274,16 +13382,27 @@ class RemoteTurnCoordinator {
13274
13382
  return this._activeTurnStartedAt;
13275
13383
  }
13276
13384
  hasInFlightTurn() {
13277
- return this.activeTurn !== null || this.pendingTurns.length > 0;
13385
+ return this.activeTurn !== null && this.activeTurn.origin === undefined || this.pendingTurns.length > 0;
13278
13386
  }
13279
13387
  trackSentTurn(runtime, callerOtid, outputFormat) {
13280
13388
  const otid = normalizeCallerOtid(callerOtid);
13281
- const turn = {
13282
- id: ++this.nextTurnId,
13283
- runtime,
13389
+ const turn = this.newTracker(runtime, {
13284
13390
  ...otid !== undefined ? { otid } : {},
13285
13391
  ...outputFormat !== undefined ? { outputFormat } : {},
13286
- clientMessageId: otid ?? `sdk-message-${Date.now()}-${++this.clientMessageCounter}`,
13392
+ clientMessageId: otid ?? `sdk-message-${Date.now()}-${++this.clientMessageCounter}`
13393
+ });
13394
+ if (this.activeTurn) {
13395
+ this.pendingTurns.push(turn);
13396
+ } else {
13397
+ this.activateTurn(turn);
13398
+ }
13399
+ return turn;
13400
+ }
13401
+ newTracker(runtime, identity) {
13402
+ return {
13403
+ id: ++this.nextTurnId,
13404
+ runtime,
13405
+ ...identity,
13287
13406
  queuedAt: Date.now(),
13288
13407
  startedAt: 0,
13289
13408
  assistantText: "",
@@ -13300,13 +13419,32 @@ class RemoteTurnCoordinator {
13300
13419
  abortRequested: false,
13301
13420
  timeout: null
13302
13421
  };
13303
- if (this.activeTurn) {
13304
- this.pendingTurns.push(turn);
13305
- } else {
13306
- this.activateTurn(turn);
13307
- }
13422
+ }
13423
+ startServerTurn(runtime) {
13424
+ const turn = this.newTracker(runtime, { clientMessageId: "", origin: this.nextServerOrigin });
13425
+ this.nextServerOrigin = "unknown";
13426
+ this.activateTurn(turn);
13308
13427
  return turn;
13309
13428
  }
13429
+ observeQueueOrigins(message, queue) {
13430
+ for (const item of queue) {
13431
+ if (!item.clientMessageId)
13432
+ continue;
13433
+ this.queueOrigins.set(item.clientMessageId, TURN_ORIGINS.has(item.source) ? item.source : "unknown");
13434
+ if (this.queueOrigins.size > MAX_RECENTLY_SETTLED_RUN_IDS) {
13435
+ this.queueOrigins.delete(this.queueOrigins.keys().next().value);
13436
+ }
13437
+ }
13438
+ for (const removed of Array.isArray(message.removed) ? message.removed : []) {
13439
+ const { client_message_id: id, disposition, source } = removed;
13440
+ if (disposition !== "dequeued" || typeof id !== "string")
13441
+ continue;
13442
+ const own = [this.activeTurn, ...this.pendingTurns].some((turn) => turn?.clientMessageId === id);
13443
+ if (own)
13444
+ continue;
13445
+ this.nextServerOrigin = typeof source === "string" && TURN_ORIGINS.has(source) ? source : this.queueOrigins.get(id) ?? "unknown";
13446
+ }
13447
+ }
13310
13448
  removeTrackedTurn(turn) {
13311
13449
  if (turn.timeout)
13312
13450
  clearTimeout(turn.timeout);
@@ -13320,6 +13458,12 @@ class RemoteTurnCoordinator {
13320
13458
  if (index !== -1)
13321
13459
  this.pendingTurns.splice(index, 1);
13322
13460
  }
13461
+ failServerTurn(detail) {
13462
+ const active = this.activeTurn;
13463
+ if (active?.origin === undefined)
13464
+ return;
13465
+ this.failTurn(active, detail, { errorCode: "stream_closed", recoverable: true });
13466
+ }
13323
13467
  markAbortRequested() {
13324
13468
  if (this.activeTurn)
13325
13469
  this.activeTurn.abortRequested = true;
@@ -13339,6 +13483,7 @@ class RemoteTurnCoordinator {
13339
13483
  type: "queue_update",
13340
13484
  queue: queueItems(message)
13341
13485
  };
13486
+ this.observeQueueOrigins(message, sdkMessage2.queue);
13342
13487
  this.enqueue(sdkMessage2);
13343
13488
  return;
13344
13489
  }
@@ -13381,9 +13526,9 @@ class RemoteTurnCoordinator {
13381
13526
  if (!delta)
13382
13527
  return;
13383
13528
  const messageType = streamDeltaMessageType(delta);
13384
- const active = messageType === "usage_statistics" || messageType === "stop_reason" ? this.activeTurn : this.activateNextTurnFromProtocol();
13385
13529
  const runId = streamDeltaRunId(delta);
13386
13530
  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");
13531
+ const active = messageType === "usage_statistics" || messageType === "stop_reason" ? this.activeTurn : this.activateNextTurnFromProtocol() ?? (isTurnEvidence && runId !== undefined ? this.startServerTurn(runtime) : null);
13387
13532
  if (active && isTurnEvidence) {
13388
13533
  active.observedTurnEvidence = true;
13389
13534
  if (runId)
@@ -13841,7 +13986,8 @@ class RemoteTurnCoordinator {
13841
13986
  stopReason,
13842
13987
  durationMs: Date.now() - (tracker?.startedAt || this._activeTurnStartedAt),
13843
13988
  conversationId: turn.runtime.conversation_id,
13844
- runIds: turn.runIds.length > 0 ? turn.runIds : undefined
13989
+ runIds: turn.runIds.length > 0 ? turn.runIds : undefined,
13990
+ ...tracker?.origin !== undefined ? { origin: tracker.origin } : {}
13845
13991
  };
13846
13992
  }
13847
13993
  enqueue(message) {
@@ -14083,9 +14229,8 @@ class RemoteClientSessionCore {
14083
14229
  }
14084
14230
  await this.send(message);
14085
14231
  for await (const msg of this.stream()) {
14086
- if (msg.type === "result") {
14232
+ if (msg.type === "result" && msg.origin === undefined)
14087
14233
  return msg;
14088
- }
14089
14234
  }
14090
14235
  return {
14091
14236
  type: "result",
@@ -14121,7 +14266,7 @@ class RemoteClientSessionCore {
14121
14266
  if (!msg)
14122
14267
  break;
14123
14268
  yield msg;
14124
- if (msg.type === "result")
14269
+ if (msg.type === "result" && msg.origin === undefined)
14125
14270
  break;
14126
14271
  }
14127
14272
  }
@@ -14431,6 +14576,7 @@ class RemoteClientSessionCore {
14431
14576
  this.controller?.close();
14432
14577
  this.controller = null;
14433
14578
  const error = new Error(`${this.label} connection closed before send`);
14579
+ this.turns.failServerTurn(error.message);
14434
14580
  for (const cancel of [...this.deviceStatusRefreshCancels])
14435
14581
  cancel(error);
14436
14582
  this.deviceStatusRefreshCancels.clear();
@@ -14571,38 +14717,6 @@ function agentToolNames(agent) {
14571
14717
  return typeof name === "string" && name.length > 0 ? [name] : [];
14572
14718
  });
14573
14719
  }
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
14720
  async function resolveAppServerToolApproval(options, toolName, toolInput, context) {
14607
14721
  if ("outputFormat" in options && options.outputFormat !== undefined && toolName === STRUCTURED_OUTPUT_TOOL) {
14608
14722
  return { behavior: "allow", updatedInput: null, updatedPermissions: [] };
@@ -14779,11 +14893,26 @@ class AppServerRuntimeController {
14779
14893
  options;
14780
14894
  clientToolAllowlist;
14781
14895
  clientToolset;
14782
- constructor(client, options, clientToolAllowlist, clientToolset) {
14896
+ toolPolicyApplied;
14897
+ constructor(client, options, clientToolAllowlist, clientToolset, toolPolicyApplied = false) {
14783
14898
  this.client = client;
14784
14899
  this.options = options;
14785
14900
  this.clientToolAllowlist = clientToolAllowlist;
14786
14901
  this.clientToolset = clientToolset;
14902
+ this.toolPolicyApplied = toolPolicyApplied;
14903
+ }
14904
+ async updateToolSelection(runtime, selection) {
14905
+ this.clientToolAllowlist = selection.allowedTools;
14906
+ this.clientToolset = selection.toolset;
14907
+ const response = await this.client.runtimeExternalToolsUpdate({
14908
+ updates: [{
14909
+ runtimes: [runtime],
14910
+ external_tools: externalToolGroups(selection.tools) ?? [],
14911
+ ...this.toolPolicyApplied ? { tool_policy: runtimeToolPolicy(selection.allowedTools, selection.toolset) } : {}
14912
+ }]
14913
+ });
14914
+ if (!response.success)
14915
+ throw new Error(response.error ?? "Failed to update session tools");
14787
14916
  }
14788
14917
  onMessage(handler) {
14789
14918
  return this.client.onMessage((message, channel) => {
@@ -14805,14 +14934,11 @@ class AppServerRuntimeController {
14805
14934
  }
14806
14935
  ]
14807
14936
  };
14808
- if (this.clientToolAllowlist !== undefined) {
14937
+ if (!this.toolPolicyApplied && this.clientToolAllowlist !== undefined) {
14809
14938
  payload.client_tool_allowlist = [...new Set(this.clientToolAllowlist)];
14810
14939
  }
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
- };
14940
+ if (!this.toolPolicyApplied && this.clientToolset !== undefined) {
14941
+ payload.client_toolset = clientToolsetPayload(this.clientToolset);
14816
14942
  }
14817
14943
  if (options.outputFormat !== undefined) {
14818
14944
  payload.response_format = {
@@ -14824,7 +14950,8 @@ class AppServerRuntimeController {
14824
14950
  }
14825
14951
  };
14826
14952
  }
14827
- payload.exclude_interactive_tools = true;
14953
+ if (!this.toolPolicyApplied)
14954
+ payload.exclude_interactive_tools = true;
14828
14955
  this.client.input({
14829
14956
  runtime,
14830
14957
  payload
@@ -14916,6 +15043,9 @@ class AppServerSession extends RemoteClientSessionCore {
14916
15043
  mcpCleanup = Promise.resolve();
14917
15044
  removeExternalToolHandler = null;
14918
15045
  removeControlRequestHandler = null;
15046
+ inheritedTools = null;
15047
+ removeInheritedListener = null;
15048
+ ownSelection = null;
14919
15049
  constructor(remoteOptions, mode) {
14920
15050
  super(mode, {
14921
15051
  label: "app-server",
@@ -14927,6 +15057,27 @@ class AppServerSession extends RemoteClientSessionCore {
14927
15057
  this.externalTools.set(tool.name, tool);
14928
15058
  }
14929
15059
  }
15060
+ inheritTools(source) {
15061
+ this.inheritedTools = source;
15062
+ this.removeInheritedListener = source.onChange(() => {
15063
+ this.refreshInheritedTools().catch(() => {
15064
+ return;
15065
+ });
15066
+ });
15067
+ }
15068
+ applyToolSelection() {
15069
+ const selection = mergeInheritedTools(this.inheritedTools?.current() ?? null, this.ownSelection ?? { tools: [] });
15070
+ this.externalTools.clear();
15071
+ for (const tool of selection.tools)
15072
+ this.externalTools.set(tool.name, tool);
15073
+ return selection;
15074
+ }
15075
+ async refreshInheritedTools() {
15076
+ if (!this.ownSelection || !this.runtime || !(this.controller instanceof AppServerRuntimeController))
15077
+ return;
15078
+ const selection = this.applyToolSelection();
15079
+ await this.controller.updateToolSelection(this.runtime, selection);
15080
+ }
14930
15081
  shouldEnableMemfs(options) {
14931
15082
  if (this.mode.kind !== "create-agent")
14932
15083
  return false;
@@ -14942,6 +15093,9 @@ class AppServerSession extends RemoteClientSessionCore {
14942
15093
  ...this.remoteOptions.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.remoteOptions.requestTimeoutMs } : {}
14943
15094
  }));
14944
15095
  const options = this.currentOptions();
15096
+ this.externalTools.clear();
15097
+ for (const tool of this.mode.options.tools ?? [])
15098
+ this.externalTools.set(tool.name, tool);
14945
15099
  this.mcpBridge = await connectMcpServers("mcpServers" in options ? options.mcpServers : undefined, {
14946
15100
  cwd: options.cwd,
14947
15101
  reservedToolNames: this.externalTools.keys()
@@ -14949,13 +15103,22 @@ class AppServerSession extends RemoteClientSessionCore {
14949
15103
  for (const tool of this.mcpBridge.tools) {
14950
15104
  this.externalTools.set(tool.name, tool);
14951
15105
  }
15106
+ const mcpToolNames = this.mcpBridge.tools.map((tool) => tool.name);
15107
+ const ownToolset = "toolset" in options ? options.toolset : undefined;
15108
+ const ownAllowedTools = expandMcpToolWildcards(options.allowedTools, mcpToolNames);
15109
+ this.ownSelection = {
15110
+ tools: [...this.externalTools.values()],
15111
+ ...ownAllowedTools !== undefined ? { allowedTools: ownAllowedTools } : {},
15112
+ ...ownToolset !== undefined ? { toolset: ownToolset } : {}
15113
+ };
15114
+ const { allowedTools, toolset: clientToolset } = this.applyToolSelection();
14952
15115
  this.removeControlRequestHandler = registerAppServerControlRequestHandler({
14953
15116
  client,
14954
15117
  getRuntime: () => this.runtime,
14955
15118
  getOptions: () => this.currentOptions()
14956
15119
  });
14957
- if (this.externalTools.size > 0) {
14958
- this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools));
15120
+ if (this.externalTools.size > 0 || this.inheritedTools) {
15121
+ this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools, () => this.runtime));
14959
15122
  }
14960
15123
  try {
14961
15124
  await client.connect();
@@ -14966,19 +15129,17 @@ class AppServerSession extends RemoteClientSessionCore {
14966
15129
  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
15130
  }
14968
15131
  }
14969
- const response = await this.startRuntime(client);
15132
+ const response = await this.startRuntime(client, runtimeToolPolicy(allowedTools, clientToolset));
14970
15133
  if (!response.success || !response.runtime) {
14971
15134
  throw new Error(response.error ?? "Failed to start app-server runtime");
14972
15135
  }
14973
15136
  this.watchTransportDisconnect(client);
15137
+ const toolPolicyApplied = await supportsRuntimeToolPolicy(client);
14974
15138
  const tools = agentToolNames(response.agent);
14975
15139
  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
15140
  const availableTools = tools === undefined && mcpToolNames.length === 0 ? undefined : [...tools ?? [], ...mcpToolNames];
14980
15141
  return {
14981
- controller: new AppServerRuntimeController(client, this.remoteOptions, allowedTools, clientToolset),
15142
+ controller: new AppServerRuntimeController(client, this.remoteOptions, allowedTools, clientToolset, toolPolicyApplied),
14982
15143
  runtime: response.runtime,
14983
15144
  model: typeof response.agent?.model === "string" ? response.agent.model : typeof response.conversation?.model === "string" ? response.conversation.model : "",
14984
15145
  modelSettings: objectRecord(response.agent?.model_settings) ?? objectRecord(response.conversation?.model_settings) ?? null,
@@ -14996,6 +15157,10 @@ class AppServerSession extends RemoteClientSessionCore {
14996
15157
  }
14997
15158
  }
14998
15159
  onCoreClose() {
15160
+ if (this.closed) {
15161
+ this.removeInheritedListener?.();
15162
+ this.removeInheritedListener = null;
15163
+ }
14999
15164
  this.removeExternalToolHandler?.();
15000
15165
  this.removeExternalToolHandler = null;
15001
15166
  this.removeControlRequestHandler?.();
@@ -15026,11 +15191,11 @@ class AppServerSession extends RemoteClientSessionCore {
15026
15191
  this.ownedConnection = connection;
15027
15192
  return connection.url;
15028
15193
  }
15029
- async startRuntime(client) {
15030
- const command = await this.buildRuntimeStartCommand(client);
15194
+ async startRuntime(client, toolPolicy) {
15195
+ const command = await this.buildRuntimeStartCommand(client, toolPolicy);
15031
15196
  return client.runtimeStart(command);
15032
15197
  }
15033
- async buildRuntimeStartCommand(client) {
15198
+ async buildRuntimeStartCommand(client, toolPolicy) {
15034
15199
  const options = this.mode.options;
15035
15200
  const command = {
15036
15201
  client_info: {
@@ -15061,6 +15226,7 @@ class AppServerSession extends RemoteClientSessionCore {
15061
15226
  const groups = externalToolGroups([...this.externalTools.values()]);
15062
15227
  if (groups)
15063
15228
  command.external_tools = groups;
15229
+ command.tool_policy = toolPolicy;
15064
15230
  if (this.mode.kind === "create-agent") {
15065
15231
  command.create_agent = {
15066
15232
  body: await createAgentBody(this.mode.options),
@@ -15543,6 +15709,9 @@ class CloudStatusTransport {
15543
15709
  if (channel === "control" && message.type === "stream_delta") {
15544
15710
  return false;
15545
15711
  }
15712
+ if (channel === "stream" && message.type === "external_tool_call_request") {
15713
+ return false;
15714
+ }
15546
15715
  if (this.isDuplicate(message))
15547
15716
  return false;
15548
15717
  this.trackEventSequence(message);
@@ -16039,7 +16208,7 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
16039
16208
  getOptions: () => this.currentOptions()
16040
16209
  });
16041
16210
  if (this.externalTools.size > 0) {
16042
- this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools));
16211
+ this.removeExternalToolHandler = client.onExternalToolCall(createExternalToolCallHandler(this.externalTools, () => this.runtime));
16043
16212
  }
16044
16213
  }
16045
16214
  cleanupTransportHandlers() {
@@ -16097,6 +16266,7 @@ class CloudEnvironmentSession extends RemoteClientSessionCore {
16097
16266
  const groups = externalToolGroups([...this.externalTools.values()]);
16098
16267
  if (groups)
16099
16268
  command.external_tools = groups;
16269
+ command.tool_policy = runtimeToolPolicy(expandMcpToolWildcards(options.allowedTools, this.mcpBridge?.tools.map((tool) => tool.name) ?? []), "toolset" in options ? options.toolset : undefined);
16100
16270
  return await client.runtimeStart(command);
16101
16271
  }
16102
16272
  async afterRuntimeInitialized() {
@@ -16662,6 +16832,221 @@ function createQuery(createSession, params) {
16662
16832
  });
16663
16833
  }
16664
16834
 
16835
+ // src/tool-registration.ts
16836
+ var RECONNECT_DELAYS_MS = [250, 1000, 5000, 15000];
16837
+ var runtimeKey = (runtime) => `${runtime.agent_id}:${runtime.conversation_id}`;
16838
+
16839
+ class ToolRegistry {
16840
+ options;
16841
+ connection = null;
16842
+ entries = new Set;
16843
+ listeners = new Set;
16844
+ reconnectAttempt = 0;
16845
+ reconnectTimer = null;
16846
+ closed = false;
16847
+ constructor(options) {
16848
+ this.options = options;
16849
+ }
16850
+ async register(options) {
16851
+ if (this.closed)
16852
+ throw new Error("LettaAgentClient is closed");
16853
+ const entry = { options, runtimes: [], call: createExternalToolCallHandler(new Map) };
16854
+ await this.apply(entry, options);
16855
+ const registry = this;
16856
+ return {
16857
+ get conversations() {
16858
+ return [...entry.options.conversations];
16859
+ },
16860
+ update: async (next) => {
16861
+ if (!registry.entries.has(entry))
16862
+ throw new Error("This tool registration is closed.");
16863
+ await registry.apply(entry, { ...entry.options, ...next });
16864
+ },
16865
+ close: () => registry.remove(entry),
16866
+ [Symbol.asyncDispose]: () => registry.remove(entry)
16867
+ };
16868
+ }
16869
+ close() {
16870
+ this.closed = true;
16871
+ this.entries.clear();
16872
+ this.disconnect();
16873
+ this.notify();
16874
+ }
16875
+ inheritFor(id, own) {
16876
+ const covering = this.covering(id);
16877
+ if (!covering)
16878
+ return null;
16879
+ const registered = new Set(covering.options.tools.map((tool) => tool.name));
16880
+ const clash = own.tools?.find((tool) => registered.has(tool.name));
16881
+ if (clash) {
16882
+ throw new Error(`Tool ${clash.name} is already registered for ${id} with registerTools(); don't pass it to the session again.`);
16883
+ }
16884
+ if (own.toolset !== undefined && covering.options.toolset !== undefined && JSON.stringify(own.toolset) !== JSON.stringify(covering.options.toolset)) {
16885
+ throw new Error(`${id} already has a toolset from registerTools(); narrow it with the session's allowedTools instead.`);
16886
+ }
16887
+ return {
16888
+ current: () => this.covering(id)?.options ?? null,
16889
+ onChange: (listener) => {
16890
+ this.listeners.add(listener);
16891
+ return () => this.listeners.delete(listener);
16892
+ }
16893
+ };
16894
+ }
16895
+ covering(id) {
16896
+ 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));
16897
+ }
16898
+ notify() {
16899
+ for (const listener of [...this.listeners])
16900
+ listener();
16901
+ }
16902
+ async apply(entry, options) {
16903
+ if (!Array.isArray(options.conversations) || options.conversations.length === 0) {
16904
+ throw new Error("registerTools() requires at least one conversation.");
16905
+ }
16906
+ if (!Array.isArray(options.tools))
16907
+ throw new Error("registerTools() requires a tools array.");
16908
+ const connection = await this.connect();
16909
+ if (!connection.toolPolicy && (options.allowedTools || options.toolset)) {
16910
+ throw new Error("registerTools() allowedTools and toolset require a Letta Code app server that supports runtime tool policies. Upgrade @letta-ai/letta-code.");
16911
+ }
16912
+ const runtimes = await Promise.all(options.conversations.map((id) => resolveRuntime(connection.client, id)));
16913
+ const taken = new Set([...this.entries].filter((other) => other !== entry).flatMap((other) => other.runtimes.map(runtimeKey)));
16914
+ const overlap = options.conversations.find((_, index) => taken.has(runtimeKey(runtimes[index])));
16915
+ if (overlap)
16916
+ throw new Error(`Tools are already registered for ${overlap}.`);
16917
+ const keys = new Set(runtimes.map(runtimeKey));
16918
+ const dropped = entry.runtimes.filter((runtime) => !keys.has(runtimeKey(runtime)));
16919
+ const previous = { ...entry };
16920
+ const isNew = !this.entries.has(entry);
16921
+ entry.options = options;
16922
+ entry.runtimes = runtimes;
16923
+ entry.call = createExternalToolCallHandler(new Map(options.tools.map((tool) => [tool.name, tool])));
16924
+ this.entries.add(entry);
16925
+ try {
16926
+ await publish(connection.client, [
16927
+ group(runtimes, options, connection.toolPolicy),
16928
+ ...dropped.length > 0 ? [{ runtimes: dropped, external_tools: [] }] : []
16929
+ ]);
16930
+ } catch (error) {
16931
+ Object.assign(entry, previous);
16932
+ if (isNew)
16933
+ this.entries.delete(entry);
16934
+ throw error;
16935
+ }
16936
+ this.notify();
16937
+ }
16938
+ async remove(entry) {
16939
+ if (!this.entries.delete(entry))
16940
+ return;
16941
+ this.notify();
16942
+ if (this.entries.size === 0) {
16943
+ this.disconnect();
16944
+ return;
16945
+ }
16946
+ const connection = await this.connect();
16947
+ await publish(connection.client, [{ runtimes: entry.runtimes, external_tools: [] }]);
16948
+ }
16949
+ connect() {
16950
+ this.connection ??= this.open().catch((error) => {
16951
+ this.connection = null;
16952
+ throw error;
16953
+ });
16954
+ return this.connection;
16955
+ }
16956
+ async open() {
16957
+ const owned = this.options.url ? null : await this.options.connect?.();
16958
+ const url = this.options.url ?? owned?.url;
16959
+ if (!url)
16960
+ throw new Error("registerTools() requires an app-server url.");
16961
+ const client = applyUniqueRequestIds(createAppServerClient({
16962
+ url,
16963
+ ...this.options.authToken !== undefined ? { authToken: this.options.authToken } : {},
16964
+ ...this.options.WebSocket ? { WebSocket: this.options.WebSocket } : {},
16965
+ ...this.options.requestTimeoutMs !== undefined ? { requestTimeoutMs: this.options.requestTimeoutMs } : {}
16966
+ }));
16967
+ client.onExternalToolCall(async (request) => {
16968
+ const runtime = request.runtime;
16969
+ const entry = runtime && [...this.entries].find((candidate) => candidate.runtimes.some((r) => runtimeKey(r) === runtimeKey(runtime)));
16970
+ if (!entry)
16971
+ throw new Error(`No tools registered for conversation ${runtime?.conversation_id}`);
16972
+ return entry.call(request);
16973
+ });
16974
+ try {
16975
+ await client.connect();
16976
+ } catch (error) {
16977
+ client.close();
16978
+ owned?.close();
16979
+ throw error;
16980
+ }
16981
+ client.onDisconnect(() => this.onDisconnect(client));
16982
+ return { client, owned: owned ?? null, toolPolicy: await supportsRuntimeToolPolicy(client) };
16983
+ }
16984
+ disconnect() {
16985
+ if (this.reconnectTimer)
16986
+ clearTimeout(this.reconnectTimer);
16987
+ this.reconnectTimer = null;
16988
+ const connection = this.connection;
16989
+ this.connection = null;
16990
+ connection?.then(({ client, owned }) => {
16991
+ client.close();
16992
+ owned?.close();
16993
+ }, () => {
16994
+ return;
16995
+ });
16996
+ }
16997
+ onDisconnect(client) {
16998
+ this.connection?.then((current) => {
16999
+ if (current.client !== client)
17000
+ return;
17001
+ current.owned?.close();
17002
+ this.connection = null;
17003
+ this.scheduleReconnect();
17004
+ }, () => {
17005
+ return;
17006
+ });
17007
+ }
17008
+ scheduleReconnect() {
17009
+ if (this.closed || this.entries.size === 0 || this.reconnectTimer)
17010
+ return;
17011
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
17012
+ this.reconnectAttempt += 1;
17013
+ this.reconnectTimer = setTimeout(() => {
17014
+ this.reconnectTimer = null;
17015
+ this.republish().catch(() => this.scheduleReconnect());
17016
+ }, delay);
17017
+ this.reconnectTimer.unref?.();
17018
+ }
17019
+ async republish() {
17020
+ const connection = await this.connect();
17021
+ await publish(connection.client, [...this.entries].map((entry) => group(entry.runtimes, entry.options, connection.toolPolicy)));
17022
+ this.reconnectAttempt = 0;
17023
+ }
17024
+ }
17025
+ function group(runtimes, options, toolPolicy) {
17026
+ return {
17027
+ runtimes,
17028
+ external_tools: externalToolGroups(options.tools) ?? [],
17029
+ ...toolPolicy ? { tool_policy: runtimeToolPolicy(options.allowedTools, options.toolset) } : {}
17030
+ };
17031
+ }
17032
+ async function publish(client, updates) {
17033
+ const response = await client.runtimeExternalToolsUpdate({ updates });
17034
+ if (!response.success)
17035
+ throw new Error(response.error ?? "Failed to register tools");
17036
+ }
17037
+ async function resolveRuntime(client, id) {
17038
+ if (!id.startsWith("conv-") && !id.startsWith("local-conv-")) {
17039
+ return { agent_id: id, conversation_id: "default" };
17040
+ }
17041
+ const response = await client.request({ type: "conversation_retrieve", request_id: client.nextRequestId("conversation_retrieve"), conversation_id: id }, {
17042
+ predicate: (message) => message.type === "conversation_retrieve_response"
17043
+ });
17044
+ if (!response.success || !response.conversation) {
17045
+ throw new Error(response.error ?? `Failed to retrieve conversation ${id}`);
17046
+ }
17047
+ return { agent_id: response.conversation.agent_id ?? null, conversation_id: id };
17048
+ }
17049
+
16665
17050
  // src/validation.ts
16666
17051
  var VALID_SKILL_SOURCES = [
16667
17052
  "bundled",
@@ -16938,6 +17323,7 @@ class LettaAgentClientBase {
16938
17323
  agentRepositoriesClient = null;
16939
17324
  cloudClient = null;
16940
17325
  managementTransport = null;
17326
+ toolRegistry = null;
16941
17327
  closed = false;
16942
17328
  closePromise = null;
16943
17329
  constructor(options = {}) {
@@ -17056,20 +17442,21 @@ class LettaAgentClientBase {
17056
17442
  const sessionOptions = stripCloudExecutionOptions(options);
17057
17443
  validateCreateSessionOptions(sessionOptions);
17058
17444
  this.assertSessionBackend("resumeSession", options);
17445
+ const inherited = this.backend === "cloud" ? null : this.toolRegistry?.inheritFor(id, sessionOptions) ?? null;
17059
17446
  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(), {
17447
+ const session2 = looksLikeConversationId(id) ? new AppServerSession(this.appServerSessionOptions(), {
17448
+ kind: "session",
17449
+ conversationId: id,
17450
+ options
17451
+ }) : new AppServerSession(this.appServerSessionOptions(), {
17068
17452
  kind: "session",
17069
17453
  agentId: id,
17070
17454
  defaultConversation: true,
17071
17455
  options
17072
17456
  });
17457
+ if (inherited)
17458
+ session2.inheritTools(inherited);
17459
+ return session2;
17073
17460
  }
17074
17461
  if (this.backend === "cloud") {
17075
17462
  if (looksLikeConversationId(id)) {
@@ -17086,7 +17473,10 @@ class LettaAgentClientBase {
17086
17473
  options
17087
17474
  }, this.getCloudClient());
17088
17475
  }
17089
- return this.resumeLocalSession(id, options);
17476
+ const session = this.resumeLocalSession(id, options);
17477
+ if (inherited && session instanceof AppServerSession)
17478
+ session.inheritTools(inherited);
17479
+ return session;
17090
17480
  }
17091
17481
  async prompt(message, agentId, options = {}) {
17092
17482
  this.assertOpen();
@@ -17294,10 +17684,23 @@ class LettaAgentClientBase {
17294
17684
  }
17295
17685
  return this.managementTransport;
17296
17686
  }
17687
+ registerTools(options) {
17688
+ this.assertOpen();
17689
+ if (this.backend === "cloud") {
17690
+ return Promise.reject(new Error('registerTools() requires backend: "local" or "remote". Cloud sessions register tools through the tools session option.'));
17691
+ }
17692
+ this.toolRegistry ??= new ToolRegistry(this.backend === "remote" ? this.remoteOptions() : this.localAppServerConnection());
17693
+ return this.toolRegistry.register(options);
17694
+ }
17695
+ localAppServerConnection() {
17696
+ throw this.localBackendUnavailableError();
17697
+ }
17297
17698
  close() {
17298
17699
  if (this.closePromise)
17299
17700
  return this.closePromise;
17300
17701
  this.closed = true;
17702
+ this.toolRegistry?.close();
17703
+ this.toolRegistry = null;
17301
17704
  const transport = this.managementTransport;
17302
17705
  this.managementTransport = null;
17303
17706
  const close = transport?.close() ?? Promise.resolve();
@@ -17842,4 +18245,4 @@ export {
17842
18245
  CloudManagedSandboxExpiredError
17843
18246
  };
17844
18247
 
17845
- //# debugId=37BAC87F328A3A1864756E2164756E21
18248
+ //# debugId=6BF8FBE2B45A41B264756E2164756E21