letta-nightly 0.7.0.dev20250423003112__py3-none-any.whl → 0.7.2.dev20250423222439__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 (55) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +113 -81
  3. letta/agents/letta_agent.py +2 -2
  4. letta/agents/letta_agent_batch.py +38 -34
  5. letta/client/client.py +10 -2
  6. letta/constants.py +4 -3
  7. letta/functions/function_sets/multi_agent.py +1 -3
  8. letta/functions/helpers.py +3 -3
  9. letta/groups/dynamic_multi_agent.py +58 -59
  10. letta/groups/round_robin_multi_agent.py +43 -49
  11. letta/groups/sleeptime_multi_agent.py +28 -18
  12. letta/groups/supervisor_multi_agent.py +21 -20
  13. letta/helpers/composio_helpers.py +1 -1
  14. letta/helpers/converters.py +29 -0
  15. letta/helpers/datetime_helpers.py +9 -0
  16. letta/helpers/message_helper.py +1 -0
  17. letta/helpers/tool_execution_helper.py +3 -3
  18. letta/jobs/llm_batch_job_polling.py +2 -1
  19. letta/llm_api/anthropic.py +10 -6
  20. letta/llm_api/anthropic_client.py +2 -2
  21. letta/llm_api/cohere.py +2 -2
  22. letta/llm_api/google_ai_client.py +2 -2
  23. letta/llm_api/google_vertex_client.py +2 -2
  24. letta/llm_api/openai.py +11 -4
  25. letta/llm_api/openai_client.py +34 -2
  26. letta/local_llm/chat_completion_proxy.py +2 -2
  27. letta/orm/agent.py +8 -1
  28. letta/orm/custom_columns.py +15 -0
  29. letta/schemas/agent.py +6 -0
  30. letta/schemas/letta_message_content.py +2 -1
  31. letta/schemas/llm_config.py +12 -2
  32. letta/schemas/message.py +18 -0
  33. letta/schemas/openai/chat_completion_response.py +52 -3
  34. letta/schemas/response_format.py +78 -0
  35. letta/schemas/tool_execution_result.py +14 -0
  36. letta/server/rest_api/chat_completions_interface.py +2 -2
  37. letta/server/rest_api/interface.py +3 -2
  38. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
  39. letta/server/rest_api/routers/v1/agents.py +4 -4
  40. letta/server/rest_api/routers/v1/groups.py +2 -2
  41. letta/server/rest_api/routers/v1/messages.py +41 -19
  42. letta/server/server.py +24 -57
  43. letta/services/agent_manager.py +6 -1
  44. letta/services/llm_batch_manager.py +28 -26
  45. letta/services/tool_executor/tool_execution_manager.py +37 -28
  46. letta/services/tool_executor/tool_execution_sandbox.py +35 -16
  47. letta/services/tool_executor/tool_executor.py +299 -68
  48. letta/services/tool_sandbox/base.py +3 -2
  49. letta/services/tool_sandbox/e2b_sandbox.py +5 -4
  50. letta/services/tool_sandbox/local_sandbox.py +11 -6
  51. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/METADATA +1 -1
  52. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/RECORD +55 -53
  53. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/LICENSE +0 -0
  54. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/WHEEL +0 -0
  55. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,17 @@
1
1
  import math
2
+ import traceback
2
3
  from abc import ABC, abstractmethod
3
- from typing import Any, Dict, Optional, Tuple
4
+ from typing import Any, Dict, Optional
4
5
 
5
- from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
6
+ from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY, CORE_MEMORY_LINE_NUMBER_WARNING, RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
6
7
  from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
7
8
  from letta.functions.helpers import execute_composio_action, generate_composio_action_from_func_name
8
9
  from letta.helpers.composio_helpers import get_composio_api_key
9
10
  from letta.helpers.json_helpers import json_dumps
10
11
  from letta.schemas.agent import AgentState
11
- from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult
12
+ from letta.schemas.sandbox_config import SandboxConfig
12
13
  from letta.schemas.tool import Tool
14
+ from letta.schemas.tool_execution_result import ToolExecutionResult
13
15
  from letta.schemas.user import User
14
16
  from letta.services.agent_manager import AgentManager
15
17
  from letta.services.message_manager import MessageManager
@@ -33,7 +35,7 @@ class ToolExecutor(ABC):
33
35
  actor: User,
