letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251024223017__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (159) hide show
  1. letta/__init__.py +2 -3
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/simple_llm_request_adapter.py +8 -5
  4. letta/adapters/simple_llm_stream_adapter.py +22 -6
  5. letta/agents/agent_loop.py +10 -3
  6. letta/agents/base_agent.py +4 -1
  7. letta/agents/helpers.py +41 -9
  8. letta/agents/letta_agent.py +11 -10
  9. letta/agents/letta_agent_v2.py +47 -37
  10. letta/agents/letta_agent_v3.py +395 -300
  11. letta/agents/voice_agent.py +8 -6
  12. letta/agents/voice_sleeptime_agent.py +3 -3
  13. letta/constants.py +30 -7
  14. letta/errors.py +20 -0
  15. letta/functions/function_sets/base.py +55 -3
  16. letta/functions/mcp_client/types.py +33 -57
  17. letta/functions/schema_generator.py +135 -23
  18. letta/groups/sleeptime_multi_agent_v3.py +6 -11
  19. letta/groups/sleeptime_multi_agent_v4.py +227 -0
  20. letta/helpers/converters.py +78 -4
  21. letta/helpers/crypto_utils.py +6 -2
  22. letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
  23. letta/interfaces/anthropic_streaming_interface.py +3 -4
  24. letta/interfaces/gemini_streaming_interface.py +4 -6
  25. letta/interfaces/openai_streaming_interface.py +63 -28
  26. letta/llm_api/anthropic_client.py +7 -4
  27. letta/llm_api/deepseek_client.py +6 -4
  28. letta/llm_api/google_ai_client.py +3 -12
  29. letta/llm_api/google_vertex_client.py +1 -1
  30. letta/llm_api/helpers.py +90 -61
  31. letta/llm_api/llm_api_tools.py +4 -1
  32. letta/llm_api/openai.py +12 -12
  33. letta/llm_api/openai_client.py +53 -16
  34. letta/local_llm/constants.py +4 -3
  35. letta/local_llm/json_parser.py +5 -2
  36. letta/local_llm/utils.py +2 -3
  37. letta/log.py +171 -7
  38. letta/orm/agent.py +43 -9
  39. letta/orm/archive.py +4 -0
  40. letta/orm/custom_columns.py +15 -0
  41. letta/orm/identity.py +11 -11
  42. letta/orm/mcp_server.py +9 -0
  43. letta/orm/message.py +6 -1
  44. letta/orm/run_metrics.py +7 -2
  45. letta/orm/sqlalchemy_base.py +2 -2
  46. letta/orm/tool.py +3 -0
  47. letta/otel/tracing.py +2 -0
  48. letta/prompts/prompt_generator.py +7 -2
  49. letta/schemas/agent.py +41 -10
  50. letta/schemas/agent_file.py +3 -0
  51. letta/schemas/archive.py +4 -2
  52. letta/schemas/block.py +2 -1
  53. letta/schemas/enums.py +36 -3
  54. letta/schemas/file.py +3 -3
  55. letta/schemas/folder.py +2 -1
  56. letta/schemas/group.py +2 -1
  57. letta/schemas/identity.py +18 -9
  58. letta/schemas/job.py +3 -1
  59. letta/schemas/letta_message.py +71 -12
  60. letta/schemas/letta_request.py +7 -3
  61. letta/schemas/letta_stop_reason.py +0 -25
  62. letta/schemas/llm_config.py +8 -2
  63. letta/schemas/mcp.py +80 -83
  64. letta/schemas/mcp_server.py +349 -0
  65. letta/schemas/memory.py +20 -8
  66. letta/schemas/message.py +212 -67
  67. letta/schemas/providers/anthropic.py +13 -6
  68. letta/schemas/providers/azure.py +6 -4
  69. letta/schemas/providers/base.py +8 -4
  70. letta/schemas/providers/bedrock.py +6 -2
  71. letta/schemas/providers/cerebras.py +7 -3
  72. letta/schemas/providers/deepseek.py +2 -1
  73. letta/schemas/providers/google_gemini.py +15 -6
  74. letta/schemas/providers/groq.py +2 -1
  75. letta/schemas/providers/lmstudio.py +9 -6
  76. letta/schemas/providers/mistral.py +2 -1
  77. letta/schemas/providers/openai.py +7 -2
  78. letta/schemas/providers/together.py +9 -3
  79. letta/schemas/providers/xai.py +7 -3
  80. letta/schemas/run.py +7 -2
  81. letta/schemas/run_metrics.py +2 -1
  82. letta/schemas/sandbox_config.py +2 -2
  83. letta/schemas/secret.py +3 -158
  84. letta/schemas/source.py +2 -2
  85. letta/schemas/step.py +2 -2
  86. letta/schemas/tool.py +24 -1
  87. letta/schemas/usage.py +0 -1
  88. letta/server/rest_api/app.py +123 -7
  89. letta/server/rest_api/dependencies.py +3 -0
  90. letta/server/rest_api/interface.py +7 -4
  91. letta/server/rest_api/redis_stream_manager.py +16 -1
  92. letta/server/rest_api/routers/v1/__init__.py +7 -0
  93. letta/server/rest_api/routers/v1/agents.py +332 -322
  94. letta/server/rest_api/routers/v1/archives.py +127 -40
  95. letta/server/rest_api/routers/v1/blocks.py +54 -6
  96. letta/server/rest_api/routers/v1/chat_completions.py +146 -0
  97. letta/server/rest_api/routers/v1/folders.py +27 -35
  98. letta/server/rest_api/routers/v1/groups.py +23 -35
  99. letta/server/rest_api/routers/v1/identities.py +24 -10
  100. letta/server/rest_api/routers/v1/internal_runs.py +107 -0
  101. letta/server/rest_api/routers/v1/internal_templates.py +162 -179
  102. letta/server/rest_api/routers/v1/jobs.py +15 -27
  103. letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
  104. letta/server/rest_api/routers/v1/messages.py +23 -34
  105. letta/server/rest_api/routers/v1/organizations.py +6 -27
  106. letta/server/rest_api/routers/v1/providers.py +35 -62
  107. letta/server/rest_api/routers/v1/runs.py +30 -43
  108. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
  109. letta/server/rest_api/routers/v1/sources.py +26 -42
  110. letta/server/rest_api/routers/v1/steps.py +16 -29
  111. letta/server/rest_api/routers/v1/tools.py +17 -13
  112. letta/server/rest_api/routers/v1/users.py +5 -17
  113. letta/server/rest_api/routers/v1/voice.py +18 -27
  114. letta/server/rest_api/streaming_response.py +5 -2
  115. letta/server/rest_api/utils.py +187 -25
  116. letta/server/server.py +27 -22
  117. letta/server/ws_api/server.py +5 -4
  118. letta/services/agent_manager.py +148 -26
  119. letta/services/agent_serialization_manager.py +6 -1
  120. letta/services/archive_manager.py +168 -15
  121. letta/services/block_manager.py +14 -4
  122. letta/services/file_manager.py +33 -29
  123. letta/services/group_manager.py +10 -0
  124. letta/services/helpers/agent_manager_helper.py +65 -11
  125. letta/services/identity_manager.py +105 -4
  126. letta/services/job_manager.py +11 -1
  127. letta/services/mcp/base_client.py +2 -2
  128. letta/services/mcp/oauth_utils.py +33 -8
  129. letta/services/mcp_manager.py +174 -78
  130. letta/services/mcp_server_manager.py +1331 -0
  131. letta/services/message_manager.py +109 -4
  132. letta/services/organization_manager.py +4 -4
  133. letta/services/passage_manager.py +9 -25
  134. letta/services/provider_manager.py +91 -15
  135. letta/services/run_manager.py +72 -15
  136. letta/services/sandbox_config_manager.py +45 -3
  137. letta/services/source_manager.py +15 -8
  138. letta/services/step_manager.py +24 -1
  139. letta/services/streaming_service.py +581 -0
  140. letta/services/summarizer/summarizer.py +1 -1
  141. letta/services/tool_executor/core_tool_executor.py +111 -0
  142. letta/services/tool_executor/files_tool_executor.py +5 -3
  143. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  144. letta/services/tool_executor/tool_execution_manager.py +1 -1
  145. letta/services/tool_manager.py +10 -3
  146. letta/services/tool_sandbox/base.py +61 -1
  147. letta/services/tool_sandbox/local_sandbox.py +1 -3
  148. letta/services/user_manager.py +2 -2
  149. letta/settings.py +49 -5
  150. letta/system.py +14 -5
  151. letta/utils.py +73 -1
  152. letta/validators.py +105 -0
  153. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
  155. letta/schemas/letta_ping.py +0 -28
  156. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  157. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,349 @@
