letta-nightly 0.11.7.dev20251006104136__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.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
  137. {letta_nightly-0.11.7.dev20251006104136.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.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
  144. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
  145. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
@@ -214,7 +214,6 @@ class VoiceAgent(BaseAgent):
214
214
  response_text=content,
215
215
  agent_id=agent_state.id,
216
216
  model=agent_state.llm_config.model,
217
- actor=self.actor,
218
217
  timezone=agent_state.timezone,
219
218
  )
220
219
  letta_message_db_queue.extend(assistant_msgs)
@@ -273,11 +272,9 @@ class VoiceAgent(BaseAgent):
273
272
  function_name=tool_call_name,
274
273
  function_arguments=tool_args,
275
274
  tool_call_id=tool_call_id,
276
- function_call_success=success_flag,
277
275
  function_response=tool_result,
278
276
  tool_execution_result=tool_execution_result,
279
277
  timezone=agent_state.timezone,
280
- actor=self.actor,
281
278
  continue_stepping=True,
282
279
  )
283
280
  letta_message_db_queue.extend(tool_call_messages)
@@ -343,8 +340,7 @@ class VoiceAgent(BaseAgent):
343
340
  tools = [
344
341
  t
345
342
  for t in agent_state.tools
346
- if t.tool_type
347
- in {ToolType.EXTERNAL_COMPOSIO, ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP}
343
+ if t.tool_type in {ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP}
348
344
  ]
349
345
  else:
350
346
  tools = agent_state.tools
@@ -504,7 +500,7 @@ class VoiceAgent(BaseAgent):
504
500
  keyword_results = {}
505
501
  if convo_keyword_queries:
506
502
  for keyword in convo_keyword_queries:
