letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.11.7.dev20251008104128__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 (145) hide show
  1. letta/adapters/letta_llm_adapter.py +1 -0
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +7 -2
  4. letta/adapters/simple_llm_request_adapter.py +88 -0
  5. letta/adapters/simple_llm_stream_adapter.py +192 -0
  6. letta/agents/agent_loop.py +6 -0
  7. letta/agents/ephemeral_summary_agent.py +2 -1
  8. letta/agents/helpers.py +142 -6
  9. letta/agents/letta_agent.py +13 -33
  10. letta/agents/letta_agent_batch.py +2 -4
  11. letta/agents/letta_agent_v2.py +87 -77
  12. letta/agents/letta_agent_v3.py +899 -0
  13. letta/agents/voice_agent.py +2 -6
  14. letta/constants.py +8 -4
  15. letta/errors.py +40 -0
  16. letta/functions/function_sets/base.py +84 -4
  17. letta/functions/function_sets/multi_agent.py +0 -3
  18. letta/functions/schema_generator.py +113 -71
  19. letta/groups/dynamic_multi_agent.py +3 -2
  20. letta/groups/helpers.py +1 -2
  21. letta/groups/round_robin_multi_agent.py +3 -2
  22. letta/groups/sleeptime_multi_agent.py +3 -2
  23. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  24. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  25. letta/groups/supervisor_multi_agent.py +84 -80
  26. letta/helpers/converters.py +3 -0
  27. letta/helpers/message_helper.py +4 -0
  28. letta/helpers/tool_rule_solver.py +92 -5
  29. letta/interfaces/anthropic_streaming_interface.py +409 -0
  30. letta/interfaces/gemini_streaming_interface.py +296 -0
  31. letta/interfaces/openai_streaming_interface.py +752 -1
  32. letta/llm_api/anthropic_client.py +126 -16
  33. letta/llm_api/bedrock_client.py +4 -2
  34. letta/llm_api/deepseek_client.py +4 -1
  35. letta/llm_api/google_vertex_client.py +123 -42
  36. letta/llm_api/groq_client.py +4 -1
  37. letta/llm_api/llm_api_tools.py +11 -4
  38. letta/llm_api/llm_client_base.py +6 -2
  39. letta/llm_api/openai.py +32 -2
  40. letta/llm_api/openai_client.py +423 -18
  41. letta/llm_api/xai_client.py +4 -1
  42. letta/main.py +9 -5
  43. letta/memory.py +1 -0
  44. letta/orm/__init__.py +1 -1
  45. letta/orm/agent.py +10 -0
  46. letta/orm/block.py +7 -16
  47. letta/orm/blocks_agents.py +8 -2
  48. letta/orm/files_agents.py +2 -0
  49. letta/orm/job.py +7 -5
  50. letta/orm/mcp_oauth.py +1 -0
  51. letta/orm/message.py +21 -6
  52. letta/orm/organization.py +2 -0
  53. letta/orm/provider.py +6 -2
  54. letta/orm/run.py +71 -0
  55. letta/orm/sandbox_config.py +7 -1
  56. letta/orm/sqlalchemy_base.py +0 -306
  57. letta/orm/step.py +6 -5
  58. letta/orm/step_metrics.py +5 -5
  59. letta/otel/tracing.py +28 -3
  60. letta/plugins/defaults.py +4 -4
  61. letta/prompts/system_prompts/__init__.py +2 -0
  62. letta/prompts/system_prompts/letta_v1.py +25 -0
  63. letta/schemas/agent.py +3 -2
  64. letta/schemas/agent_file.py +9 -3
  65. letta/schemas/block.py +23 -10
  66. letta/schemas/enums.py +21 -2
  67. letta/schemas/job.py +17 -4
  68. letta/schemas/letta_message_content.py +71 -2
  69. letta/schemas/letta_stop_reason.py +5 -5
  70. letta/schemas/llm_config.py +53 -3
  71. letta/schemas/memory.py +1 -1
  72. letta/schemas/message.py +504 -117
  73. letta/schemas/openai/responses_request.py +64 -0
  74. letta/schemas/providers/__init__.py +2 -0
  75. letta/schemas/providers/anthropic.py +16 -0
  76. letta/schemas/providers/ollama.py +115 -33
  77. letta/schemas/providers/openrouter.py +52 -0
  78. letta/schemas/providers/vllm.py +2 -1
  79. letta/schemas/run.py +48 -42
  80. letta/schemas/step.py +2 -2
  81. letta/schemas/step_metrics.py +1 -1
  82. letta/schemas/tool.py +15 -107
  83. letta/schemas/tool_rule.py +88 -5
  84. letta/serialize_schemas/marshmallow_agent.py +1 -0
  85. letta/server/db.py +86 -408
  86. letta/server/rest_api/app.py +61 -10
  87. letta/server/rest_api/dependencies.py +14 -0
  88. letta/server/rest_api/redis_stream_manager.py +19 -8
  89. letta/server/rest_api/routers/v1/agents.py +364 -292
  90. letta/server/rest_api/routers/v1/blocks.py +14 -20
  91. letta/server/rest_api/routers/v1/identities.py +45 -110
  92. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  93. letta/server/rest_api/routers/v1/jobs.py +23 -6
  94. letta/server/rest_api/routers/v1/messages.py +1 -1
  95. letta/server/rest_api/routers/v1/runs.py +126 -85
  96. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  97. letta/server/rest_api/routers/v1/tools.py +281 -594
  98. letta/server/rest_api/routers/v1/voice.py +1 -1
  99. letta/server/rest_api/streaming_response.py +29 -29
  100. letta/server/rest_api/utils.py +122 -64
  101. letta/server/server.py +160 -887
  102. letta/services/agent_manager.py +236 -919
  103. letta/services/agent_serialization_manager.py +16 -0
  104. letta/services/archive_manager.py +0 -100
  105. letta/services/block_manager.py +211 -168
  106. letta/services/file_manager.py +1 -1
  107. letta/services/files_agents_manager.py +24 -33
  108. letta/services/group_manager.py +0 -142
  109. letta/services/helpers/agent_manager_helper.py +7 -2
  110. letta/services/helpers/run_manager_helper.py +85 -0
  111. letta/services/job_manager.py +96 -411
  112. letta/services/lettuce/__init__.py +6 -0
  113. letta/services/lettuce/lettuce_client_base.py +86 -0
  114. letta/services/mcp_manager.py +38 -6
  115. letta/services/message_manager.py +165 -362
  116. letta/services/organization_manager.py +0 -36
  117. letta/services/passage_manager.py +0 -345
  118. letta/services/provider_manager.py +0 -80
  119. letta/services/run_manager.py +301 -0
  120. letta/services/sandbox_config_manager.py +0 -234
  121. letta/services/step_manager.py +62 -39
  122. letta/services/summarizer/summarizer.py +9 -7
  123. letta/services/telemetry_manager.py +0 -16
  124. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  125. letta/services/tool_executor/core_tool_executor.py +397 -2
  126. letta/services/tool_executor/files_tool_executor.py +3 -3
  127. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  128. letta/services/tool_executor/tool_execution_manager.py +6 -8
  129. letta/services/tool_executor/tool_executor_base.py +3 -3
  130. letta/services/tool_manager.py +85 -339
  131. letta/services/tool_sandbox/base.py +24 -13
  132. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  133. letta/services/tool_schema_generator.py +123 -0
  134. letta/services/user_manager.py +0 -99
  135. letta/settings.py +20 -4
  136. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
  137. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
  138. letta/agents/temporal/activities/__init__.py +0 -4
  139. letta/agents/temporal/activities/example_activity.py +0 -7
  140. letta/agents/temporal/activities/prepare_messages.py +0 -10
  141. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  142. letta/agents/temporal/types.py +0 -25
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
  144. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
  145. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,7 @@ from letta.constants import (
11
11
  from letta.helpers.json_helpers import json_dumps
12
12
  from letta.log import get_logger
13
13
  from letta.schemas.agent import AgentState
14
+ from letta.schemas.block import BlockUpdate
14
15
  from letta.schemas.enums import MessageRole, TagMatchMode
15
16
  from letta.schemas.sandbox_config import SandboxConfig
16
17
  from letta.schemas.tool import Tool
@@ -46,8 +47,11 @@ class LettaCoreToolExecutor(ToolExecutor):
46
47
  "core_memory_replace": self.core_memory_replace,
47
48
  "memory_replace": self.memory_replace,
48
49
  "memory_insert": self.memory_insert,
50
+ "memory_str_replace": self.memory_str_replace,
51
+ "memory_str_insert": self.memory_str_insert,
49
52
  "memory_rethink": self.memory_rethink,
50
53
  "memory_finish_edits": self.memory_finish_edits,
54
+ "memory": self.memory,
51
55
  }