1
+ import json
2
+ from datetime import datetime
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ from pydantic import Field
6
+
7
+ from letta.functions.mcp_client.types import (
8
+ MCP_AUTH_HEADER_AUTHORIZATION,
9
+ MCP_AUTH_TOKEN_BEARER_PREFIX,
10
+ MCPServerType,
11
+ SSEServerConfig,
12
+ StdioServerConfig,
13
+ StreamableHTTPServerConfig,
14
+ )
15
+ from letta.orm.mcp_oauth import OAuthSessionStatus
16
+ from letta.schemas.letta_base import LettaBase
17
+ from letta.schemas.secret import Secret
18
+
19
+
20
+ class BaseMCPServer(LettaBase):
21
+ __id_prefix__ = "mcp_server"
22
+
23
+
24
+ # Create Schemas (for POST requests)
25
+ class CreateStdioMCPServer(StdioServerConfig):
26
+ """Create a new Stdio MCP server"""
27
+
28
+
29
+ class CreateSSEMCPServer(SSEServerConfig):
30
+ """Create a new SSE MCP server"""
31
+
32
+
33
+ class CreateStreamableHTTPMCPServer(StreamableHTTPServerConfig):
34
+ """Create a new Streamable HTTP MCP server"""
35
+
36
+
37
+ CreateMCPServerUnion = Union[CreateStdioMCPServer, CreateSSEMCPServer, CreateStreamableHTTPMCPServer]
38
+
39
+
40
+ class StdioMCPServer(CreateStdioMCPServer):
41
+ """A Stdio MCP server"""
42
+
43
+ id: str = BaseMCPServer.generate_id_field()
44
+ type: MCPServerType = MCPServerType.STDIO
45
+
46
+
47
+ class SSEMCPServer(CreateSSEMCPServer):
48
+ """An SSE MCP server"""
49
+
50
+ id: str = BaseMCPServer.generate_id_field()
51
+ type: MCPServerType = MCPServerType.SSE
52
+
53
+
54
+ class StreamableHTTPMCPServer(CreateStreamableHTTPMCPServer):
55
+ """A Streamable HTTP MCP server"""
56
+
57
+ id: str = BaseMCPServer.generate_id_field()
58
+ type: MCPServerType = MCPServerType.STREAMABLE_HTTP
59
+
60
+
61
+ MCPServerUnion = Union[StdioMCPServer, SSEMCPServer, StreamableHTTPMCPServer]
62
+
63
+
64
+ # Update Schemas (for PATCH requests) - same shape as Create/Config, but all fields optional.
65
+ # We exclude fields that aren't persisted on the server model to avoid invalid ORM assignments.
66
+ class UpdateStdioMCPServer(LettaBase):
67
+ """Update schema for Stdio MCP server - all fields optional"""
68
+
69
+ server_name: Optional[str] = Field(None, description="The name of the MCP server")
70
+ command: Optional[str] = Field(None, description="The command to run the MCP server")
71
+ args: Optional[List[str]] = Field(None, description="The arguments to pass to the command")
72
+ env: Optional[Dict[str, str]] = Field(None, description="Environment variables to set")
73
+
74
+
75
+ class UpdateSSEMCPServer(LettaBase):
76
+ """Update schema for SSE MCP server - all fields optional"""
77
+
78
+ server_name: Optional[str] = Field(None, description="The name of the MCP server")
79
+ server_url: Optional[str] = Field(None, description="The URL of the SSE MCP server")
80
+ # Accept both `auth_token` (API surface) and `token` (internal ORM naming)
81
+ auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
82
+ token: Optional[str] = Field(None, description="The authentication token (internal)")
83
+ auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
84
+ custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom headers to send with requests")
85
+
86
+
87
+ class UpdateStreamableHTTPMCPServer(LettaBase):
88
+ """Update schema for Streamable HTTP MCP server - all fields optional"""
89
+
90
+ server_name: Optional[str] = Field(None, description="The name of the MCP server")
91
+ server_url: Optional[str] = Field(None, description="The URL of the Streamable HTTP MCP server")
92
+ # Accept both `auth_token` (API surface) and `token` (internal ORM naming)
93
+ auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
94
+ token: Optional[str] = Field(None, description="The authentication token (internal)")
95
+ auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
96
+ custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom headers to send with requests")
97
+
98
+
99
+ UpdateMCPServerUnion = Union[UpdateStdioMCPServer, UpdateSSEMCPServer, UpdateStreamableHTTPMCPServer]
100
+
101
+
102
+ # OAuth-related schemas
103
+ class BaseMCPOAuth(LettaBase):
104
+ __id_prefix__ = "mcp-oauth"
105
+
106
+
107
+ class MCPOAuthSession(BaseMCPOAuth):
108
+ """OAuth session for MCP server authentication."""
109
+
110
+ id: str = BaseMCPOAuth.generate_id_field()
111
+ state: str = Field(..., description="OAuth state parameter")
112
+ server_id: Optional[str] = Field(None, description="MCP server ID")
113
+ server_url: str = Field(..., description="MCP server URL")
114
+ server_name: str = Field(..., description="MCP server display name")
115
+
116
+ # User and organization context
117
+ user_id: Optional[str] = Field(None, description="User ID associated with the session")
118
+ organization_id: str = Field(..., description="Organization ID associated with the session")
119
+
120
+ # OAuth flow data
121
+ authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
122
+ authorization_code: Optional[str] = Field(None, description="OAuth authorization code")
123
+
124
+ # Encrypted authorization code (for internal use)
125
+ authorization_code_enc: Secret | None = Field(None, description="Encrypted OAuth authorization code as Secret object")
126
+
127
+ # Token data
128
+ access_token: Optional[str] = Field(None, description="OAuth access token")
129
+ refresh_token: Optional[str] = Field(None, description="OAuth refresh token")
130
+ token_type: str = Field(default="Bearer", description="Token type")
131
+ expires_at: Optional[datetime] = Field(None, description="Token expiry time")
132
+ scope: Optional[str] = Field(None, description="OAuth scope")
133
+
134
+ # Encrypted token fields (for internal use)
135
+ access_token_enc: Secret | None = Field(None, description="Encrypted OAuth access token as Secret object")
136
+ refresh_token_enc: Secret | None = Field(None, description="Encrypted OAuth refresh token as Secret object")
137
+
138
+ # Client configuration
139
+ client_id: Optional[str] = Field(None, description="OAuth client ID")
140
+ client_secret: Optional[str] = Field(None, description="OAuth client secret")
141
+ redirect_uri: Optional[str] = Field(None, description="OAuth redirect URI")
142
+
143
+ # Encrypted client secret (for internal use)
144
+ client_secret_enc: Secret | None = Field(None, description="Encrypted OAuth client secret as Secret object")
145
+
146
+ # Session state
147
+ status: OAuthSessionStatus = Field(default=OAuthSessionStatus.PENDING, description="Session status")
148
+
149
+ # Timestamps
150
+ created_at: datetime = Field(default_factory=datetime.now, description="Session creation time")
151
+ updated_at: datetime = Field(default_factory=datetime.now, description="Last update time")
152
+
153
+ def get_access_token_secret(self) -> Secret:
154
+ """Get the access token as a Secret object, preferring encrypted over plaintext."""
155
+ if self.access_token_enc is not None:
156
+ return self.access_token_enc
157
+ return Secret.from_db(None, self.access_token)
158
+
159
+ def get_refresh_token_secret(self) -> Secret:
160
+ """Get the refresh token as a Secret object, preferring encrypted over plaintext."""
161
+ if self.refresh_token_enc is not None:
162
+ return self.refresh_token_enc
163
+ return Secret.from_db(None, self.refresh_token)
164
+
165
+ def get_client_secret_secret(self) -> Secret:
166
+ """Get the client secret as a Secret object, preferring encrypted over plaintext."""
167
+ if self.client_secret_enc is not None:
168
+ return self.client_secret_enc
169
+ return Secret.from_db(None, self.client_secret)
170
+
171
+ def get_authorization_code_secret(self) -> Secret:
172
+ """Get the authorization code as a Secret object, preferring encrypted over plaintext."""
173
+ if self.authorization_code_enc is not None:
174
+ return self.authorization_code_enc
175
+ return Secret.from_db(None, self.authorization_code)
176
+
177
+ def set_access_token_secret(self, secret: Secret) -> None:
178
+ """Set access token from a Secret object."""
179
+ self.access_token_enc = secret
180
+ secret_dict = secret.to_dict()
181
+ if not secret.was_encrypted:
182
+ self.access_token = secret_dict["plaintext"]
183
+ else:
184
+ self.access_token = None
185
+
186
+ def set_refresh_token_secret(self, secret: Secret) -> None:
187
+ """Set refresh token from a Secret object."""
188
+ self.refresh_token_enc = secret
189
+ secret_dict = secret.to_dict()
190
+ if not secret.was_encrypted:
191
+ self.refresh_token = secret_dict["plaintext"]
192
+ else:
193
+ self.refresh_token = None
194
+
195
+ def set_client_secret_secret(self, secret: Secret) -> None:
196
+ """Set client secret from a Secret object."""
197
+ self.client_secret_enc = secret
198
+ secret_dict = secret.to_dict()
199
+ if not secret.was_encrypted:
200
+ self.client_secret = secret_dict["plaintext"]
201
+ else:
202
+ self.client_secret = None
203
+
204
+ def set_authorization_code_secret(self, secret: Secret) -> None:
205
+ """Set authorization code from a Secret object."""
206
+ self.authorization_code_enc = secret
207
+ secret_dict = secret.to_dict()
208
+ if not secret.was_encrypted:
209
+ self.authorization_code = secret_dict["plaintext"]
210
+ else:
211
+ self.authorization_code = None
212
+
213
+
214
+ class MCPOAuthSessionCreate(BaseMCPOAuth):
215
+ """Create a new OAuth session."""
216
+
217
+ server_url: str = Field(..., description="MCP server URL")
218
+ server_name: str = Field(..., description="MCP server display name")
219
+ user_id: Optional[str] = Field(None, description="User ID associated with the session")
220
+ organization_id: str = Field(..., description="Organization ID associated with the session")
221
+ state: Optional[str] = Field(None, description="OAuth state parameter")
222
+
223
+
224
+ class MCPOAuthSessionUpdate(BaseMCPOAuth):
225
+ """Update an existing OAuth session."""
226
+
227
+ authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
228
+ authorization_code: Optional[str] = Field(None, description="OAuth authorization code")
229
+ access_token: Optional[str] = Field(None, description="OAuth access token")
230
+ refresh_token: Optional[str] = Field(None, description="OAuth refresh token")
231
+ token_type: Optional[str] = Field(None, description="Token type")
232
+ expires_at: Optional[datetime] = Field(None, description="Token expiry time")
233
+ scope: Optional[str] = Field(None, description="OAuth scope")
234
+ client_id: Optional[str] = Field(None, description="OAuth client ID")
235
+ client_secret: Optional[str] = Field(None, description="OAuth client secret")
236
+ redirect_uri: Optional[str] = Field(None, description="OAuth redirect URI")
237
+ status: Optional[OAuthSessionStatus] = Field(None, description="Session status")
238
+
239
+
240
+ class MCPServerResyncResult(LettaBase):
241
+ """Result of resyncing MCP server tools."""
242
+
243
+ deleted: List[str] = Field(default_factory=list, description="List of deleted tool names")
244
+ updated: List[str] = Field(default_factory=list, description="List of updated tool names")
245
+ added: List[str] = Field(default_factory=list, description="List of added tool names")
246
+
247
+
248
+ class MCPToolExecuteRequest(LettaBase):
249
+ """Request to execute an MCP tool by IDs."""
250
+
251
+ args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the MCP tool")
252
+
253
+
254
+ def convert_generic_to_union(server) -> MCPServerUnion:
255
+ """
256
+ Convert a generic MCPServer (from letta.schemas.mcp) to the appropriate MCPServerUnion type
257
+ based on the server_type field.
258
+
259
+ This is used to convert internal MCPServer representations to the API response types.
260
+
261
+ Args:
262
+ server: A GenericMCPServer instance from letta.schemas.mcp
263
+
264
+ Returns:
265
+ The appropriate MCPServerUnion type (StdioMCPServer, SSEMCPServer, or StreamableHTTPMCPServer)
266
+ """
267
+ # Import here to avoid circular dependency
268
+ from letta.schemas.mcp import MCPServer as GenericMCPServer
269
+
270
+ if not isinstance(server, GenericMCPServer):
271
+ raise TypeError(f"Expected GenericMCPServer, got {type(server)}")
272
+
273
+ if server.server_type == MCPServerType.STDIO:
274
+ return StdioMCPServer(
275
+ id=server.id,
276
+ server_name=server.server_name,
277
+ type=MCPServerType.STDIO,
278
+ command=server.stdio_config.command if server.stdio_config else None,
279
+ args=server.stdio_config.args if server.stdio_config else None,
280
+ env=server.stdio_config.env if server.stdio_config else None,
281
+ )
282
+ elif server.server_type == MCPServerType.SSE:
283
+ return SSEMCPServer(
284
+ id=server.id,
285
+ server_name=server.server_name,
286
+ type=MCPServerType.SSE,
287
+ server_url=server.server_url,
288
+ auth_header="Authorization" if server.token else None,
289
+ auth_token=f"Bearer {server.token}" if server.token else None,
290
+ custom_headers=server.custom_headers,
291
+ )
292
+ elif server.server_type == MCPServerType.STREAMABLE_HTTP:
293
+ return StreamableHTTPMCPServer(
294
+ id=server.id,
295
+ server_name=server.server_name,
296
+ type=MCPServerType.STREAMABLE_HTTP,
297
+ server_url=server.server_url,
298
+ auth_header="Authorization" if server.token else None,
299
+ auth_token=f"Bearer {server.token}" if server.token else None,
300
+ custom_headers=server.custom_headers,
301
+ )
302
+ else:
303
+ raise ValueError(f"Unknown server type: {server.server_type}")
304
+
305
+
306
+ def convert_update_to_internal(request: Union[UpdateStdioMCPServer, UpdateSSEMCPServer, UpdateStreamableHTTPMCPServer]):
307
+ """Convert external API update models to internal UpdateMCPServer union used by the manager.
308
+
309
+ - Flattens stdio fields into StdioServerConfig inside UpdateStdioMCPServer
310
+ - Maps `auth_token` to `token` for HTTP-based transports
311
+ - Ignores `auth_header` at update time (header is derived from token)
312
+ """
313
+ # Local import to avoid circulars
314
+ from letta.functions.mcp_client.types import MCPServerType as MCPType, StdioServerConfig as StdioCfg
315
+ from letta.schemas.mcp import (
316
+ UpdateSSEMCPServer as InternalUpdateSSE,
317
+ UpdateStdioMCPServer as InternalUpdateStdio,
318
+ UpdateStreamableHTTPMCPServer as InternalUpdateHTTP,
319
+ )
320
+
321
+ if isinstance(request, UpdateStdioMCPServer):
322
+ stdio_cfg = None
323
+ # Only build stdio_config if command and args are explicitly provided to avoid overwriting existing config
324
+ if request.command is not None and request.args is not None:
325
+ stdio_cfg = StdioCfg(
326
+ server_name=request.server_name or "",
327
+ type=MCPType.STDIO,
328
+ command=request.command,
329
+ args=request.args,
330
+ env=request.env,
331
+ )
332
+ kwargs: dict = {}
333
+ if request.server_name is not None:
334
+ kwargs["server_name"] = request.server_name
335
+ if stdio_cfg is not None:
336
+ kwargs["stdio_config"] = stdio_cfg
337
+ return InternalUpdateStdio(**kwargs)
338
+ elif isinstance(request, UpdateSSEMCPServer):
339
+ token_value = request.auth_token or request.token
340
+ return InternalUpdateSSE(
341
+ server_name=request.server_name, server_url=request.server_url, token=token_value, custom_headers=request.custom_headers
342
+ )
343
+ elif isinstance(request, UpdateStreamableHTTPMCPServer):
344
+ token_value = request.auth_token or request.token
345
+ return InternalUpdateHTTP(
346
+ server_name=request.server_name, server_url=request.server_url, auth_token=token_value, custom_headers=request.custom_headers
347
+ )
348
+ else:
349
+ raise TypeError(f"Unsupported update request type: {type(request)}")
letta/schemas/memory.py CHANGED
@@ -4,6 +4,10 @@ from datetime import datetime
4
4
  from io import StringIO
5
5
  from typing import TYPE_CHECKING, List, Optional, Union
6
6
 
7
+ from letta.log import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
7
11
  from openai.types.beta.function_tool import FunctionTool as OpenAITool
8
12
  from pydantic import BaseModel, Field, field_validator
9
13
 
@@ -153,11 +157,11 @@ class Memory(BaseModel, validate_assignment=True):
153
157
  s.write(f"\n- chars_current={len(value)}")
154
158
  s.write(f"\n- chars_limit={limit}\n")
155
159
  s.write("</metadata>\n")
160
+ s.write(f"<warning>\n{CORE_MEMORY_LINE_NUMBER_WARNING}\n</warning>\n")
156
161
  s.write("<value>\n")
157
- s.write(f"{CORE_MEMORY_LINE_NUMBER_WARNING}\n")
158
162
  if value:
159
163
  for i, line in enumerate(value.split("\n"), start=1):
160
- s.write(f"Line {i}: {line}\n")
164
+ s.write(f"{i} {line}\n")
161
165
  s.write("</value>\n")
162
166
  s.write(f"</{label}>\n")
163
167
  if idx != len(self.blocks) - 1:
@@ -264,14 +268,21 @@ class Memory(BaseModel, validate_assignment=True):
264
268
  s.write("</directory>\n")
