letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__py3-none-any.whl

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 (136) hide show
  1. letta/__init__.py +7 -1
  2. letta/agent.py +14 -7
  3. letta/agents/base_agent.py +1 -0
  4. letta/agents/ephemeral_summary_agent.py +104 -0
  5. letta/agents/helpers.py +35 -3
  6. letta/agents/letta_agent.py +492 -176
  7. letta/agents/letta_agent_batch.py +22 -16
  8. letta/agents/prompts/summary_system_prompt.txt +62 -0
  9. letta/agents/voice_agent.py +22 -7
  10. letta/agents/voice_sleeptime_agent.py +13 -8
  11. letta/constants.py +33 -1
  12. letta/data_sources/connectors.py +52 -36
  13. letta/errors.py +4 -0
  14. letta/functions/ast_parsers.py +13 -30
  15. letta/functions/function_sets/base.py +3 -1
  16. letta/functions/functions.py +2 -0
  17. letta/functions/mcp_client/base_client.py +151 -97
  18. letta/functions/mcp_client/sse_client.py +49 -31
  19. letta/functions/mcp_client/stdio_client.py +107 -106
  20. letta/functions/schema_generator.py +22 -22
  21. letta/groups/helpers.py +3 -4
  22. letta/groups/sleeptime_multi_agent.py +4 -4
  23. letta/groups/sleeptime_multi_agent_v2.py +22 -0
  24. letta/helpers/composio_helpers.py +16 -0
  25. letta/helpers/converters.py +20 -0
  26. letta/helpers/datetime_helpers.py +1 -6
  27. letta/helpers/tool_rule_solver.py +2 -1
  28. letta/interfaces/anthropic_streaming_interface.py +17 -2
  29. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
  30. letta/interfaces/openai_streaming_interface.py +18 -2
  31. letta/llm_api/anthropic_client.py +24 -3
  32. letta/llm_api/google_ai_client.py +0 -15
  33. letta/llm_api/google_vertex_client.py +6 -5
  34. letta/llm_api/llm_client_base.py +15 -0
  35. letta/llm_api/openai.py +2 -2
  36. letta/llm_api/openai_client.py +60 -8
  37. letta/orm/__init__.py +2 -0
  38. letta/orm/agent.py +45 -43
  39. letta/orm/base.py +0 -2
  40. letta/orm/block.py +1 -0
  41. letta/orm/custom_columns.py +13 -0
  42. letta/orm/enums.py +5 -0
  43. letta/orm/file.py +3 -1
  44. letta/orm/files_agents.py +68 -0
  45. letta/orm/mcp_server.py +48 -0
  46. letta/orm/message.py +1 -0
  47. letta/orm/organization.py +11 -2
  48. letta/orm/passage.py +25 -10
  49. letta/orm/sandbox_config.py +5 -2
  50. letta/orm/sqlalchemy_base.py +171 -110
  51. letta/prompts/system/memgpt_base.txt +6 -1
  52. letta/prompts/system/memgpt_v2_chat.txt +57 -0
  53. letta/prompts/system/sleeptime.txt +2 -0
  54. letta/prompts/system/sleeptime_v2.txt +28 -0
  55. letta/schemas/agent.py +87 -20
  56. letta/schemas/block.py +7 -1
  57. letta/schemas/file.py +57 -0
  58. letta/schemas/mcp.py +74 -0
  59. letta/schemas/memory.py +5 -2
  60. letta/schemas/message.py +9 -0
  61. letta/schemas/openai/openai.py +0 -6
  62. letta/schemas/providers.py +33 -4
  63. letta/schemas/tool.py +26 -21
  64. letta/schemas/tool_execution_result.py +5 -0
  65. letta/server/db.py +23 -8
  66. letta/server/rest_api/app.py +73 -56
  67. letta/server/rest_api/interface.py +4 -4
  68. letta/server/rest_api/routers/v1/agents.py +132 -47
  69. letta/server/rest_api/routers/v1/blocks.py +3 -2
  70. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  71. letta/server/rest_api/routers/v1/groups.py +3 -3
  72. letta/server/rest_api/routers/v1/jobs.py +14 -17
  73. letta/server/rest_api/routers/v1/organizations.py +10 -10
  74. letta/server/rest_api/routers/v1/providers.py +12 -10
  75. letta/server/rest_api/routers/v1/runs.py +3 -3
  76. letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
  77. letta/server/rest_api/routers/v1/sources.py +108 -43
  78. letta/server/rest_api/routers/v1/steps.py +8 -6
  79. letta/server/rest_api/routers/v1/tools.py +134 -95
  80. letta/server/rest_api/utils.py +12 -1
  81. letta/server/server.py +272 -73
  82. letta/services/agent_manager.py +246 -313
  83. letta/services/block_manager.py +30 -9
  84. letta/services/context_window_calculator/__init__.py +0 -0
  85. letta/services/context_window_calculator/context_window_calculator.py +150 -0
  86. letta/services/context_window_calculator/token_counter.py +82 -0
  87. letta/services/file_processor/__init__.py +0 -0
  88. letta/services/file_processor/chunker/__init__.py +0 -0
  89. letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
  90. letta/services/file_processor/embedder/__init__.py +0 -0
  91. letta/services/file_processor/embedder/openai_embedder.py +84 -0
  92. letta/services/file_processor/file_processor.py +123 -0
  93. letta/services/file_processor/parser/__init__.py +0 -0
  94. letta/services/file_processor/parser/base_parser.py +9 -0
  95. letta/services/file_processor/parser/mistral_parser.py +54 -0
  96. letta/services/file_processor/types.py +0 -0
  97. letta/services/files_agents_manager.py +184 -0
  98. letta/services/group_manager.py +118 -0
  99. letta/services/helpers/agent_manager_helper.py +76 -21
  100. letta/services/helpers/tool_execution_helper.py +3 -0
  101. letta/services/helpers/tool_parser_helper.py +100 -0
  102. letta/services/identity_manager.py +44 -42
  103. letta/services/job_manager.py +21 -10
  104. letta/services/mcp/base_client.py +5 -2
  105. letta/services/mcp/sse_client.py +3 -5
  106. letta/services/mcp/stdio_client.py +3 -5
  107. letta/services/mcp_manager.py +281 -0
  108. letta/services/message_manager.py +40 -26
  109. letta/services/organization_manager.py +55 -19
  110. letta/services/passage_manager.py +211 -13
  111. letta/services/provider_manager.py +48 -2
  112. letta/services/sandbox_config_manager.py +105 -0
  113. letta/services/source_manager.py +4 -5
  114. letta/services/step_manager.py +9 -6
  115. letta/services/summarizer/summarizer.py +50 -23
  116. letta/services/telemetry_manager.py +7 -0
  117. letta/services/tool_executor/tool_execution_manager.py +11 -52
  118. letta/services/tool_executor/tool_execution_sandbox.py +4 -34
  119. letta/services/tool_executor/tool_executor.py +107 -105
  120. letta/services/tool_manager.py +56 -17
  121. letta/services/tool_sandbox/base.py +39 -92
  122. letta/services/tool_sandbox/e2b_sandbox.py +16 -11
  123. letta/services/tool_sandbox/local_sandbox.py +51 -23
  124. letta/services/user_manager.py +36 -3
  125. letta/settings.py +10 -3
  126. letta/templates/__init__.py +0 -0
  127. letta/templates/sandbox_code_file.py.j2 +47 -0
  128. letta/templates/template_helper.py +16 -0
  129. letta/tracing.py +30 -1
  130. letta/types/__init__.py +7 -0
  131. letta/utils.py +25 -1
  132. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
  133. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
  134. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
  135. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
  136. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -9,6 +9,8 @@ from typing import Any, Dict, List, Literal, Optional