52
56
 
53
57
  if function_name not in function_map:
@@ -479,8 +483,14 @@ class LettaCoreToolExecutor(ToolExecutor):
479
483
  "are for display purposes only)."
480
484
  )
481
485
 
482
- if agent_state.memory.get_block(label) is None:
483
- agent_state.memory.create_block(label=label, value=new_memory)
486
+ try:
487
+ agent_state.memory.get_block(label)
488
+ except KeyError:
489
+ # Block doesn't exist, create it
490
+ from letta.schemas.block import Block
491
+
492
+ new_block = Block(label=label, value=new_memory)
493
+ agent_state.memory.set_block(new_block)
484
494
 
485
495
  agent_state.memory.update_block_value(label=label, value=new_memory)
486
496
 
@@ -502,3 +512,388 @@ class LettaCoreToolExecutor(ToolExecutor):
502
512
 
503
513
  async def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
504
514
  return None
515
+
516
+ async def memory_delete(self, agent_state: AgentState, actor: User, path: str) -> str:
517
+ """Delete a memory block by detaching it from the agent."""
518
+ # Extract memory block label from path
519
+ label = path.removeprefix("/memories/").replace("/", "_")
520
+
521
+ try:
522
+ # Check if memory block exists
523
+ memory_block = agent_state.memory.get_block(label)
524
+ if memory_block is None:
525
+ raise ValueError(f"Error: Memory block '{label}' does not exist")
526
+
527
+ # Detach the block from the agent
528
+ updated_agent_state = await self.agent_manager.detach_block_async(
529
+ agent_id=agent_state.id, block_id=memory_block.id, actor=actor
530
+ )
531
+
532
+ # Update the agent state with the updated memory from the database
533
+ agent_state.memory = updated_agent_state.memory
534
+
535
+ return f"Successfully deleted memory block '{label}'"
536
+
537
+ except Exception as e:
538
+ return f"Error performing delete: {str(e)}"
539
+
540
+ async def memory_update_description(self, agent_state: AgentState, actor: User, path: str, description: str) -> str:
541
+ """Update the description of a memory block."""
542
+ label = path.removeprefix("/memories/").replace("/", "_")
543
+
544
+ try:
545
+ # Check if old memory block exists
546
+ memory_block = agent_state.memory.get_block(label)
547
+ if memory_block is None:
548
+ raise ValueError(f"Error: Memory block '{label}' does not exist")
549
+
550
+ await self.block_manager.update_block_async(
551
+ block_id=memory_block.id, block_update=BlockUpdate(description=description), actor=actor
552
+ )
553
+ await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
554
+
555
+ return f"Successfully updated description of memory block '{label}'"
556
+
557
+ except Exception as e:
558
+ raise Exception(f"Error performing update_description: {str(e)}")
559
+
560
+ async def memory_rename(self, agent_state: AgentState, actor: User, old_path: str, new_path: str) -> str:
561
+ """Rename a memory block by copying content to new label and detaching old one."""
562
+ # Extract memory block labels from paths
563
+ old_label = old_path.removeprefix("/memories/").replace("/", "_")
564
+ new_label = new_path.removeprefix("/memories/").replace("/", "_")
565
+
566
+ try:
567
+ # Check if old memory block exists
568
+ memory_block = agent_state.memory.get_block(old_label)
569
+ if memory_block is None:
570
+ raise ValueError(f"Error: Memory block '{old_label}' does not exist")
571
+
572
+ await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(label=new_label), actor=actor)
573
+ await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
574
+
575
+ return f"Successfully renamed memory block '{old_label}' to '{new_label}'"
576
+
577
+ except Exception as e:
578
+ raise Exception(f"Error performing rename: {str(e)}")
579
+
580
+ async def memory_view(self, agent_state: AgentState, actor: User, path: str, view_range: Optional[int] = None) -> str:
581
+ """View the content of a memory block with optional line range."""
582
+ try:
583
+ # Special case: if path is "/memories", list all blocks
584
+ if path == "/memories":
585
+ blocks = agent_state.memory.get_blocks()
586
+
587
+ if not blocks:
588
+ raise ValueError("No memory blocks found.")
589
+
590
+ result_lines = [f"Found {len(blocks)} memory block(s):\n"]
591
+
592
+ for i, block in enumerate(blocks, 1):
593
+ content = str(block.value)
594
+ content_length = len(content)
595
+ line_count = len(content.split("\n")) if content else 0
596
+
597
+ # Basic info
598
+ block_info = [f"{i}. {block.label}"]
599
+
600
+ # Add description if available
601
+ if block.description:
602
+ block_info.append(f" Description: {block.description}")
603
+
604
+ # Add read-only status
605
+ if block.read_only:
606
+ block_info.append(" Read-only: true")
607
+
608
+ # Add content stats
609
+ block_info.append(f" Character limit: {block.limit}")
610
+ block_info.append(f" Current length: {content_length} characters")
611
+ block_info.append(f" Lines: {line_count}")
612
+
613
+ # Add content preview (first 100 characters)
614
+ if content:
615
+ preview = content[:100].replace("\n", "\\n")
616
+ if len(content) > 100:
617
+ preview += "..."
618
+ block_info.append(f" Preview: {preview}")
619
+ else:
620
+ block_info.append(" Preview: (empty)")
621
+
622
+ result_lines.append("\n".join(block_info))
623
+ if i < len(blocks): # Add separator between blocks
624
+ result_lines.append("")
625
+
626
+ return "\n".join(result_lines)
627
+
628
+ # Extract memory block label from path (e.g., "/memories/preferences.txt" -> "preferences.txt")
629
+ if path.startswith("/memories/"):
630
+ label = path[10:] # Remove "/memories/" prefix
631
+ else:
632
+ label = path
633
+
634
+ # Get the memory block
635
+ memory_block = agent_state.memory.get_block(label)
636
+ if memory_block is None:
637
+ raise ValueError(f"Error: Memory block '{label}' does not exist")
638
+
639
+ # Get the content
640
+ content = str(memory_block.value)
641
+ if not content:
642
+ raise ValueError(f"Memory block '{label}' is empty")
643
+
644
+ # Split content into lines
645
+ lines = content.split("\n")
646
+ total_lines = len(lines)
647
+
648
+ # Handle view_range parameter
649
+ if view_range is not None:
650
+ if view_range <= 0:
651
+ raise ValueError(f"Error: view_range must be positive, got {view_range}")
652
+
653
+ # Show only the first view_range lines
654
+ lines_to_show = lines[:view_range]
655
+ range_info = f" (showing first {view_range} of {total_lines} lines)"
656
+ else:
657
+ lines_to_show = lines
658
+ range_info = f" ({total_lines} lines total)"
659
+
660
+ # Format output with line numbers
661
+ numbered_lines = []
662
+ for i, line in enumerate(lines_to_show, start=1):
663
+ numbered_lines.append(f"Line {i}: {line}")
664
+
665
+ numbered_content = "\n".join(numbered_lines)
666
+
667
+ # Add metadata information
668
+ metadata_info = []
669
+ if memory_block.description:
670
+ metadata_info.append(f"Description: {memory_block.description}")
671
+ if memory_block.read_only:
672
+ metadata_info.append("Read-only: true")
673
+ metadata_info.append(f"Character limit: {memory_block.limit}")
674
+ metadata_info.append(f"Current length: {len(content)} characters")
675
+
676
+ metadata_str = "\n".join(metadata_info)
677
+
678
+ result = f"Memory block: {label}{range_info}\n"
679
+ result += f"Metadata:\n{metadata_str}\n\n"
680
+ result += f"Content:\n{numbered_content}"
681
+
682
+ return result
683
+
684
+ except KeyError:
685
+ raise ValueError(f"Error: Memory block '{label}' does not exist")
686
+ except Exception as e:
687
+ raise Exception(f"Error viewing memory block: {str(e)}")
688
+
689
+ async def memory_create(
690
+ self, agent_state: AgentState, actor: User, path: str, description: str, file_text: Optional[str] = None
691
+ ) -> str:
692
+ """Create a memory block by setting its value to an empty string."""
693
+ from letta.schemas.block import Block
694
+
695
+ label = path.removeprefix("/memories/").replace("/", "_")
696
+
697
+ # Create a new block and persist it to the database
698
+ new_block = Block(label=label, value=file_text if file_text else "", description=description)
699
+ persisted_block = await self.block_manager.create_or_update_block_async(new_block, actor)
700
+
701
+ # Attach the block to the agent
702
+ await self.agent_manager.attach_block_async(agent_id=agent_state.id, block_id=persisted_block.id, actor=actor)
703
+
704
+ # Add the persisted block to memory
705
+ agent_state.memory.set_block(persisted_block)
706
+
707
+ await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
708
+ return f"Successfully created memory block '{label}'"
709
+
710
+ async def memory_str_replace(self, agent_state: AgentState, actor: User, path: str, old_str: str, new_str: str) -> str:
711
+ """Replace text in a memory block."""
712
+ label = path.removeprefix("/memories/").replace("/", "_")
713
+
714
+ memory_block = agent_state.memory.get_block(label)
715
+ if memory_block is None:
716
+ raise ValueError(f"Error: Memory block '{label}' does not exist")
717
+
718
+ if memory_block.read_only:
719
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
720
+
721
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(old_str)):
722
+ raise ValueError(
723
+ "old_str contains a line number prefix, which is not allowed. "
724
+ "Do not include line numbers when calling memory tools (line "
725
+ "numbers are for display purposes only)."
726
+ )
727
+ if CORE_MEMORY_LINE_NUMBER_WARNING in old_str:
728
+ raise ValueError(
729
+ "old_str contains a line number warning, which is not allowed. "
730
+ "Do not include line number information when calling memory tools "
731
+ "(line numbers are for display purposes only)."
732
+ )
733
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
734
+ raise ValueError(
735
+ "new_str contains a line number prefix, which is not allowed. "
736
+ "Do not include line numbers when calling memory tools (line "
737
+ "numbers are for display purposes only)."
738
+ )
739
+
740
+ old_str = str(old_str).expandtabs()
741
+ new_str = str(new_str).expandtabs()
742
+ current_value = str(memory_block.value).expandtabs()
743
+
744
+ # Check if old_str is unique in the block
745
+ occurences = current_value.count(old_str)
746
+ if occurences == 0:
747
+ raise ValueError(
748
+ f"No replacement was performed, old_str `{old_str}` did not appear verbatim in memory block with label `{label}`."
749
+ )
750
+ elif occurences > 1:
751
+ content_value_lines = current_value.split("\n")
752
+ lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_str in line]
753
+ raise ValueError(
754
+ f"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {lines}. Please ensure it is unique."
755
+ )
756
+
757
+ # Replace old_str with new_str
758
+ new_value = current_value.replace(str(old_str), str(new_str))
759
+
760
+ # Write the new content to the block
761
+ await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(value=new_value), actor=actor)
762
+ await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
763
+
764
+ # Prepare the success message
765
+ success_msg = f"The core memory block with label `{label}` has been edited. "
766
+ success_msg += (
767
+ "Review the changes and make sure they are as expected (correct indentation, "
768
+ "no duplicate lines, etc). Edit the memory block again if necessary."
769
+ )
770
+
771
+ return success_msg
772
+
773
+ async def memory_str_insert(self, agent_state: AgentState, actor: User, path: str, insert_text: str, insert_line: int = -1) -> str:
774
+ """Insert text into a memory block at a specific line."""
775
+ label = path.removeprefix("/memories/").replace("/", "_")
776
+
777
+ memory_block = agent_state.memory.get_block(label)
778
+ if memory_block is None:
779
+ raise ValueError(f"Error: Memory block '{label}' does not exist")
780
+
781
+ if memory_block.read_only:
782
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
783
+
784
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(insert_text)):
785
+ raise ValueError(
786
+ "insert_text contains a line number prefix, which is not allowed. "
787
+ "Do not include line numbers when calling memory tools (line "
788
+ "numbers are for display purposes only)."
789
+ )
790
+ if CORE_MEMORY_LINE_NUMBER_WARNING in insert_text:
791
+ raise ValueError(
792
+ "insert_text contains a line number warning, which is not allowed. "
793
+ "Do not include line number information when calling memory tools "
794
+ "(line numbers are for display purposes only)."
795
+ )
796
+
797
+ current_value = str(memory_block.value).expandtabs()
798
+ insert_text = str(insert_text).expandtabs()
799
+ current_value_lines = current_value.split("\n")
800
+ n_lines = len(current_value_lines)
801
+
802
+ # Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
803
+ if insert_line == -1:
804
+ insert_line = n_lines
805
+ elif insert_line < 0 or insert_line > n_lines:
806
+ raise ValueError(
807
+ f"Invalid `insert_line` parameter: {insert_line}. It should be within "
808
+ f"the range of lines of the memory block: {[0, n_lines]}, or -1 to "
809
+ f"append to the end of the memory block."
810
+ )
811
+
812
+ # Insert the new text as a line
813
+ SNIPPET_LINES = 3
814
+ insert_text_lines = insert_text.split("\n")
815
+ new_value_lines = current_value_lines[:insert_line] + insert_text_lines + current_value_lines[insert_line:]
816
+ snippet_lines = (
817
+ current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
818
+ + insert_text_lines
819
+ + current_value_lines[insert_line : insert_line + SNIPPET_LINES]
820
+ )
821
+
822
+ # Collate into the new value to update
823
+ new_value = "\n".join(new_value_lines)
824
+ snippet = "\n".join(snippet_lines)
825
+
826
+ # Write into the block
827
+ await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(value=new_value), actor=actor)
828
+ await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
829
+
830
+ # Prepare the success message
831
+ success_msg = f"The core memory block with label `{label}` has been edited. "
832
+ success_msg += (
833
+ "Review the changes and make sure they are as expected (correct indentation, "
834
+ "no duplicate lines, etc). Edit the memory block again if necessary."
835
+ )
836
+
837
+ return success_msg
838
+
839
+ async def memory(
840
+ self,
841
+ agent_state: AgentState,
842
+ actor: User,
843
+ command: str,
844
+ file_text: Optional[str] = None,
845
+ description: Optional[str] = None,
846
+ path: Optional[str] = None,
847
+ old_str: Optional[str] = None,
848
+ new_str: Optional[str] = None,
849
+ insert_line: Optional[int] = None,
850
+ insert_text: Optional[str] = None,
851
+ old_path: Optional[str] = None,
852
+ new_path: Optional[str] = None,
853
+ view_range: Optional[int] = None,
854
+ ) -> Optional[str]:
855
+ if command == "view":
856
+ if path is None:
857
+ raise ValueError("Error: path is required for view command")
858
+ return await self.memory_view(agent_state, actor, path, view_range)
859
+
860
+ elif command == "create":
861
+ if path is None:
862
+ raise ValueError("Error: path is required for create command")
863
+ if description is None:
864
+ raise ValueError("Error: description is required for create command")
865
+ return await self.memory_create(agent_state, actor, path, description, file_text)
866
+
867
+ elif command == "str_replace":
868
+ if path is None:
869
+ raise ValueError("Error: path is required for str_replace command")
870
+ if old_str is None:
871
+ raise ValueError("Error: old_str is required for str_replace command")
872
+ if new_str is None:
873
+ raise ValueError("Error: new_str is required for str_replace command")
874
+ return await self.memory_str_replace(agent_state, actor, path, old_str, new_str)
875
+
876
+ elif command == "insert":
877
+ if path is None:
878
+ raise ValueError("Error: path is required for insert command")
879
+ if insert_text is None:
880
+ raise ValueError("Error: insert_text is required for insert command")
881
+ return await self.memory_str_insert(agent_state, actor, path, insert_text, insert_line)
882
+
883
+ elif command == "delete":
884
+ if path is None:
885
+ raise ValueError("Error: path is required for delete command")
886
+ return await self.memory_delete(agent_state, actor, path)
887
+
888
+ elif command == "rename":
889
+ if path and description:
890
+ return await self.memory_update_description(agent_state, actor, path, description)
891
+ elif old_path and new_path:
892
+ return await self.memory_rename(agent_state, actor, old_path, new_path)
893
+ else:
894
+ raise ValueError(
895
+ "Error: path and description are required for update_description command, or old_path and new_path are required for rename command"
896
+ )
897
+
898
+ else:
899
+ raise ValueError(f"Error: Unknown command '{command}'. Supported commands: str_replace, str_insert, insert, delete, rename")
@@ -20,9 +20,9 @@ from letta.services.block_manager import BlockManager
20
20
  from letta.services.file_manager import FileManager