507
- messages = await self.message_manager.list_messages_for_agent_async(
503
+ messages = await self.message_manager.list_messages(
508
504
  agent_id=self.agent_id,
509
505
  actor=self.actor,
510
506
  query_text=keyword,
letta/constants.py CHANGED
@@ -13,9 +13,6 @@ API_PREFIX = "/v1"
13
13
  OLLAMA_API_PREFIX = "/v1"
14
14
  OPENAI_API_PREFIX = "/openai"
15
15
 
16
- COMPOSIO_ENTITY_ENV_VAR_KEY = "COMPOSIO_ENTITY"
17
- COMPOSIO_TOOL_TAG_NAME = "composio"
18
-
19
16
  MCP_CONFIG_NAME = "mcp_config.json"
20
17
  MCP_TOOL_TAG_NAME_PREFIX = "mcp" # full format, mcp:server_name
21
18
 
@@ -89,7 +86,7 @@ SEND_MESSAGE_TOOL_NAME = "send_message"
89
86
  BASE_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_insert", "archival_memory_search"]
90
87
  DEPRECATED_LETTA_TOOLS = ["archival_memory_insert", "archival_memory_search"]
91
88
  # Base memory tools CAN be edited, and are added by default by the server
92
- BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
89
+ BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace", "memory"]
93
90
  # New v2 collection of the base memory tools (effecitvely same as sleeptime set), to pair with memgpt_v2 prompt
94
91
  BASE_MEMORY_TOOLS_V2 = [
95
92
  "memory_replace",
@@ -98,6 +95,11 @@ BASE_MEMORY_TOOLS_V2 = [
98
95
  # "memory_rethink",
99
96
  # "memory_finish_edits",
100
97
  ]
98
+
99
+ # v3 collection, currently just a omni memory tool for anthropic
100
+ BASE_MEMORY_TOOLS_V3 = [
101
+ "memory",
102
+ ]
101
103
  # Base tools if the memgpt agent has enable_sleeptime on
102
104
  BASE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_search"]
103
105
  # Base memory tools for sleeptime agent
@@ -118,6 +120,7 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
118
120
  "rethink_user_memory",
119
121
  "finish_rethinking_memory",
120
122
  ]
123
+
121
124
  # Multi agent tools
122
125
  MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
123
126
  LOCAL_ONLY_MULTI_AGENT_TOOLS = ["send_message_to_agent_async"]
@@ -219,6 +222,7 @@ LLM_MAX_TOKENS = {
219
222
  "gpt-5-mini-2025-08-07": 272000,
220
223
  "gpt-5-nano": 272000,
221
224
  "gpt-5-nano-2025-08-07": 272000,
225
+ "gpt-5-codex": 272000,
222
226
  # reasoners
223
227
  "o1": 200000,
224
228
  # "o1-pro": 200000, # responses API only
letta/errors.py CHANGED
@@ -97,6 +97,46 @@ class LettaUserNotFoundError(LettaError):
97
97
  """Error raised when a user is not found."""
98
98
 
99
99
 
100
+ class LettaInvalidArgumentError(LettaError):
101
+ """Error raised when an invalid argument is provided."""
102
+
103
+ def __init__(self, message: str, argument_name: Optional[str] = None):
104
+ details = {"argument_name": argument_name} if argument_name else {}
105
+ super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details)
106
+
107
+
108
+ class LettaMCPError(LettaError):
109
+ """Base error for MCP-related issues."""
110
+
111
+
112
+ class LettaInvalidMCPSchemaError(LettaMCPError):
113
+ """Error raised when an invalid MCP schema is provided."""
114
+
115
+ def __init__(self, server_name: str, mcp_tool_name: str, reasons: List[str]):
116
+ details = {"server_name": server_name, "mcp_tool_name": mcp_tool_name, "reasons": reasons}
117
+ super().__init__(
118
+ message=f"MCP tool {mcp_tool_name} has an invalid schema and cannot be attached - reasons: {reasons}",
119
+ code=ErrorCode.INVALID_ARGUMENT,
120
+ details=details,
121
+ )
122
+
123
+
124
+ class LettaMCPConnectionError(LettaMCPError):
125
+ """Error raised when unable to connect to MCP server."""
126
+
127
+ def __init__(self, message: str, server_name: Optional[str] = None):
128
+ details = {"server_name": server_name} if server_name else {}
129
+ super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details)
130
+
131
+
132
+ class LettaMCPTimeoutError(LettaMCPError):
133
+ """Error raised when MCP server operation times out."""
134
+
135
+ def __init__(self, message: str, server_name: Optional[str] = None):
136
+ details = {"server_name": server_name} if server_name else {}
137
+ super().__init__(message=message, code=ErrorCode.TIMEOUT, details=details)
138
+
139
+
100
140
  class LettaUnexpectedStreamCancellationError(LettaError):
101
141
  """Error raised when a streaming request is terminated unexpectedly."""
102
142
 
@@ -1,8 +1,82 @@
1
- from typing import List, Literal, Optional
1
+ from typing import TYPE_CHECKING, Any, List, Literal, Optional
2
2
 
3
- from letta.agent import Agent
4
3
  from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING
5
4
 
5
+ if TYPE_CHECKING:
6
+ from letta.schemas.agent import AgentState
7
+
8
+
9
+ def memory(
10
+ agent_state: "AgentState",
11
+ command: str,
12
+ path: Optional[str] = None,
13
+ file_text: Optional[str] = None,
14
+ description: Optional[str] = None,
15
+ old_str: Optional[str] = None,
16
+ new_str: Optional[str] = None,
17
+ insert_line: Optional[int] = None,
18
+ insert_text: Optional[str] = None,
19
+ old_path: Optional[str] = None,
20
+ new_path: Optional[str] = None,
21
+ ) -> Optional[str]:
22
+ """
23
+ Memory management tool with various sub-commands for memory block operations.
24
+
25
+ Args:
26
+ command (str): The sub-command to execute. Supported commands:
27
+ - "view": List memory blocks or view specific block content
28
+ - "create": Create a new memory block
29
+ - "str_replace": Replace text in a memory block
30
+ - "insert": Insert text at a specific line in a memory block
31
+ - "delete": Delete a memory block
32
+ - "rename": Rename a memory block
33
+ path (Optional[str]): Path to the memory block (for str_replace, insert, delete)
34
+ file_text (Optional[str]): The value to set in the memory block (for create)
35
+ description (Optional[str]): The description to set in the memory block (for create, rename)
36
+ old_str (Optional[str]): Old text to replace (for str_replace)
37
+ new_str (Optional[str]): New text to replace with (for str_replace)
38
+ insert_line (Optional[int]): Line number to insert at (for insert)
39
+ insert_text (Optional[str]): Text to insert (for insert)
40
+ old_path (Optional[str]): Old path for rename operation
41
+ new_path (Optional[str]): New path for rename operation
42
+ view_range (Optional[int]): Range of lines to view (for view)
43
+
44
+ Returns:
45
+ Optional[str]: Success message or error description
46
+
47
+ Examples:
48
+ # List all memory blocks
49
+ memory(agent_state, "view", path="/memories")
50
+
51
+ # View specific memory block content
52
+ memory(agent_state, "view", path="/memories/user_preferences")
53
+
54
+ # View first 10 lines of a memory block
55
+ memory(agent_state, "view", path="/memories/user_preferences", view_range=10)
56
+
57
+ # Replace text in a memory block
58
+ memory(agent_state, "str_replace", path="/memories/user_preferences", old_str="theme: dark", new_str="theme: light")
59
+
60
+ # Insert text at line 5
61
+ memory(agent_state, "insert", path="/memories/notes", insert_line=5, insert_text="New note here")
62
+
63
+ # Delete a memory block
64
+ memory(agent_state, "delete", path="/memories/old_notes")
65
+
66
+ # Rename a memory block
67
+ memory(agent_state, "rename", old_path="/memories/temp", new_path="/memories/permanent")
68
+
69
+ # Update the description of a memory block
70
+ memory(agent_state, "rename", path="/memories/temp", description="The user's temporary notes.")
71
+
72
+ # Create a memory block with starting text
73
+ memory(agent_state, "create", path="/memories/coding_preferences", "description": "The user's coding preferences.", "file_text": "The user seems to add type hints to all of their Python code.")
74
+
75
+ # Create an empty memory block
76
+ memory(agent_state, "create", path="/memories/coding_preferences", "description": "The user's coding preferences.")
77
+ """
78
+ raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.")
79
+
6
80
 
7
81
  def send_message(self: "Agent", message: str) -> Optional[str]:
8
82
  """
@@ -202,7 +276,10 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
202
276
  """
203
277
 
204
278
  if agent_state.memory.get_block(target_block_label) is None:
205
- agent_state.memory.create_block(label=target_block_label, value=new_memory)
279
+ from letta.schemas.block import Block
280
+
281
+ new_block = Block(label=target_block_label, value=new_memory)
282
+ agent_state.memory.set_block(new_block)
206
283
 
207
284
  agent_state.memory.update_block_value(label=target_block_label, value=new_memory)
208
285
  return None
@@ -395,7 +472,10 @@ def memory_rethink(agent_state: "AgentState", label: str, new_memory: str) -> No
395
472
  )