34
36
  sandbox_config: Optional[SandboxConfig] = None,
35
37
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
36
- ) -> Tuple[Any, Optional[SandboxRunResult]]:
38
+ ) -> ToolExecutionResult:
37
39
  """Execute the tool and return the result."""
38
40
 
39
41
 
@@ -49,13 +51,19 @@ class LettaCoreToolExecutor(ToolExecutor):
49
51
  actor: User,
50
52
  sandbox_config: Optional[SandboxConfig] = None,
51
53
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
52
- ) -> Tuple[Any, Optional[SandboxRunResult]]:
54
+ ) -> ToolExecutionResult:
53
55
  # Map function names to method calls
54
56
  function_map = {
55
57
  "send_message": self.send_message,
56
58
  "conversation_search": self.conversation_search,
57
59
  "archival_memory_search": self.archival_memory_search,
58
60
  "archival_memory_insert": self.archival_memory_insert,
61
+ "core_memory_append": self.core_memory_append,
62
+ "core_memory_replace": self.core_memory_replace,
63
+ "memory_replace": self.memory_replace,
64
+ "memory_insert": self.memory_insert,
65
+ "memory_rethink": self.memory_rethink,
66
+ "memory_finish_edits": self.memory_finish_edits,
59
67
  }
60
68
 
61
69
  if function_name not in function_map:
@@ -64,7 +72,10 @@ class LettaCoreToolExecutor(ToolExecutor):
64
72
  # Execute the appropriate function
65
73
  function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
66
74
  function_response = function_map[function_name](agent_state, actor, **function_args_copy)
67
- return function_response, None
75
+ return ToolExecutionResult(
76
+ status="success",
77
+ func_return=function_response,
78
+ )
68
79
 
69
80
  def send_message(self, agent_state: AgentState, actor: User, message: str) -> Optional[str]:
70
81
  """
@@ -181,51 +192,7 @@ class LettaCoreToolExecutor(ToolExecutor):
181
192
  AgentManager().rebuild_system_prompt(agent_id=agent_state.id, actor=actor, force=True)
182
193
  return None
183
194
 
184
-
185
- class LettaMultiAgentToolExecutor(ToolExecutor):
186
- """Executor for LETTA multi-agent core tools."""
187
-
188
- # TODO: Implement
189
- # def execute(self, function_name: str, function_args: dict, agent: "Agent", tool: Tool) -> Tuple[
190
- # Any, Optional[SandboxRunResult]]:
191
- # callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name)
192
- # function_args["self"] = agent # need to attach self to arg since it's dynamically linked
193
- # function_response = callable_func(**function_args)
194
- # return function_response, None
195
-
196
-
197
- class LettaMemoryToolExecutor(ToolExecutor):
198
- """Executor for LETTA memory core tools with direct implementation."""
199
-
200
- def execute(
201
- self,
202
- function_name: str,
203
- function_args: dict,
204
- agent_state: AgentState,
205
- tool: Tool,
206
- actor: User,
207
- sandbox_config: Optional[SandboxConfig] = None,
208
- sandbox_env_vars: Optional[Dict[str, Any]] = None,
209
- ) -> Tuple[Any, Optional[SandboxRunResult]]:
210
- # Map function names to method calls
211
- function_map = {
212
- "core_memory_append": self.core_memory_append,
213
- "core_memory_replace": self.core_memory_replace,
214
- }
215
-
216
- if function_name not in function_map:
217
- raise ValueError(f"Unknown function: {function_name}")
218
-
219
- # Execute the appropriate function with the copied state
220
- function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
221
- function_response = function_map[function_name](agent_state, **function_args_copy)
222
-
223
- # Update memory if changed
224
- AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
225
-
226
- return function_response, None
227
-
228
- def core_memory_append(self, agent_state: "AgentState", label: str, content: str) -> Optional[str]:
195
+ def core_memory_append(self, agent_state: "AgentState", actor: User, label: str, content: str) -> Optional[str]:
229
196
  """
230
197
  Append to the contents of core memory.
231
198
 
@@ -239,9 +206,17 @@ class LettaMemoryToolExecutor(ToolExecutor):
239
206
  current_value = str(agent_state.memory.get_block(label).value)
240
207
  new_value = current_value + "\n" + str(content)
241
208
  agent_state.memory.update_block_value(label=label, value=new_value)