21
21
  from letta.services.file_processor.chunker.line_chunker import LineChunker
22
22
  from letta.services.files_agents_manager import FileAgentManager
23
- from letta.services.job_manager import JobManager
24
23
  from letta.services.message_manager import MessageManager
25
24
  from letta.services.passage_manager import PassageManager
25
+ from letta.services.run_manager import RunManager
26
26
  from letta.services.source_manager import SourceManager
27
27
  from letta.services.tool_executor.tool_executor_base import ToolExecutor
28
28
  from letta.utils import get_friendly_error_msg
@@ -47,7 +47,7 @@ class LettaFileToolExecutor(ToolExecutor):
47
47
  message_manager: MessageManager,
48
48
  agent_manager: AgentManager,
49
49
  block_manager: BlockManager,
50
- job_manager: JobManager,
50
+ run_manager: RunManager,
51
51
  passage_manager: PassageManager,
52
52
  actor: User,
53
53
  ):
@@ -55,7 +55,7 @@ class LettaFileToolExecutor(ToolExecutor):
55
55
  message_manager=message_manager,
56
56
  agent_manager=agent_manager,
57
57
  block_manager=block_manager,
58
- job_manager=job_manager,
58
+ run_manager=run_manager,
59
59
  passage_manager=passage_manager,