396
473
 
397
474
  if agent_state.memory.get_block(label) is None:
398
- agent_state.memory.create_block(label=label, value=new_memory)
475
+ from letta.schemas.block import Block
476
+
477
+ new_block = Block(label=label, value=new_memory)
478
+ agent_state.memory.set_block(new_block)
399
479
 
400
480
  agent_state.memory.update_block_value(label=label, value=new_memory)
401
481
 
@@ -14,9 +14,6 @@ from letta.schemas.message import MessageCreate
14
14
  from letta.server.rest_api.dependencies import get_letta_server
15
15
  from letta.settings import settings
16
16
 
17
- if TYPE_CHECKING:
18
- from letta.agent import Agent
19
-
20
17
 
21
18
  def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_agent_id: str) -> str:
22
19
  """
@@ -2,7 +2,6 @@ import inspect
2
2
  import warnings
3
3
  from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
4
4
 
5
- from composio.client.collections import ActionParametersModel
6
5
  from docstring_parser import parse
7
6
  from pydantic import BaseModel
8
7
  from typing_extensions import Literal
@@ -588,11 +587,111 @@ def generate_schema_from_args_schema_v2(
588
587
  return function_call_json
589
588
 
590
589
 
590
+ def normalize_mcp_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
591
+ """
592
+ Normalize an MCP JSON schema to fix common issues:
593
+ 1. Add explicit 'additionalProperties': false to all object types
594
+ 2. Add explicit 'type' field to properties using $ref
595
+ 3. Process $defs recursively
596
+
597
+ Args:
598
+ schema: The JSON schema to normalize (will be modified in-place)
599
+
600
+ Returns:
601
+ The normalized schema (same object, modified in-place)
602
+ """
603
+ import copy
604
+
605
+ # Work on a deep copy to avoid modifying the original
606
+ schema = copy.deepcopy(schema)
607
+
608
+ def normalize_object_schema(obj_schema: Dict[str, Any], defs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
609
+ """Recursively normalize an object schema."""
610
+
611
+ # If this is an object type, add additionalProperties if missing
612
+ if obj_schema.get("type") == "object":
613
+ if "additionalProperties" not in obj_schema:
614
+ obj_schema["additionalProperties"] = False
615
+
616
+ # Handle properties
617
+ if "properties" in obj_schema:
618
+ for prop_name, prop_schema in obj_schema["properties"].items():
619
+ # Handle $ref references
620
+ if "$ref" in prop_schema:
621
+ # Add explicit type based on the reference
622
+ if "type" not in prop_schema:
623
+ # Try to resolve the type from $defs if available
624
+ if defs and prop_schema["$ref"].startswith("#/$defs/"):
625
+ def_name = prop_schema["$ref"].split("/")[-1]
626
+ if def_name in defs:
627
+ ref_schema = defs[def_name]
628
+ if "type" in ref_schema:
629
+ prop_schema["type"] = ref_schema["type"]
630
+
631
+ # If still no type, assume object (common case for model references)
632
+ if "type" not in prop_schema:
633
+ prop_schema["type"] = "object"
634
+
635
+ # Don't add additionalProperties to properties with $ref
636
+ # The $ref schema itself will have additionalProperties
637
+ # Adding it here makes the validator think it allows empty objects
638
+ continue
639
+
640
+ # Recursively normalize nested objects
641
+ if isinstance(prop_schema, dict):
642
+ if prop_schema.get("type") == "object":
643
+ normalize_object_schema(prop_schema, defs)
644
+
645
+ # Handle arrays with object items
646
+ if prop_schema.get("type") == "array" and "items" in prop_schema:
647
+ items = prop_schema["items"]
648
+ if isinstance(items, dict):
649
+ # Handle $ref in items
650
+ if "$ref" in items and "type" not in items:
651
+ if defs and items["$ref"].startswith("#/$defs/"):
652
+ def_name = items["$ref"].split("/")[-1]
653
+ if def_name in defs and "type" in defs[def_name]:
654
+ items["type"] = defs[def_name]["type"]
655
+ if "type" not in items:
656
+ items["type"] = "object"
657
+
658
+ # Recursively normalize items
659
+ if items.get("type") == "object":
660
+ normalize_object_schema(items, defs)
661
+
662
+ # Handle anyOf (complex union types)
663
+ if "anyOf" in prop_schema:
664
+ for option in prop_schema["anyOf"]:
665
+ if isinstance(option, dict) and option.get("type") == "object":
666
+ normalize_object_schema(option, defs)
667
+
668
+ # Handle array items at the top level
669
+ if "items" in obj_schema and isinstance(obj_schema["items"], dict):
670
+ if obj_schema["items"].get("type") == "object":
671
+ normalize_object_schema(obj_schema["items"], defs)
672
+
673
+ return obj_schema
674
+
675
+ # Process $defs first if they exist
676
+ defs = schema.get("$defs", {})
677
+ if defs:
678
+ for def_name, def_schema in defs.items():
679
+ if isinstance(def_schema, dict):
680
+ normalize_object_schema(def_schema, defs)
681
+
682
+ # Process the main schema
683
+ normalize_object_schema(schema, defs)
684
+
685
+ return schema
686
+
687
+
591
688
  def generate_tool_schema_for_mcp(
592
689
  mcp_tool: MCPTool,
593
690
  append_heartbeat: bool = True,
594
691
  strict: bool = False,
595
692
  ) -> Dict[str, Any]:
693
+ from letta.functions.schema_validator import validate_complete_json_schema
694
+
596
695
  # MCP tool.inputSchema is a JSON schema
597
696
  # https://github.com/modelcontextprotocol/python-sdk/blob/775f87981300660ee957b63c2a14b448ab9c3675/src/mcp/types.py#L678
598
697
  parameters_schema = mcp_tool.inputSchema
@@ -603,11 +702,16 @@ def generate_tool_schema_for_mcp(
603
702
  assert "properties" in parameters_schema, parameters_schema
604
703
  # assert "required" in parameters_schema, parameters_schema
605
704
 
705
+ # Normalize the schema to fix common issues with MCP schemas
706
+ # This adds additionalProperties: false and explicit types for $ref properties
707
+ parameters_schema = normalize_mcp_schema(parameters_schema)
708
+
606
709
  # Zero-arg tools often omit "required" because nothing is required.
607
710
  # Normalise so downstream code can treat it consistently.
608
711
  parameters_schema.setdefault("required", [])
609
712
 
610
713
  # Process properties to handle anyOf types and make optional fields strict-compatible
714
+ # TODO: de-duplicate with handling in normalize_mcp_schema
611
715
  if "properties" in parameters_schema:
612
716
  for field_name, field_props in parameters_schema["properties"].items():
613
717
  # Handle anyOf types by flattening to type array
@@ -660,6 +764,14 @@ def generate_tool_schema_for_mcp(
660
764
  if REQUEST_HEARTBEAT_PARAM not in parameters_schema["required"]:
661
765
  parameters_schema["required"].append(REQUEST_HEARTBEAT_PARAM)
662
766
 
767
+ # Re-validate the schema after normalization and update the health status
768
+ # This allows previously INVALID schemas to pass if normalization fixed them
769
+ if mcp_tool.health:
770
+ health_status, health_reasons = validate_complete_json_schema(parameters_schema)
771
+ mcp_tool.health.status = health_status.value
772
+ mcp_tool.health.reasons = health_reasons
773
+ logger.debug(f"MCP tool {name} schema health after normalization: {health_status.value}, reasons: {health_reasons}")
774
+
663
775
  # Return the final schema
664
776
  if strict:
665
777
  # https://platform.openai.com/docs/guides/function-calling#strict-mode
@@ -679,73 +791,3 @@ def generate_tool_schema_for_mcp(
679
791
  "description": description,
680
792
  "parameters": parameters_schema,
681
793
  }
682
-
683
-
684
- def generate_tool_schema_for_composio(
685
- parameters_model: ActionParametersModel,
686
- name: str,
687
- description: str,
688
- append_heartbeat: bool = True,
689
- strict: bool = False,
690
- ) -> Dict[str, Any]:
691
- properties_json = {}
692
- required_fields = parameters_model.required or []
693
-
694
- # Extract properties from the ActionParametersModel
695
- for field_name, field_props in parameters_model.properties.items():
696
- # Initialize the property structure
697
- property_schema = {
698
- "type": field_props["type"],
699
- "description": field_props.get("description", ""),
700
- }
701
-
702
- # Handle optional default values
703
- if "default" in field_props:
704
- property_schema["default"] = field_props["default"]
705
-
706
- # Handle enumerations
707
- if "enum" in field_props:
708
- property_schema["enum"] = field_props["enum"]
709
-
710
- # Handle array item types
711
- if field_props["type"] == "array":
712
- if "items" in field_props:
713
- property_schema["items"] = field_props["items"]
714
- elif "anyOf" in field_props:
715
- property_schema["items"] = [t for t in field_props["anyOf"] if "items" in t][0]["items"]
716
-
717
- # Add the property to the schema
718
- properties_json[field_name] = property_schema
719
-
720
- # Add the optional heartbeat parameter
721
- if append_heartbeat:
722
- properties_json[REQUEST_HEARTBEAT_PARAM] = {
723
- "type": "boolean",
724
- "description": REQUEST_HEARTBEAT_DESCRIPTION,
725
- }
726
- required_fields.append(REQUEST_HEARTBEAT_PARAM)
727
-
728
- # Return the final schema
729
- if strict:
730
- # https://platform.openai.com/docs/guides/function-calling#strict-mode
731
- return {
732
- "name": name,
733
- "description": description,
734
- "strict": True, # NOTE
735
- "parameters": {
736
- "type": "object",
737
- "properties": properties_json,
738
- "additionalProperties": False, # NOTE
739
- "required": required_fields,
740
- },
741
- }
742
- else:
743
- return {
744
- "name": name,
745
- "description": description,
746
- "parameters": {
747
- "type": "object",
748
- "properties": properties_json,
749
- "required": required_fields,
750
- },
751
- }
@@ -1,8 +1,9 @@
1
1
  from typing import List, Optional
2
2
 
3
- from letta.agent import Agent, AgentState
3
+ from letta.agents.base_agent import BaseAgent
4
4
  from letta.interface import AgentInterface
5
5
  from letta.orm import User
6
+ from letta.schemas.agent import AgentState
6
7
  from letta.schemas.block import Block
7
8
  from letta.schemas.letta_message_content import TextContent
8
9
  from letta.schemas.message import Message, MessageCreate
@@ -11,7 +12,7 @@ from letta.schemas.usage import LettaUsageStatistics
11
12
  from letta.services.tool_manager import ToolManager
12
13
 
13
14
 
14
- class DynamicMultiAgent(Agent):
15
+ class DynamicMultiAgent(BaseAgent):
15
16
  def __init__(
16
17
  self,
17
18
  interface: AgentInterface,
letta/groups/helpers.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import json
2
2
  from typing import Dict, Optional, Union
3
3
 
4
- from letta.agent import Agent
5
4
  from letta.interface import AgentInterface
6
5
  from letta.orm.group import Group
7
6
  from letta.orm.user import User
@@ -18,7 +17,7 @@ def load_multi_agent(
18
17
  actor: User,
19
18
  interface: Union[AgentInterface, None] = None,
20
19
  mcp_clients: Optional[Dict[str, AsyncBaseMCPClient]] = None,
21
- ) -> Agent:
20
+ ) -> "Agent":
22
21
  if len(group.agent_ids) == 0:
23
22
  raise ValueError("Empty group: group must have at least one agent")
24
23
 
@@ -1,15 +1,16 @@
1
1
  from typing import List, Optional
2
2
 
3
- from letta.agent import Agent, AgentState
3
+ from letta.agents.base_agent import BaseAgent
4
4
  from letta.interface import AgentInterface
5
5
  from letta.orm import User
6
+ from letta.schemas.agent import AgentState
6
7
  from letta.schemas.letta_message_content import TextContent
7
8
  from letta.schemas.message import Message, MessageCreate
8
9
  from letta.schemas.openai.chat_completion_response import UsageStatistics
9
10
  from letta.schemas.usage import LettaUsageStatistics
10
11
 
11
12
 
12
- class RoundRobinMultiAgent(Agent):
13
+ class RoundRobinMultiAgent(BaseAgent):
13
14
  def __init__(
14
15
  self,
15
16
  interface: AgentInterface,
@@ -3,10 +3,11 @@ import threading
3
3
  from datetime import datetime, timezone
4
4
  from typing import List, Optional
5
5
 
6
- from letta.agent import Agent, AgentState
6
+ from letta.agents.base_agent import BaseAgent
7
7
  from letta.groups.helpers import stringify_message
8
8
  from letta.interface import AgentInterface
9
9
  from letta.orm import User
10
+ from letta.schemas.agent import AgentState
10
11
  from letta.schemas.enums import JobStatus
11
12
  from letta.schemas.job import JobUpdate
12
13
  from letta.schemas.letta_message_content import TextContent
@@ -19,7 +20,7 @@ from letta.services.job_manager import JobManager
19
20
  from letta.services.message_manager import MessageManager
20
21
 
21
22
 
22
- class SleeptimeMultiAgent(Agent):
23
+ class SleeptimeMultiAgent(BaseAgent):
23
24
  def __init__(
24
25
  self,
25
26
  interface: AgentInterface,
@@ -268,7 +268,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
268
268
  prior_messages = []
269
269
  if self.group.sleeptime_agent_frequency:
270
270
  try:
271
- prior_messages = await self.message_manager.list_messages_for_agent_async(
271
+ prior_messages = await self.message_manager.list_messages(
272
272
  agent_id=foreground_agent_id,
273
273
  actor=self.actor,
274
274
  after=last_processed_message_id,
@@ -7,14 +7,14 @@ from letta.constants import DEFAULT_MAX_STEPS
7
7
  from letta.groups.helpers import stringify_message
8
8
  from letta.otel.tracing import trace_method
9
9
  from letta.schemas.agent import AgentState
10
- from letta.schemas.enums import JobStatus
10
+ from letta.schemas.enums import JobStatus, RunStatus
11
11
  from letta.schemas.group import Group, ManagerType
12
12
  from letta.schemas.job import JobUpdate
13
13
  from letta.schemas.letta_message import MessageType
14
14
  from letta.schemas.letta_message_content import TextContent
15
15
  from letta.schemas.letta_response import LettaResponse
16
16
  from letta.schemas.message import Message, MessageCreate
17
- from letta.schemas.run import Run
17
+ from letta.schemas.run import Run, RunUpdate
18
18
  from letta.schemas.user import User
19
19
  from letta.services.group_manager import GroupManager
20
20
  from letta.utils import safe_create_task
@@ -134,14 +134,14 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
134
134
  use_assistant_message: bool = True,
135
135
  ) -> str:
136
136
  run = Run(
137
- user_id=self.actor.id,
138
- status=JobStatus.created,
137
+ agent_id=sleeptime_agent_id,
138
+ status=RunStatus.created,
139
139
  metadata={
140
- "job_type": "sleeptime_agent_send_message_async", # is this right?
140
+ "run_type": "sleeptime_agent_send_message_async", # is this right?
141
141
  "agent_id": sleeptime_agent_id,
142
142
  },
143
143
  )
144
- run = await self.job_manager.create_job_async(pydantic_job=run, actor=self.actor)
144
+ run = await self.run_manager.create_run(pydantic_run=run, actor=self.actor)
145
145
 
146
146
  safe_create_task(
147
147
  self._participant_agent_step(
@@ -167,15 +167,15 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
167
167
  use_assistant_message: bool = True,
168
168
  ) -> LettaResponse:
169
169
  try:
170
- # Update job status
171
- job_update = JobUpdate(status=JobStatus.running)
172
- await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
170
+ # Update run status
171
+ run_update = RunUpdate(status=RunStatus.running)
172
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
173
173
 
174
174
  # Create conversation transcript
175
175
  prior_messages = []
176
176
  if self.group.sleeptime_agent_frequency:
177
177
  try:
178
- prior_messages = await self.message_manager.list_messages_for_agent_async(
178
+ prior_messages = await self.message_manager.list_messages(
179
179
  agent_id=foreground_agent_id,
180
180
  actor=self.actor,
181
181
  after=last_processed_message_id,
@@ -212,22 +212,22 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
212
212
  use_assistant_message=use_assistant_message,
213
213
  )
214
214
 
215
- # Update job status
216
- job_update = JobUpdate(
217
- status=JobStatus.completed,
215
+ # Update run status
216
+ run_update = RunUpdate(
217
+ status=RunStatus.completed,
218
218
  completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
219
219
  metadata={
220
220
  "result": result.model_dump(mode="json"),
221
221
  "agent_id": sleeptime_agent_state.id,
222
222
  },
223
223
  )
224
- await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
224
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
225
225
  return result
226
226
  except Exception as e:
227
- job_update = JobUpdate(
228
- status=JobStatus.failed,
227
+ run_update = RunUpdate(
228
+ status=RunStatus.failed,
229
229
  completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
230
230
  metadata={"error": str(e)},
231
231
  )
232
- await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
232
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
233
233
  raise