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.
- letta/__init__.py +1 -1
- letta/agent.py +113 -81
- letta/agents/letta_agent.py +2 -2
- letta/agents/letta_agent_batch.py +38 -34
- letta/client/client.py +10 -2
- letta/constants.py +4 -3
- letta/functions/function_sets/multi_agent.py +1 -3
- letta/functions/helpers.py +3 -3
- letta/groups/dynamic_multi_agent.py +58 -59
- letta/groups/round_robin_multi_agent.py +43 -49
- letta/groups/sleeptime_multi_agent.py +28 -18
- letta/groups/supervisor_multi_agent.py +21 -20
- letta/helpers/composio_helpers.py +1 -1
- letta/helpers/converters.py +29 -0
- letta/helpers/datetime_helpers.py +9 -0
- letta/helpers/message_helper.py +1 -0
- letta/helpers/tool_execution_helper.py +3 -3
- letta/jobs/llm_batch_job_polling.py +2 -1
- letta/llm_api/anthropic.py +10 -6
- letta/llm_api/anthropic_client.py +2 -2
- letta/llm_api/cohere.py +2 -2
- letta/llm_api/google_ai_client.py +2 -2
- letta/llm_api/google_vertex_client.py +2 -2
- letta/llm_api/openai.py +11 -4
- letta/llm_api/openai_client.py +34 -2
- letta/local_llm/chat_completion_proxy.py +2 -2
- letta/orm/agent.py +8 -1
- letta/orm/custom_columns.py +15 -0
- letta/schemas/agent.py +6 -0
- letta/schemas/letta_message_content.py +2 -1
- letta/schemas/llm_config.py +12 -2
- letta/schemas/message.py +18 -0
- letta/schemas/openai/chat_completion_response.py +52 -3
- letta/schemas/response_format.py +78 -0
- letta/schemas/tool_execution_result.py +14 -0
- letta/server/rest_api/chat_completions_interface.py +2 -2
- letta/server/rest_api/interface.py +3 -2
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
- letta/server/rest_api/routers/v1/agents.py +4 -4
- letta/server/rest_api/routers/v1/groups.py +2 -2
- letta/server/rest_api/routers/v1/messages.py +41 -19
- letta/server/server.py +24 -57
- letta/services/agent_manager.py +6 -1
- letta/services/llm_batch_manager.py +28 -26
- letta/services/tool_executor/tool_execution_manager.py +37 -28
- letta/services/tool_executor/tool_execution_sandbox.py +35 -16
- letta/services/tool_executor/tool_executor.py +299 -68
- letta/services/tool_sandbox/base.py +3 -2
- letta/services/tool_sandbox/e2b_sandbox.py +5 -4
- letta/services/tool_sandbox/local_sandbox.py +11 -6
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/METADATA +1 -1
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/RECORD +55 -53
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/WHEEL +0 -0
- {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 | 
| 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 | 
| 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 | 
            -
                ) ->  | 
| 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 | 
            -
                ) ->  | 
| 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  | 
| 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( | 
| 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 | 
            -
                ) ->  | 
| 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  | 
| 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) ->  | 
| 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 | 
            -
                #      | 
| 320 | 
            -
                # | 
| 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 | 
            -
                ) ->  | 
| 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 | 
            -
                         | 
| 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  | 
| 393 | 
            -
                            AgentManager().update_memory_if_changed(agent_state.id,  | 
| 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  | 
| 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( | 
| 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  | 
| 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 | 
| 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 | 
            -
                ) ->  | 
| 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,  | 
| 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 | 
            -
                ) ->  | 
| 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 | 
            -
                ) ->  | 
| 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  | 
| 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,  | 
| 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 | 
            -
                ) ->  | 
| 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( | 
| 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 | 
            -
                ) ->  | 
| 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  | 
| 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  | 
| 217 | 
            +
                        return ToolExecutionResult(
         | 
| 213 218 | 
             
                            func_return=func_return,
         | 
| 214 219 | 
             
                            agent_state=None,
         | 
| 215 220 | 
             
                            stdout=[],
         |