60
60
  actor=actor,
61
61
  )
@@ -7,10 +7,12 @@ from letta.schemas.enums import MessageRole
7
7
  from letta.schemas.letta_message import AssistantMessage
8
8
  from letta.schemas.letta_message_content import TextContent
9
9
  from letta.schemas.message import MessageCreate
10
+ from letta.schemas.run import Run
10
11
  from letta.schemas.sandbox_config import SandboxConfig
11
12
  from letta.schemas.tool import Tool
12
13
  from letta.schemas.tool_execution_result import ToolExecutionResult
13
14
  from letta.schemas.user import User
15
+ from letta.services.run_manager import RunManager
14
16
  from letta.services.tool_executor.tool_executor_base import ToolExecutor
15
17
  from letta.settings import settings
16
18
  from letta.utils import safe_create_task
@@ -43,13 +45,15 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
43
45
 
44
46
  # Execute the appropriate function
45
47
  function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
46
- function_response = await function_map[function_name](agent_state, **function_args_copy)
48
+ function_response = await function_map[function_name](agent_state, actor, **function_args_copy)
47
49
  return ToolExecutionResult(
48
50
  status="success",
49
51
  func_return=function_response,
50
52
  )
51
53
 
52
- async def send_message_to_agent_and_wait_for_reply(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
54
+ async def send_message_to_agent_and_wait_for_reply(
55
+ self, agent_state: AgentState, actor: User, message: str, other_agent_id: str
56
+ ) -> str:
53
57
  augmented_message = (
54
58
  f"[Incoming message from agent with ID '{agent_state.id}' - to reply to this message, "
55
59
  f"make sure to use the 'send_message' at the end, and the system will notify the sender of your response] "
@@ -57,10 +61,10 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
57
61
  )
58
62
 
59
63
  other_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=other_agent_id, actor=self.actor)
60
- return str(await self._process_agent(agent_state=other_agent_state, message=augmented_message))
64
+ return str(await self._process_agent(agent_state=other_agent_state, message=augmented_message, actor=actor))
61
65
 
62
66
  async def send_message_to_agents_matching_tags_async(
63
- self, agent_state: AgentState, message: str, match_all: List[str], match_some: List[str]
67
+ self, agent_state: AgentState, actor: User, message: str, match_all: List[str], match_some: List[str]
64
68
  ) -> str:
65
69
  # Find matching agents
66
70
  matching_agents = await self.agent_manager.list_agents_matching_tags_async(
@@ -76,25 +80,36 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
76
80
  f"{message}"
77
81
  )
78
82
 
79
- tasks = [
80
- safe_create_task(
81
- self._process_agent(agent_state=agent_state, message=augmented_message), label=f"process_agent_{agent_state.id}"
82
- )
83
- for agent_state in matching_agents
84
- ]
85
- results = await asyncio.gather(*tasks)
83
+ # Run concurrent requests and collect their return values.
84
+ # Note: Do not wrap with safe_create_task here — it swallows return values (returns None).
85
+ coros = [self._process_agent(agent_state=a_state, message=augmented_message, actor=actor) for a_state in matching_agents]
86
+ results = await asyncio.gather(*coros)
86
87
  return str(results)