209
+ AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
242
210
  return None
243
211
 
244
- def core_memory_replace(self, agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]:
212
+ def core_memory_replace(
213
+ self,
214
+ agent_state: "AgentState",
215
+ actor: User,
216
+ label: str,
217
+ old_content: str,
218
+ new_content: str,
219
+ ) -> Optional[str]:
245
220
  """
246
221
  Replace the contents of core memory. To delete memories, use an empty string for new_content.
247
222
 
@@ -258,8 +233,253 @@ class LettaMemoryToolExecutor(ToolExecutor):
258
233
  raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
259
234
  new_value = current_value.replace(str(old_content), str(new_content))
260
235
  agent_state.memory.update_block_value(label=label, value=new_value)
236
+ AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
261
237
  return None
262
238
 
239
+ def memory_replace(
240
+ agent_state: "AgentState",
241
+ actor: User,
242
+ label: str,
243
+ old_str: str,
244
+ new_str: Optional[str] = None,
245
+ ) -> str:
246
+ """
247
+ The memory_replace command allows you to replace a specific string in a memory
248
+ block with a new string. This is used for making precise edits.
249
+
250
+ Args:
251
+ label (str): Section of the memory to be edited, identified by its label.
252
+ old_str (str): The text to replace (must match exactly, including whitespace
253
+ and indentation).
254
+ new_str (Optional[str]): The new text to insert in place of the old text.
255
+ Omit this argument to delete the old_str.
256
+
257
+ Returns:
258
+ str: The success message
259
+ """
260
+ import re
261
+
262
+ if bool(re.search(r"\nLine \d+: ", old_str)):
263
+ raise ValueError(
264
+ "old_str contains a line number prefix, which is not allowed. "
265
+ "Do not include line numbers when calling memory tools (line "
266
+ "numbers are for display purposes only)."
267
+ )
268
+ if CORE_MEMORY_LINE_NUMBER_WARNING in old_str:
269
+ raise ValueError(
270
+ "old_str contains a line number warning, which is not allowed. "
271
+ "Do not include line number information when calling memory tools "
272
+ "(line numbers are for display purposes only)."
273
+ )
274
+ if bool(re.search(r"\nLine \d+: ", new_str)):
275
+ raise ValueError(
276
+ "new_str contains a line number prefix, which is not allowed. "
277
+ "Do not include line numbers when calling memory tools (line "
278
+ "numbers are for display purposes only)."
279
+ )
280
+
281
+ old_str = str(old_str).expandtabs()
282
+ new_str = str(new_str).expandtabs()
283
+ current_value = str(agent_state.memory.get_block(label).value).expandtabs()
284
+
285
+ # Check if old_str is unique in the block
286
+ occurences = current_value.count(old_str)
287
+ if occurences == 0:
288
+ raise ValueError(
289
+ f"No replacement was performed, old_str `{old_str}` did not appear " f"verbatim in memory block with label `{label}`."
290
+ )
291
+ elif occurences > 1:
292
+ content_value_lines = current_value.split("\n")
293
+ lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_str in line]
294
+ raise ValueError(
295
+ f"No replacement was performed. Multiple occurrences of "
296
+ f"old_str `{old_str}` in lines {lines}. Please ensure it is unique."
297
+ )
298
+
299
+ # Replace old_str with new_str
300
+ new_value = current_value.replace(str(old_str), str(new_str))
301
+
302
+ # Write the new content to the block
303
+ agent_state.memory.update_block_value(label=label, value=new_value)
304
+
305
+ AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
306
+
307
+ # Create a snippet of the edited section
308
+ SNIPPET_LINES = 3
309
+ replacement_line = current_value.split(old_str)[0].count("\n")
310
+ start_line = max(0, replacement_line - SNIPPET_LINES)
311
+ end_line = replacement_line + SNIPPET_LINES + new_str.count("\n")
312
+ snippet = "\n".join(new_value.split("\n")[start_line : end_line + 1])
313
+
314
+ # Prepare the success message
315
+ success_msg = f"The core memory block with label `{label}` has been edited. "
316
+ # success_msg += self._make_output(
317
+ # snippet, f"a snippet of {path}", start_line + 1
318
+ # )
319
+ # success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
320
+ success_msg += (
321
+ "Review the changes and make sure they are as expected (correct indentation, "
322
+ "no duplicate lines, etc). Edit the memory block again if necessary."
323
+ )
324
+
325
+ # return None
326
+ return success_msg
327
+
328
+ def memory_insert(
329
+ agent_state: "AgentState",
330
+ actor: User,
331
+ label: str,
332
+ new_str: str,
333
+ insert_line: int = -1,
334
+ ) -> str:
335
+ """
336
+ The memory_insert command allows you to insert text at a specific location
337
+ in a memory block.
338
+
339
+ Args:
340
+ label (str): Section of the memory to be edited, identified by its label.
341
+ new_str (str): The text to insert.
342
+ insert_line (int): The line number after which to insert the text (0 for
343
+ beginning of file). Defaults to -1 (end of the file).
344
+
345
+ Returns:
346
+ str: The success message
347
+ """
348
+ import re
349
+
350
+ if bool(re.search(r"\nLine \d+: ", new_str)):
351
+ raise ValueError(
352
+ "new_str contains a line number prefix, which is not allowed. Do not "
353
+ "include line numbers when calling memory tools (line numbers are for "
354
+ "display purposes only)."
355
+ )
356
+ if CORE_MEMORY_LINE_NUMBER_WARNING in new_str:
357
+ raise ValueError(
358
+ "new_str contains a line number warning, which is not allowed. Do not "
359
+ "include line number information when calling memory tools (line numbers "
360
+ "are for display purposes only)."
361
+ )
362
+
363
+ current_value = str(agent_state.memory.get_block(label).value).expandtabs()
364
+ new_str = str(new_str).expandtabs()
365
+ current_value_lines = current_value.split("\n")
366
+ n_lines = len(current_value_lines)
367
+
368
+ # Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
369
+ if insert_line < 0 or insert_line > n_lines:
370
+ raise ValueError(
371
+ f"Invalid `insert_line` parameter: {insert_line}. It should be within "
372
+ f"the range of lines of the memory block: {[0, n_lines]}, or -1 to "
373
+ f"append to the end of the memory block."
374
+ )
375
+
376
+ # Insert the new string as a line
377
+ SNIPPET_LINES = 3
378
+ new_str_lines = new_str.split("\n")
379
+ new_value_lines = current_value_lines[:insert_line] + new_str_lines + current_value_lines[insert_line:]
380
+ snippet_lines = (
381
+ current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
382
+ + new_str_lines
383
+ + current_value_lines[insert_line : insert_line + SNIPPET_LINES]
384
+ )
385
+
386
+ # Collate into the new value to update
387
+ new_value = "\n".join(new_value_lines)
388
+ snippet = "\n".join(snippet_lines)
389
+
390
+ # Write into the block
391
+ agent_state.memory.update_block_value(label=label, value=new_value)
392
+
393
+ AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
394
+
395
+ # Prepare the success message
396
+ success_msg = f"The core memory block with label `{label}` has been edited. "
397
+ # success_msg += self._make_output(
398
+ # snippet,
399
+ # "a snippet of the edited file",
400
+ # max(1, insert_line - SNIPPET_LINES + 1),
401
+ # )
402
+ # success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
403
+ success_msg += (
404
+ "Review the changes and make sure they are as expected (correct indentation, "
405
+ "no duplicate lines, etc). Edit the memory block again if necessary."
406
+ )
407
+
408
+ return success_msg
409
+
410
+ def memory_rethink(agent_state: "AgentState", actor: User, label: str, new_memory: str) -> str:
411
+ """
412
+ The memory_rethink command allows you to completely rewrite the contents of a
413
+ memory block. Use this tool to make large sweeping changes (e.g. when you want
414
+ to condense or reorganize the memory blocks), do NOT use this tool to make small
415
+ precise edits (e.g. add or remove a line, replace a specific string, etc).
416
+
417
+ Args:
418
+ label (str): The memory block to be rewritten, identified by its label.
419
+ new_memory (str): The new memory contents with information integrated from
420
+ existing memory blocks and the conversation context.
421
+
422
+ Returns:
423
+ str: The success message
424
+ """
425
+ import re
426
+
427
+ if bool(re.search(r"\nLine \d+: ", new_memory)):
428
+ raise ValueError(
429
+ "new_memory contains a line number prefix, which is not allowed. Do not "
430
+ "include line numbers when calling memory tools (line numbers are for "
431
+ "display purposes only)."
432
+ )
433
+ if CORE_MEMORY_LINE_NUMBER_WARNING in new_memory:
434
+ raise ValueError(
435
+ "new_memory contains a line number warning, which is not allowed. Do not "
436
+ "include line number information when calling memory tools (line numbers "
437
+ "are for display purposes only)."
438
+ )
439
+
440
+ if agent_state.memory.get_block(label) is None:
441
+ agent_state.memory.create_block(label=label, value=new_memory)
442
+
443
+ agent_state.memory.update_block_value(label=label, value=new_memory)
444
+
445
+ AgentManager().update_memory_if_changed(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
446
+
447
+ # Prepare the success message
448
+ success_msg = f"The core memory block with label `{label}` has been edited. "
449
+ # success_msg += self._make_output(
450
+ # snippet, f"a snippet of {path}", start_line + 1
451
+ # )
452
+ # success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
453
+ success_msg += (
454
+ "Review the changes and make sure they are as expected (correct indentation, "
455
+ "no duplicate lines, etc). Edit the memory block again if necessary."
456
+ )
457
+
458
+ # return None
459
+ return success_msg
460
+
461
+ def memory_finish_edits(agent_state: "AgentState") -> None:
462
+ """
463
+ Call the memory_finish_edits command when you are finished making edits
464
+ (integrating all new information) into the memory blocks. This function
465
+ is called when the agent is done rethinking the memory.
466
+
467
+ Returns:
468
+ Optional[str]: None is always returned as this function does not produce a response.
469
+ """
470
+ return None
471
+
472
+
473
+ class LettaMultiAgentToolExecutor(ToolExecutor):
474
+ """Executor for LETTA multi-agent core tools."""
475
+
476
+ # TODO: Implement
477
+ # def execute(self, function_name: str, function_args: dict, agent: "Agent", tool: Tool) -> ToolExecutionResult:
478
+ # callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name)
479
+ # function_args["self"] = agent # need to attach self to arg since it's dynamically linked
480
+ # function_response = callable_func(**function_args)
481
+ # return ToolExecutionResult(func_return=function_response)
482
+
263
483
 
264
484
  class ExternalComposioToolExecutor(ToolExecutor):
265
485
  """Executor for external Composio tools."""
@@ -273,7 +493,7 @@ class ExternalComposioToolExecutor(ToolExecutor):
273
493
  actor: User,
274
494
  sandbox_config: Optional[SandboxConfig] = None,
275
495
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
276
- ) -> Tuple[Any, Optional[SandboxRunResult]]:
496
+ ) -> ToolExecutionResult:
277
497
  action_name = generate_composio_action_from_func_name(tool.name)
278
498
 
279
499
  # Get entity ID from the agent_state
@@ -287,7 +507,10 @@ class ExternalComposioToolExecutor(ToolExecutor):
287
507
  action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
288
508
  )
289
509
 
290
- return function_response, None
510
+ return ToolExecutionResult(
511
+ status="success",
512
+ func_return=function_response,
513
+ )
291
514
 
292
515
  def _get_entity_id(self, agent_state: AgentState) -> Optional[str]:
293
516
  """Extract the entity ID from environment variables."""
@@ -302,8 +525,7 @@ class ExternalMCPToolExecutor(ToolExecutor):
302
525
 
303
526
  # TODO: Implement
304
527
  #
305
- # def execute(self, function_name: str, function_args: dict, agent_state: AgentState, tool: Tool, actor: User) -> Tuple[
306
- # Any, Optional[SandboxRunResult]]:
528
+ # def execute(self, function_name: str, function_args: dict, agent_state: AgentState, tool: Tool, actor: User) -> ToolExecutionResult:
307
529
  # # Get the server name from the tool tag
308
530
  # server_name = self._extract_server_name(tool)
309
531
  #
@@ -316,8 +538,10 @@ class ExternalMCPToolExecutor(ToolExecutor):
316
538
  # # Execute the tool
317
539
  # function_response, is_error = mcp_client.execute_tool(tool_name=function_name, tool_args=function_args)
318
540
  #
319
- # sandbox_run_result = SandboxRunResult(status="error" if is_error else "success")
320
- # return function_response, sandbox_run_result
541
+ # return ToolExecutionResult(
542
+ # status="error" if is_error else "success",
543
+ # func_return=function_response,
544
+ # )
321
545
  #
322
546
  # def _extract_server_name(self, tool: Tool) -> str:
323
547
  # """Extract server name from tool tags."""
@@ -360,7 +584,7 @@ class SandboxToolExecutor(ToolExecutor):
360
584
  actor: User,
361
585
  sandbox_config: Optional[SandboxConfig] = None,
362
586
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
363
- ) -> Tuple[Any, Optional[SandboxRunResult]]:
587
+ ) -> ToolExecutionResult:
364
588
 
365
589
  # Store original memory state
366
590
  orig_memory_str = agent_state.memory.compile()
@@ -381,21 +605,19 @@ class SandboxToolExecutor(ToolExecutor):
381
605
  function_name, function_args, actor, tool_object=tool, sandbox_config=sandbox_config, sandbox_env_vars=sandbox_env_vars
382
606
  )
383
607
 
384
- sandbox_run_result = await sandbox.run(agent_state=agent_state_copy)
385
-
386
- function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
608
+ tool_execution_result = await sandbox.run(agent_state=agent_state_copy)
387
609
 
388
610
  # Verify memory integrity
389
611
  assert orig_memory_str == agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
390
612
 
391
613
  # Update agent memory if needed
392
- if updated_agent_state is not None:
393
- AgentManager().update_memory_if_changed(agent_state.id, updated_agent_state.memory, actor)
614
+ if tool_execution_result.agent_state is not None:
615
+ AgentManager().update_memory_if_changed(agent_state.id, tool_execution_result.agent_state.memory, actor)
394
616
 
395
- return function_response, sandbox_run_result
617
+ return tool_execution_result
396
618
 
397
619
  except Exception as e:
398
- return self._handle_execution_error(e, function_name)
620
+ return self._handle_execution_error(e, function_name, traceback.format_exc())
399
621
 
400
622
  def _prepare_function_args(self, function_args: dict, tool: Tool, function_name: str) -> dict:
401
623
  """Prepare function arguments with proper type coercion."""
@@ -417,9 +639,18 @@ class SandboxToolExecutor(ToolExecutor):
417
639
  agent_state_copy.tool_rules = []
418
640
  return agent_state_copy
419
641
 
420
- def _handle_execution_error(self, exception: Exception, function_name: str) -> Tuple[str, SandboxRunResult]:
642
+ def _handle_execution_error(
643
+ self,
644
+ exception: Exception,
645
+ function_name: str,
646
+ stderr: str,
647
+ ) -> ToolExecutionResult:
421
648
  """Handle tool execution errors."""
422
649
  error_message = get_friendly_error_msg(
423
650
  function_name=function_name, exception_name=type(exception).__name__, exception_message=str(exception)
424
651
  )
425
- return error_message, SandboxRunResult(status="error")
652
+ return ToolExecutionResult(
653
+ status="error",
654
+ func_return=error_message,
655
+ stderr=[stderr],
656
+ )
@@ -7,8 +7,9 @@ from typing import Any, Dict, Optional, Tuple
7
7
 
8
8
  from letta.functions.helpers import generate_model_from_args_json_schema
9
9
  from letta.schemas.agent import AgentState
10
- from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult
10
+ from letta.schemas.sandbox_config import SandboxConfig
11
11
  from letta.schemas.tool import Tool
12
+ from letta.schemas.tool_execution_result import ToolExecutionResult
12
13
  from letta.services.helpers.tool_execution_helper import add_imports_and_pydantic_schemas_for_args
13
14
  from letta.services.sandbox_config_manager import SandboxConfigManager
14
15
  from letta.services.tool_manager import ToolManager
@@ -64,7 +65,7 @@ class AsyncToolSandboxBase(ABC):
64
65
  self,
65
66
  agent_state: Optional[AgentState] = None,
66
67
  additional_env_vars: Optional[Dict] = None,
67
- ) -> SandboxRunResult:
68
+ ) -> ToolExecutionResult:
68
69
  """