9
9
  from letta.constants import (
10
10
  COMPOSIO_ENTITY_ENV_VAR_KEY,
11
11
  CORE_MEMORY_LINE_NUMBER_WARNING,
12
+ MCP_TOOL_TAG_NAME_PREFIX,
13
+ MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX,
12
14
  READ_ONLY_BLOCK_EDIT_ERROR,
13
15
  RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
14
16
  WEB_SEARCH_CLIP_CONTENT,
@@ -17,7 +19,7 @@ from letta.constants import (
17
19
  )
18
20
  from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
19
21
  from letta.functions.composio_helpers import execute_composio_action_async, generate_composio_action_from_func_name
20
- from letta.helpers.composio_helpers import get_composio_api_key
22
+ from letta.helpers.composio_helpers import get_composio_api_key_async
21
23
  from letta.helpers.json_helpers import json_dumps
22
24
  from letta.log import get_logger
23
25
  from letta.schemas.agent import AgentState
@@ -31,12 +33,14 @@ from letta.schemas.tool_execution_result import ToolExecutionResult
31
33
  from letta.schemas.user import User
32
34
  from letta.services.agent_manager import AgentManager
33
35
  from letta.services.block_manager import BlockManager
36
+ from letta.services.mcp_manager import MCPManager
34
37
  from letta.services.message_manager import MessageManager
35
38
  from letta.services.passage_manager import PassageManager
36
39
  from letta.services.tool_sandbox.e2b_sandbox import AsyncToolSandboxE2B
37
40
  from letta.services.tool_sandbox.local_sandbox import AsyncToolSandboxLocal
38
41
  from letta.settings import tool_settings
39
42
  from letta.tracing import trace_method
43
+ from letta.types import JsonDict
40
44
  from letta.utils import get_friendly_error_msg
41
45
 
42
46
  logger = get_logger(__name__)
@@ -60,13 +64,13 @@ class ToolExecutor(ABC):
60
64
  self.actor = actor
61
65
 
62
66
  @abstractmethod
63
- def execute(
67
+ async def execute(
64
68
  self,
65
69
  function_name: str,
66
70
  function_args: dict,
67
- agent_state: AgentState,
68
71
  tool: Tool,
69
72
  actor: User,
73
+ agent_state: Optional[AgentState] = None,
70
74
  sandbox_config: Optional[SandboxConfig] = None,
71
75
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
72
76
  ) -> ToolExecutionResult:
@@ -76,17 +80,18 @@ class ToolExecutor(ABC):
76
80
  class LettaCoreToolExecutor(ToolExecutor):
77
81
  """Executor for LETTA core tools with direct implementation of functions."""
78
82
 
79
- def execute(
83
+ async def execute(
80
84
  self,
81
85
  function_name: str,
82
86
  function_args: dict,
83
- agent_state: AgentState,
84
87
  tool: Tool,
85
88
  actor: User,
89
+ agent_state: Optional[AgentState] = None,
86
90
  sandbox_config: Optional[SandboxConfig] = None,
87
91
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
88
92
  ) -> ToolExecutionResult:
89
93
  # Map function names to method calls
94
+ assert agent_state is not None, "Agent state is required for core tools"
90
95
  function_map = {
91
96
  "send_message": self.send_message,
92
97
  "conversation_search": self.conversation_search,
@@ -105,13 +110,22 @@ class LettaCoreToolExecutor(ToolExecutor):
105
110
 
106
111
  # Execute the appropriate function
107
112
  function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
108
- function_response = function_map[function_name](agent_state, actor, **function_args_copy)
109
- return ToolExecutionResult(
110
- status="success",
111
- func_return=function_response,
112
- )
113
+ try:
114
+ function_response = await function_map[function_name](agent_state, actor, **function_args_copy)
115
+ return ToolExecutionResult(
116
+ status="success",
117
+ func_return=function_response,
118
+ agent_state=agent_state,
119
+ )
120
+ except Exception as e:
121
+ return ToolExecutionResult(
122
+ status="error",
123
+ func_return=e,
124
+ agent_state=agent_state,
125
+ stderr=[get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e))],
126
+ )
113
127
 
114
- def send_message(self, agent_state: AgentState, actor: User, message: str) -> Optional[str]:
128
+ async def send_message(self, agent_state: AgentState, actor: User, message: str) -> Optional[str]:
115
129
  """
116
130
  Sends a message to the human user.
117
131
 
@@ -123,7 +137,7 @@ class LettaCoreToolExecutor(ToolExecutor):
123
137
  """
124
138
  return "Sent message successfully."
125
139
 
126
- def conversation_search(self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0) -> Optional[str]:
140
+ async def conversation_search(self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0) -> Optional[str]:
127
141
  """
128
142
  Search prior conversation history using case-insensitive string matching.
129
143
 
@@ -142,7 +156,7 @@ class LettaCoreToolExecutor(ToolExecutor):
142
156
  raise ValueError(f"'page' argument must be an integer")
143
157
 
144
158
  count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
145
- messages = MessageManager().list_user_messages_for_agent(
159
+ messages = await MessageManager().list_user_messages_for_agent_async(
146
160
  agent_id=agent_state.id,
147
161
  actor=actor,
148
162
  query_text=query,
@@ -161,7 +175,7 @@ class LettaCoreToolExecutor(ToolExecutor):
161
175
 
162
176
  return results_str
163
177
 
164
- def archival_memory_search(
178
+ async def archival_memory_search(
165
179
  self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0, start: Optional[int] = 0
166
180
  ) -> Optional[str]:
167
181
  """
@@ -186,7 +200,7 @@ class LettaCoreToolExecutor(ToolExecutor):
186
200
 
187
201
  try:
188
202
  # Get results using passage manager
189
- all_results = AgentManager().list_passages(
203
+ all_results = await AgentManager().list_passages_async(
190
204
  actor=actor,
191
205
  agent_id=agent_state.id,
192
206
  query_text=query,
@@ -207,7 +221,7 @@ class LettaCoreToolExecutor(ToolExecutor):
207
221
  except Exception as e:
208
222
  raise e
209
223
 
210
- def archival_memory_insert(self, agent_state: AgentState, actor: User, content: str) -> Optional[str]:
224
+ async def archival_memory_insert(self, agent_state: AgentState, actor: User, content: str) -> Optional[str]:
211
225
  """
212
226
  Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
213
227
 
@@ -217,16 +231,16 @@ class LettaCoreToolExecutor(ToolExecutor):
217
231
  Returns:
218
232
  Optional[str]: None is always returned as this function does not produce a response.
219
233
  """
220
- PassageManager().insert_passage(
234
+ await PassageManager().insert_passage_async(
221
235
  agent_state=agent_state,
222
236
  agent_id=agent_state.id,
223
237
  text=content,
224
238
  actor=actor,
225
239
  )
226
- AgentManager().rebuild_system_prompt(agent_id=agent_state.id, actor=actor, force=True)
240
+ await AgentManager().rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
227
241
  return None
228
242
 
229
- def core_memory_append(self, agent_state: AgentState, actor: User, label: str, content: str) -> Optional[str]:
243
+ async def core_memory_append(self, agent_state: AgentState, actor: User, label: str, content: str) -> Optional[str]:
230
244
  """
231
245
  Append to the contents of core memory.
232
246
 
@@ -242,10 +256,10 @@ class LettaCoreToolExecutor(ToolExecutor):
242
256
  current_value = str(agent_state.memory.get_block(label).value)
243
257
  new_value = current_value + "\n" + str(content)
244
258
  agent_state.memory.update_block_value(label=label, value=new_value)
245
- AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
259
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
246
260
  return None
247
261
 
248
- def core_memory_replace(
262
+ async def core_memory_replace(
249
263
  self,
250
264
  agent_state: AgentState,
251
265
  actor: User,
@@ -271,10 +285,10 @@ class LettaCoreToolExecutor(ToolExecutor):
271
285
  raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
272
286
  new_value = current_value.replace(str(old_content), str(new_content))
273
287
  agent_state.memory.update_block_value(label=label, value=new_value)
274
- AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
288
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
275
289
  return None
276
290
 
277
- def memory_replace(
291
+ async def memory_replace(
278
292
  self,
279
293
  agent_state: AgentState,
280
294
  actor: User,
@@ -289,19 +303,18 @@ class LettaCoreToolExecutor(ToolExecutor):
289
303
  Args:
290
304
  label (str): Section of the memory to be edited, identified by its label.
291
305
  old_str (str): The text to replace (must match exactly, including whitespace
292
- and indentation).
306
+ and indentation). Do not include line number prefixes.
293
307
  new_str (Optional[str]): The new text to insert in place of the old text.
294
- Omit this argument to delete the old_str.
308
+ Omit this argument to delete the old_str. Do not include line number prefixes.
295
309
 
296
310
  Returns:
297
311
  str: The success message
298
312
  """
299
- import re
300
313
 
301
314
  if agent_state.memory.get_block(label).read_only:
302
315
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
303
316
 
304
- if bool(re.search(r"\nLine \d+: ", old_str)):
317
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(old_str)):
305
318
  raise ValueError(
306
319
  "old_str contains a line number prefix, which is not allowed. "
307
320
  "Do not include line numbers when calling memory tools (line "
@@ -313,7 +326,7 @@ class LettaCoreToolExecutor(ToolExecutor):
313
326
  "Do not include line number information when calling memory tools "
314
327
  "(line numbers are for display purposes only)."
315
328
  )
316
- if bool(re.search(r"\nLine \d+: ", new_str)):
329
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
317
330
  raise ValueError(
318
331
  "new_str contains a line number prefix, which is not allowed. "
319
332
  "Do not include line numbers when calling memory tools (line "
@@ -344,7 +357,7 @@ class LettaCoreToolExecutor(ToolExecutor):
344
357
  # Write the new content to the block
345
358
  agent_state.memory.update_block_value(label=label, value=new_value)
346
359
 
347
- AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
360
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
348
361
 
349
362
  # Create a snippet of the edited section
350
363
  SNIPPET_LINES = 3
@@ -367,7 +380,7 @@ class LettaCoreToolExecutor(ToolExecutor):
367
380
  # return None
368
381
  return success_msg
369
382
 
370
- def memory_insert(
383
+ async def memory_insert(
371
384
  self,
372
385
  agent_state: AgentState,
373
386
  actor: User,
@@ -381,19 +394,18 @@ class LettaCoreToolExecutor(ToolExecutor):
381
394
 
382
395
  Args:
383
396
  label (str): Section of the memory to be edited, identified by its label.
384
- new_str (str): The text to insert.
397
+ new_str (str): The text to insert. Do not include line number prefixes.
385
398
  insert_line (int): The line number after which to insert the text (0 for
386
399
  beginning of file). Defaults to -1 (end of the file).
387
400
 
388
401
  Returns:
389
402
  str: The success message
390
403
  """
391
- import re
392
404
 
393
405
  if agent_state.memory.get_block(label).read_only:
394
406
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
395
407
 
396
- if bool(re.search(r"\nLine \d+: ", new_str)):
408
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
397
409
  raise ValueError(
398
410
  "new_str contains a line number prefix, which is not allowed. Do not "
399
411
  "include line numbers when calling memory tools (line numbers are for "
@@ -412,7 +424,9 @@ class LettaCoreToolExecutor(ToolExecutor):
412
424
  n_lines = len(current_value_lines)
413
425
 
414
426
  # Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
415
- if insert_line < 0 or insert_line > n_lines:
427
+ if insert_line == -1:
428
+ insert_line = n_lines
429
+ elif insert_line < 0 or insert_line > n_lines:
416
430
  raise ValueError(
417
431
  f"Invalid `insert_line` parameter: {insert_line}. It should be within "
418
432
  f"the range of lines of the memory block: {[0, n_lines]}, or -1 to "
@@ -436,7 +450,7 @@ class LettaCoreToolExecutor(ToolExecutor):
436
450
  # Write into the block
437
451
  agent_state.memory.update_block_value(label=label, value=new_value)
438
452
 
439
- AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
453
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
440
454
 
441
455
  # Prepare the success message
442
456
  success_msg = f"The core memory block with label `{label}` has been edited. "
@@ -453,7 +467,7 @@ class LettaCoreToolExecutor(ToolExecutor):
453
467
 
454
468
  return success_msg
455
469
 
456
- def memory_rethink(self, agent_state: AgentState, actor: User, label: str, new_memory: str) -> str:
470
+ async def memory_rethink(self, agent_state: AgentState, actor: User, label: str, new_memory: str) -> str:
457
471
  """
458
472
  The memory_rethink command allows you to completely rewrite the contents of a
459
473
  memory block. Use this tool to make large sweeping changes (e.g. when you want
@@ -463,17 +477,15 @@ class LettaCoreToolExecutor(ToolExecutor):
463
477
  Args:
464
478
  label (str): The memory block to be rewritten, identified by its label.
465
479
  new_memory (str): The new memory contents with information integrated from
466
- existing memory blocks and the conversation context.
480
+ existing memory blocks and the conversation context. Do not include line number prefixes.
467
481
 
468
482
  Returns:
469
483
  str: The success message
470
484
  """
471
- import re
472
-
473
485
  if agent_state.memory.get_block(label).read_only:
474
486
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
475
487
 
476
- if bool(re.search(r"\nLine \d+: ", new_memory)):
488
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_memory)):
477
489
  raise ValueError(
478
490
  "new_memory contains a line number prefix, which is not allowed. Do not "
479
491
  "include line numbers when calling memory tools (line numbers are for "
@@ -491,7 +503,7 @@ class LettaCoreToolExecutor(ToolExecutor):
491
503
 
492
504
  agent_state.memory.update_block_value(label=label, value=new_memory)
493
505
 
494
- AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
506
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
495
507
 
496
508
  # Prepare the success message
497
509
  success_msg = f"The core memory block with label `{label}` has been edited. "
@@ -507,7 +519,7 @@ class LettaCoreToolExecutor(ToolExecutor):
507
519
  # return None
508
520
  return success_msg
509
521
 
510
- def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
522
+ async def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
511
523
  """
512
524
  Call the memory_finish_edits command when you are finished making edits
513
525
  (integrating all new information) into the memory blocks. This function
@@ -526,16 +538,17 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
526
538
  self,
527
539
  function_name: str,
528
540
  function_args: dict,
529
- agent_state: AgentState,
530
541
  tool: Tool,
531
542
  actor: User,
543
+ agent_state: Optional[AgentState] = None,
532
544
  sandbox_config: Optional[SandboxConfig] = None,
533
545
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
534
546
  ) -> ToolExecutionResult:
547
+ assert agent_state is not None, "Agent state is required for multi-agent tools"
535
548
  function_map = {
536
549
  "send_message_to_agent_and_wait_for_reply": self.send_message_to_agent_and_wait_for_reply,
537
550
  "send_message_to_agent_async": self.send_message_to_agent_async,
538
- "send_message_to_agents_matching_tags": self.send_message_to_agents_matching_tags,
551
+ "send_message_to_agents_matching_tags": self.send_message_to_agents_matching_tags_async,
539
552
  }
540
553
 
541
554
  if function_name not in function_map:
@@ -573,11 +586,13 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
573
586
 
574
587
  return "Successfully sent message"
575
588
 
576
- async def send_message_to_agents_matching_tags(
589
+ async def send_message_to_agents_matching_tags_async(
577
590
  self, agent_state: AgentState, message: str, match_all: List[str], match_some: List[str]
578
591
  ) -> str:
579
592
  # Find matching agents
580
- matching_agents = self.agent_manager.list_agents_matching_tags(actor=self.actor, match_all=match_all, match_some=match_some)
593
+ matching_agents = await self.agent_manager.list_agents_matching_tags_async(
594
+ actor=self.actor, match_all=match_all, match_some=match_some
595
+ )
581
596
  if not matching_agents:
582
597
  return str([])
583
598
 
@@ -633,19 +648,20 @@ class ExternalComposioToolExecutor(ToolExecutor):
633
648
  self,
634
649
  function_name: str,
635
650
  function_args: dict,
636
- agent_state: AgentState,
637
651
  tool: Tool,
638
652
  actor: User,
653
+ agent_state: Optional[AgentState] = None,
639
654
  sandbox_config: Optional[SandboxConfig] = None,
640
655
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
641
656
  ) -> ToolExecutionResult:
657
+ assert agent_state is not None, "Agent state is required for external Composio tools"
642
658
  action_name = generate_composio_action_from_func_name(tool.name)
643
659
 
644
660
  # Get entity ID from the agent_state
645
661
  entity_id = self._get_entity_id(agent_state)
646
662
 
647
663
  # Get composio_api_key
648
- composio_api_key = get_composio_api_key(actor=actor)
664
+ composio_api_key = await get_composio_api_key_async(actor=actor)
649
665
 
650
666
  # TODO (matt): Roll in execute_composio_action into this class
651
667
  function_response = await execute_composio_action_async(
@@ -668,53 +684,35 @@ class ExternalComposioToolExecutor(ToolExecutor):
668
684
  class ExternalMCPToolExecutor(ToolExecutor):
669
685
  """Executor for external MCP tools."""
670
686
 
671
- # TODO: Implement
672
- #
673
- # def execute(self, function_name: str, function_args: dict, agent_state: AgentState, tool: Tool, actor: User) -> ToolExecutionResult:
674
- # # Get the server name from the tool tag
675
- # server_name = self._extract_server_name(tool)
676
- #
677
- # # Get the MCPClient
678
- # mcp_client = self._get_mcp_client(agent, server_name)
679
- #
680
- # # Validate tool exists
681
- # self._validate_tool_exists(mcp_client, function_name, server_name)
682
- #
683
- # # Execute the tool
684
- # function_response, is_error = mcp_client.execute_tool(tool_name=function_name, tool_args=function_args)
685
- #
686
- # return ToolExecutionResult(
687
- # status="error" if is_error else "success",
688
- # func_return=function_response,
689
- # )
690
- #
691
- # def _extract_server_name(self, tool: Tool) -> str:
692
- # """Extract server name from tool tags."""
693
- # return tool.tags[0].split(":")[1]
694
- #
695
- # def _get_mcp_client(self, agent: "Agent", server_name: str):
696
- # """Get the MCP client for the given server name."""
697
- # if not agent.mcp_clients:
698
- # raise ValueError("No MCP client available to use")
699
- #
700
- # if server_name not in agent.mcp_clients:
701
- # raise ValueError(f"Unknown MCP server name: {server_name}")
702
- #
703
- # mcp_client = agent.mcp_clients[server_name]
704
- # if not isinstance(mcp_client, BaseMCPClient):
705
- # raise RuntimeError(f"Expected an MCPClient, but got: {type(mcp_client)}")
706
- #
707
- # return mcp_client
708
- #
709
- # def _validate_tool_exists(self, mcp_client, function_name: str, server_name: str):
710
- # """Validate that the tool exists in the MCP server."""
711
- # available_tools = mcp_client.list_tools()
712
- # available_tool_names = [t.name for t in available_tools]
713
- #
714
- # if function_name not in available_tool_names:
715
- # raise ValueError(
716
- # f"{function_name} is not available in MCP server {server_name}. " f"Please check your `~/.letta/mcp_config.json` file."
717
- # )
687
+ @trace_method
688
+ async def execute(
689
+ self,
690
+ function_name: str,
691
+ function_args: dict,
692
+ tool: Tool,
693
+ actor: User,
694
+ agent_state: Optional[AgentState] = None,
695
+ sandbox_config: Optional[SandboxConfig] = None,
696
+ sandbox_env_vars: Optional[Dict[str, Any]] = None,
697
+ ) -> ToolExecutionResult:
698
+
699
+ pass
700
+
701
+ mcp_server_tag = [tag for tag in tool.tags if tag.startswith(f"{MCP_TOOL_TAG_NAME_PREFIX}:")]
702
+ if not mcp_server_tag:
703
+ raise ValueError(f"Tool {tool.name} does not have a valid MCP server tag")
704
+ mcp_server_name = mcp_server_tag[0].split(":")[1]
705
+
706
+ mcp_manager = MCPManager()
707
+ # TODO: may need to have better client connection management
708
+ function_response, success = await mcp_manager.execute_mcp_server_tool(
709
+ mcp_server_name=mcp_server_name, tool_name=function_name, tool_args=function_args, actor=actor
710
+ )
711
+
712
+ return ToolExecutionResult(
713
+ status="success" if success else "error",
714
+ func_return=function_response,
715
+ )
718
716
 
719
717
 
720
718
  class SandboxToolExecutor(ToolExecutor):
@@ -724,22 +722,22 @@ class SandboxToolExecutor(ToolExecutor):
724
722
  async def execute(
725
723
  self,
726
724
  function_name: str,
727
- function_args: dict,
728
- agent_state: AgentState,
725
+ function_args: JsonDict,
729
726
  tool: Tool,
730
727
  actor: User,
728
+ agent_state: Optional[AgentState] = None,
731
729
  sandbox_config: Optional[SandboxConfig] = None,
732
730
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
733
731
  ) -> ToolExecutionResult:
734
732
 
735
733
  # Store original memory state
736
- orig_memory_str = agent_state.memory.compile()
734
+ orig_memory_str = agent_state.memory.compile() if agent_state else None
737
735
 
738
736
  try:
739
737
  # Prepare function arguments
740
738
  function_args = self._prepare_function_args(function_args, tool, function_name)
741
739
 
742
- agent_state_copy = self._create_agent_state_copy(agent_state)
740
+ agent_state_copy = self._create_agent_state_copy(agent_state) if agent_state else None
743
741
 
744
742
  # Execute in sandbox depending on API key
745
743
  if tool_settings.e2b_api_key:
@@ -754,18 +752,20 @@ class SandboxToolExecutor(ToolExecutor):
754
752
  tool_execution_result = await sandbox.run(agent_state=agent_state_copy)
755
753
 
756
754
  # Verify memory integrity
757
- assert orig_memory_str == agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
755
+ if agent_state:
756
+ assert orig_memory_str == agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
758
757
 
759
758
  # Update agent memory if needed
760
759
  if tool_execution_result.agent_state is not None:
761
- AgentManager().update_memory_if_changed(agent_state.id, tool_execution_result.agent_state.memory, actor)
760
+ await AgentManager().update_memory_if_changed_async(agent_state.id, tool_execution_result.agent_state.memory, actor)
762
761
 
763
762
  return tool_execution_result
764
763
 
765
764
  except Exception as e:
766
765
  return self._handle_execution_error(e, function_name, traceback.format_exc())
767
766
 
768
- def _prepare_function_args(self, function_args: dict, tool: Tool, function_name: str) -> dict:
767
+ @staticmethod
768
+ def _prepare_function_args(function_args: JsonDict, tool: Tool, function_name: str) -> dict:
769
769
  """Prepare function arguments with proper type coercion."""
770
770
  try:
771
771
  # Parse the source code to extract function annotations
@@ -777,7 +777,8 @@ class SandboxToolExecutor(ToolExecutor):
777
777
  # This is defensive programming - we try to coerce but fall back if it fails
778
778
  return function_args
779
779
 
780
- def _create_agent_state_copy(self, agent_state: AgentState):
780
+ @staticmethod
781
+ def _create_agent_state_copy(agent_state: AgentState):
781
782
  """Create a copy of agent state for sandbox execution."""
782
783
  agent_state_copy = agent_state.__deepcopy__()
783
784
  # Remove tools from copy to prevent nested tool execution
@@ -785,8 +786,8 @@ class SandboxToolExecutor(ToolExecutor):
785
786
  agent_state_copy.tool_rules = []
786
787
  return agent_state_copy
787
788
 
789
+ @staticmethod
788
790
  def _handle_execution_error(
789
- self,
790
791
  exception: Exception,
791
792
  function_name: str,
792
793
  stderr: str,
@@ -810,9 +811,9 @@ class LettaBuiltinToolExecutor(ToolExecutor):
810
811
  self,
811
812
  function_name: str,
812
813
  function_args: dict,
813
- agent_state: AgentState,
814
814
  tool: Tool,
815
815
  actor: User,
816
+ agent_state: Optional[AgentState] = None,
816
817
  sandbox_config: Optional[SandboxConfig] = None,
817
818
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
818
819
  ) -> ToolExecutionResult:
@@ -828,6 +829,7 @@ class LettaBuiltinToolExecutor(ToolExecutor):
828
829
  return ToolExecutionResult(
829
830
  status="success",
830
831
  func_return=function_response,
832
+ agent_state=agent_state,
831
833
  )
832
834
 
833
835
  async def run_code(self, code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str: