letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.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
@@ -0,0 +1,161 @@
1
+ """
2
+ Database URI utilities for consistent database connection handling across the application.
3
+
4
+ This module provides utilities for parsing and converting database URIs to ensure
5
+ consistent behavior between the main application, alembic migrations, and other
6
+ database-related components.
7
+ """
8
+
9
+ from typing import Optional
10
+ from urllib.parse import urlparse, urlunparse
11
+
12
+
13
+ def parse_database_uri(uri: str) -> dict[str, Optional[str]]:
14
+ """
15
+ Parse a database URI into its components.
16
+
17
+ Args:
18
+ uri: Database URI (e.g., postgresql://user:pass@host:port/db)
19
+
20
+ Returns:
21
+ Dictionary with parsed components: scheme, driver, user, password, host, port, database
22
+ """
23
+ parsed = urlparse(uri)
24
+
25
+ # Extract driver from scheme (e.g., postgresql+asyncpg -> asyncpg)
26
+ scheme_parts = parsed.scheme.split("+")
27
+ base_scheme = scheme_parts[0] if scheme_parts else ""
28
+ driver = scheme_parts[1] if len(scheme_parts) > 1 else None
29
+
30
+ return {
31
+ "scheme": base_scheme,
32
+ "driver": driver,
33
+ "user": parsed.username,
34
+ "password": parsed.password,
35
+ "host": parsed.hostname,
36
+ "port": str(parsed.port) if parsed.port else None,
37
+ "database": parsed.path.lstrip("/") if parsed.path else None,
38
+ "query": parsed.query,
39
+ "fragment": parsed.fragment,
40
+ }
41
+
42
+
43
+ def build_database_uri(
44
+ scheme: str = "postgresql",
45
+ driver: Optional[str] = None,
46
+ user: Optional[str] = None,
47
+ password: Optional[str] = None,
48
+ host: Optional[str] = None,
49
+ port: Optional[str] = None,
50
+ database: Optional[str] = None,
51
+ query: Optional[str] = None,
52
+ fragment: Optional[str] = None,
53
+ ) -> str:
54
+ """
55
+ Build a database URI from components.
56
+
57
+ Args:
58
+ scheme: Base scheme (e.g., "postgresql")
59
+ driver: Driver name (e.g., "asyncpg", "pg8000")
60
+ user: Username
61
+ password: Password
62
+ host: Hostname
63
+ port: Port number
64
+ database: Database name
65
+ query: Query string
66
+ fragment: Fragment
67
+
68
+ Returns:
69
+ Complete database URI
70
+ """
71
+ # Combine scheme and driver
72
+ full_scheme = f"{scheme}+{driver}" if driver else scheme
73
+
74
+ # Build netloc (user:password@host:port)
75
+ netloc_parts = []
76
+ if user:
77
+ if password:
78
+ netloc_parts.append(f"{user}:{password}")
79
+ else:
80
+ netloc_parts.append(user)
81
+
82
+ if host:
83
+ if port:
84
+ netloc_parts.append(f"{host}:{port}")
85
+ else:
86
+ netloc_parts.append(host)
87
+
88
+ netloc = "@".join(netloc_parts) if netloc_parts else ""
89
+
90
+ # Build path
91
+ path = f"/{database}" if database else ""
92
+
93
+ # Build the URI
94
+ return urlunparse((full_scheme, netloc, path, "", query or "", fragment or ""))
95
+
96
+
97
+ def convert_to_async_uri(uri: str) -> str:
98
+ """
99
+ Convert a database URI to use the asyncpg driver for async operations.
100
+
101
+ Args:
102
+ uri: Original database URI
103
+
104
+ Returns:
105
+ URI with asyncpg driver and ssl parameter adjustments
106
+ """
107
+ components = parse_database_uri(uri)
108
+
109
+ # Convert to asyncpg driver
110
+ components["driver"] = "asyncpg"
111
+
112
+ # Build the new URI
113
+ new_uri = build_database_uri(**components)
114
+
115
+ # Replace sslmode= with ssl= for asyncpg compatibility
116
+ new_uri = new_uri.replace("sslmode=", "ssl=")
117
+
118
+ return new_uri
119
+
120
+
121
+ def convert_to_sync_uri(uri: str) -> str:
122
+ """
123
+ Convert a database URI to use the pg8000 driver for sync operations (alembic).
124
+
125
+ Args:
126
+ uri: Original database URI
127
+
128
+ Returns:
129
+ URI with pg8000 driver and sslmode parameter adjustments
130
+ """
131
+ components = parse_database_uri(uri)
132
+
133
+ # Convert to pg8000 driver
134
+ components["driver"] = "pg8000"
135
+
136
+ # Build the new URI
137
+ new_uri = build_database_uri(**components)
138
+
139
+ # Replace ssl= with sslmode= for pg8000 compatibility
140
+ new_uri = new_uri.replace("ssl=", "sslmode=")
141
+
142
+ return new_uri
143
+
144
+
145
+ def get_database_uri_for_context(uri: str, context: str = "async") -> str:
146
+ """
147
+ Get the appropriate database URI for a specific context.
148
+
149
+ Args:
150
+ uri: Original database URI
151
+ context: Context type ("async" for asyncpg, "sync" for pg8000, "alembic" for pg8000)
152
+
153
+ Returns:
154
+ URI formatted for the specified context
155
+ """
156
+ if context in ["async"]:
157
+ return convert_to_async_uri(uri)
158
+ elif context in ["sync", "alembic"]:
159
+ return convert_to_sync_uri(uri)
160
+ else:
161
+ raise ValueError(f"Unknown context: {context}. Must be 'async', 'sync', or 'alembic'")
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