69
70
  Run the tool in a sandbox environment asynchronously.
70
71
  Must be implemented by subclasses.
@@ -2,8 +2,9 @@ from typing import Any, Dict, Optional
2
2
 
3
3
  from letta.log import get_logger
4
4
  from letta.schemas.agent import AgentState
5
- from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
5
+ from letta.schemas.sandbox_config import SandboxConfig, SandboxType
6
6
  from letta.schemas.tool import Tool
7
+ from letta.schemas.tool_execution_result import ToolExecutionResult
7
8
  from letta.services.tool_sandbox.base import AsyncToolSandboxBase
8
9
  from letta.utils import get_friendly_error_msg
9
10
 
@@ -30,7 +31,7 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
30
31
  self,
31
32
  agent_state: Optional[AgentState] = None,
32
33
  additional_env_vars: Optional[Dict] = None,
33
- ) -> SandboxRunResult:
34
+ ) -> ToolExecutionResult:
34
35
  """
35
36
  Run the tool in a sandbox environment asynchronously,
36
37
  *always* using a subprocess for execution.
@@ -45,7 +46,7 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
45
46
 
46
47
  async def run_e2b_sandbox(
47
48
  self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None
48
- ) -> SandboxRunResult:
49
+ ) -> ToolExecutionResult:
49
50
  if self.provided_sandbox_config:
50
51
  sbx_config = self.provided_sandbox_config
51
52
  else:
@@ -94,7 +95,7 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
94
95
  else:
95
96
  raise ValueError(f"Tool {self.tool_name} returned execution with None")
96
97
 
97
- return SandboxRunResult(
98
+ return ToolExecutionResult(
98
99
  func_return=func_return,
99
100
  agent_state=agent_state,
100
101
  stdout=execution.logs.stdout,
@@ -5,8 +5,9 @@ import tempfile
5
5
  from typing import Any, Dict, Optional, Tuple
6
6
 
7
7
  from letta.schemas.agent import AgentState
8
- from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
8
+ from letta.schemas.sandbox_config import SandboxConfig, SandboxType
9
9
  from letta.schemas.tool import Tool
10
+ from letta.schemas.tool_execution_result import ToolExecutionResult
10
11
  from letta.services.helpers.tool_execution_helper import (
11
12
  create_venv_for_local_sandbox,
12
13
  find_python_executable,
@@ -39,7 +40,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
39
40
  self,
40
41
  agent_state: Optional[AgentState] = None,
41
42
  additional_env_vars: Optional[Dict] = None,
42
- ) -> SandboxRunResult:
43
+ ) -> ToolExecutionResult:
43
44
  """