265
269
  s.write("</directories>")
266
270
 
267
- def compile(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
271
+ def compile(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None) -> str:
268
272
  """Efficiently render memory, tool rules, and sources into a prompt string."""
269
273
  s = StringIO()
270
274
 
271
275
  raw_type = self.agent_type.value if hasattr(self.agent_type, "value") else (self.agent_type or "")
272
276
  norm_type = raw_type.lower()
273
277
  is_react = norm_type in ("react_agent", "workflow_agent")
274
- is_line_numbered = norm_type in ("sleeptime_agent", "memgpt_v2_agent", "letta_v1_agent")
278
+
279
+ # Check if we should use line numbers based on both agent type and model provider
280
+ is_line_numbered = False # Default to no line numbers
281
+ if llm_config and hasattr(llm_config, "model_endpoint_type"):
282
+ is_anthropic = llm_config.model_endpoint_type == "anthropic"
283
+ is_line_numbered_agent_type = norm_type in ("sleeptime_agent", "memgpt_v2_agent", "letta_v1_agent")
284
+ # Only use line numbers for specific agent types AND Anthropic models
285
+ is_line_numbered = is_line_numbered_agent_type and is_anthropic
275
286
 
276
287
  # Memory blocks (not for react/workflow). Always include wrapper for preview/tests.
277
288
  if not is_react:
@@ -297,22 +308,23 @@ class Memory(BaseModel, validate_assignment=True):
297
308
  return s.getvalue()
298
309
 
299
310
  @trace_method
300
- async def compile_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
311
+ async def compile_async(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None) -> str:
301
312
  """Async version that offloads to a thread for CPU-bound string building."""
302
313
  return await asyncio.to_thread(
303
314
  self.compile,
304
315
  tool_usage_rules=tool_usage_rules,
305
316
  sources=sources,
306
317
  max_files_open=max_files_open,
318
+ llm_config=llm_config,
307
319
  )
308
320
 
309
321
  @trace_method
310
- async def compile_in_thread_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
322
+ async def compile_in_thread_async(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None) -> str:
311
323
  """Deprecated: use compile() instead."""
312
324
  import warnings
313
325
 
314
- warnings.warn("compile_in_thread_async is deprecated; use compile()", DeprecationWarning, stacklevel=2)
315
- return self.compile(tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open)
326
+ logger.warning("compile_in_thread_async is deprecated; use compile()", stacklevel=2)
327
+ return self.compile(tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open, llm_config=llm_config)
316
328
 
317
329
  def list_block_labels(self) -> List[str]:
318
330
  """Return a list of the block names held inside the memory object"""