87
88
 
88
- async def _process_agent(self, agent_state: AgentState, message: str) -> Dict[str, Any]:
89
+ async def _process_agent(self, agent_state: AgentState, message: str, actor: User) -> Dict[str, Any]:
89
90
  from letta.agents.letta_agent_v2 import LettaAgentV2
90
91
 
91
92
  try:
93
+ runs_manager = RunManager()
94
+ run = await runs_manager.create_run(
95
+ pydantic_run=Run(
96
+ agent_id=agent_state.id,
97
+ background=False,
98
+ metadata={
99
+ "run_type": "agent_send_message_to_agent", # TODO: Make this a constant
100
+ },
101
+ ),
102
+ actor=actor,
103
+ )
104
+
92
105
  letta_agent = LettaAgentV2(
93
106
  agent_state=agent_state,
94
107
  actor=self.actor,
95
108
  )
96
109
 
97
- letta_response = await letta_agent.step([MessageCreate(role=MessageRole.system, content=[TextContent(text=message)])])
110
+ letta_response = await letta_agent.step(
111
+ [MessageCreate(role=MessageRole.system, content=[TextContent(text=message)])], run_id=run.id
112
+ )
98
113
  messages = letta_response.messages
99
114
 
100
115
  send_message_content = [message.content for message in messages if isinstance(message, AssistantMessage)]