44
45
  Run the tool in a sandbox environment asynchronously,
45
46
  *always* using a subprocess for execution.
@@ -53,7 +54,11 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
53
54
  return result
54
55
 
55
56
  @trace_method
56
- async def run_local_dir_sandbox(self, agent_state: Optional[AgentState], additional_env_vars: Optional[Dict]) -> SandboxRunResult:
57
+ async def run_local_dir_sandbox(
58
+ self,
59
+ agent_state: Optional[AgentState],
60
+ additional_env_vars: Optional[Dict],
61
+ ) -> ToolExecutionResult:
57
62
  """
58
63
  Unified asynchronougit pus method to run the tool in a local sandbox environment,
59
64
  always via subprocess for multi-core parallelism.
@@ -156,7 +161,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
156
161
  @trace_method
157
162
  async def _execute_tool_subprocess(
158
163
  self, sbx_config, python_executable: str, temp_file_path: str, env: Dict[str, str], cwd: str
159
- ) -> SandboxRunResult:
164
+ ) -> ToolExecutionResult:
160
165
  """
161
166
  Execute user code in a subprocess, always capturing stdout and stderr.
162
167
  We parse special markers to extract the pickled result string.
@@ -189,7 +194,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
189
194
  func_result, stdout_text = self.parse_out_function_results_markers(stdout)
190
195
  func_return, agent_state = self.parse_best_effort(func_result)
191
196
 
192
- return SandboxRunResult(
197
+ return ToolExecutionResult(
193
198
  func_return=func_return,
194
199
  agent_state=agent_state,
195
200
  stdout=[stdout_text] if stdout_text else [],
@@ -209,7 +214,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
209
214
  exception_name=type(e).__name__,
210
215
  exception_message=str(e),
211
216
  )
212
- return SandboxRunResult(
217
+ return ToolExecutionResult(
213
218
  func_return=func_return,
214
219
  agent_state=None,
215
220
  stdout=[],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.7.0.dev20250423003112
3
+ Version: 0.7.2.dev20250423222439
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team