@@ -111,7 +126,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
111
126
  "type": type(e).__name__,
112
127
  }
113
128
 
114
- async def send_message_to_agent_async(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
129
+ async def send_message_to_agent_async(self, agent_state: AgentState, actor: User, message: str, other_agent_id: str) -> str:
115
130
  if settings.environment == "PRODUCTION":
116
131
  raise RuntimeError("This tool is not allowed to be run on Letta Cloud.")
117
132
 
@@ -125,7 +140,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
125
140
 
126
141
  other_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=other_agent_id, actor=self.actor)
127
142
  task = safe_create_task(
128
- self._process_agent(agent_state=other_agent_state, message=prefixed), label=f"send_message_to_{other_agent_id}"
143
+ self._process_agent(agent_state=other_agent_state, message=prefixed, actor=actor), label=f"send_message_to_{other_agent_id}"
129
144
  )
130
145
 
131
146
  task.add_done_callback(lambda t: (logger.error(f"Async send_message task failed: {t.exception()}") if t.exception() else None))
@@ -16,11 +16,10 @@ from letta.schemas.tool_execution_result import ToolExecutionResult
16
16
  from letta.schemas.user import User
17
17
  from letta.services.agent_manager import AgentManager
18
18
  from letta.services.block_manager import BlockManager
19
- from letta.services.job_manager import JobManager
20
19
  from letta.services.message_manager import MessageManager
21
20
  from letta.services.passage_manager import PassageManager
21
+ from letta.services.run_manager import RunManager
22
22
  from letta.services.tool_executor.builtin_tool_executor import LettaBuiltinToolExecutor
23
- from letta.services.tool_executor.composio_tool_executor import ExternalComposioToolExecutor
24
23
  from letta.services.tool_executor.core_tool_executor import LettaCoreToolExecutor
25
24
  from letta.services.tool_executor.files_tool_executor import LettaFileToolExecutor
26
25
  from letta.services.tool_executor.mcp_tool_executor import ExternalMCPToolExecutor
@@ -40,7 +39,6 @@ class ToolExecutorFactory:
40
39
  ToolType.LETTA_MULTI_AGENT_CORE: LettaMultiAgentToolExecutor,
41
40
  ToolType.LETTA_BUILTIN: LettaBuiltinToolExecutor,
42
41
  ToolType.LETTA_FILES_CORE: LettaFileToolExecutor,
43
- ToolType.EXTERNAL_COMPOSIO: ExternalComposioToolExecutor,
44
42
  ToolType.EXTERNAL_MCP: ExternalMCPToolExecutor,
45
43
  }
46
44
 
@@ -51,7 +49,7 @@ class ToolExecutorFactory:
51
49
  message_manager: MessageManager,
52
50
  agent_manager: AgentManager,
53
51
  block_manager: BlockManager,
54
- job_manager: JobManager,
52
+ run_manager: RunManager,
55
53
  passage_manager: PassageManager,
56
54
  actor: User,
57
55
  ) -> ToolExecutor:
@@ -61,7 +59,7 @@ class ToolExecutorFactory:
61
59
  message_manager=message_manager,
62
60
  agent_manager=agent_manager,
63
61
  block_manager=block_manager,
64
- job_manager=job_manager,
62
+ run_manager=run_manager,
65
63
  passage_manager=passage_manager,
66
64
  actor=actor,
67
65
  )
@@ -75,7 +73,7 @@ class ToolExecutionManager:
75
73
  message_manager: MessageManager,
76
74
  agent_manager: AgentManager,
77
75
  block_manager: BlockManager,
78
- job_manager: JobManager,
76
+ run_manager: RunManager,
79
77
  passage_manager: PassageManager,
80
78
  actor: User,
81
79
  agent_state: Optional[AgentState] = None,
@@ -85,7 +83,7 @@ class ToolExecutionManager:
85
83
  self.message_manager = message_manager
86
84
  self.agent_manager = agent_manager
87
85
  self.block_manager = block_manager
88
- self.job_manager = job_manager
86
+ self.run_manager = run_manager
89
87
  self.passage_manager = passage_manager
90
88
  self.agent_state = agent_state
91
89
  self.logger = get_logger(__name__)
@@ -107,7 +105,7 @@ class ToolExecutionManager:
107
105
  message_manager=self.message_manager,
108
106
  agent_manager=self.agent_manager,
109
107
  block_manager=self.block_manager,
110
- job_manager=self.job_manager,
108
+ run_manager=self.run_manager,
111
109
  passage_manager=self.passage_manager,
112
110
  actor=self.actor,
113
111
  )
@@ -8,9 +8,9 @@ from letta.schemas.tool_execution_result import ToolExecutionResult
8
8
  from letta.schemas.user import User
9
9
  from letta.services.agent_manager import AgentManager
10
10
  from letta.services.block_manager import BlockManager
11
- from letta.services.job_manager import JobManager
12
11
  from letta.services.message_manager import MessageManager
13
12
  from letta.services.passage_manager import PassageManager
13
+ from letta.services.run_manager import RunManager
14
14
 
15
15
 
16
16
  class ToolExecutor(ABC):
@@ -21,14 +21,14 @@ class ToolExecutor(ABC):
21
21
  message_manager: MessageManager,
22
22
  agent_manager: AgentManager,
23
23
  block_manager: BlockManager,
24
- job_manager: JobManager,
24
+ run_manager: RunManager,
25
25
  passage_manager: PassageManager,
26
26
  actor: User,
27
27
  ):
28
28
  self.message_manager = message_manager
29
29
  self.agent_manager = agent_manager
30
30
  self.block_manager = block_manager
31
- self.job_manager = job_manager
31
+ self.run_manager = run_manager
32
32
  self.passage_manager = passage_manager
33
33
  self.actor = actor
34
34