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
@@ -1,13 +1,13 @@
1
- import asyncio
2
1
  from collections.abc import AsyncGenerator
3
2
  from datetime import datetime, timezone
4
3
 
5
4
  from letta.agents.letta_agent_v2 import LettaAgentV2
5
+ from letta.agents.letta_agent_v3 import LettaAgentV3
6
6
  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, RunStatus
10
+ from letta.schemas.enums import 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
@@ -59,7 +59,7 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
59
59
  request_start_timestamp_ns=request_start_timestamp_ns,
60
60
  )
61
61
 
62
- await self.run_sleeptime_agents(use_assistant_message=use_assistant_message)
62
+ await self.run_sleeptime_agents()
63
63
 
64
64
  response.usage.run_ids = self.run_ids
65
65
  return response
@@ -92,10 +92,10 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
92
92
  ):
93
93
  yield chunk
94
94
 
95
- await self.run_sleeptime_agents(use_assistant_message=use_assistant_message)
95
+ await self.run_sleeptime_agents()
96
96
 
97
97
  @trace_method
98
- async def run_sleeptime_agents(self, use_assistant_message: bool = True):
98
+ async def run_sleeptime_agents(self):
99
99
  # Get response messages
100
100
  last_response_messages = self.response_messages
101
101
 
@@ -117,7 +117,6 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
117
117
  sleeptime_agent_id,
118
118
  last_response_messages,
119
119
  last_processed_message_id,
120
- use_assistant_message,
121
120
  )
122
121
  self.run_ids.append(sleeptime_run_id)
123
122
  except Exception as e:
@@ -131,7 +130,6 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
131
130
  sleeptime_agent_id: str,
132
131
  response_messages: list[Message],
133
132
  last_processed_message_id: str,
134
- use_assistant_message: bool = True,
135
133
  ) -> str:
136
134
  run = Run(
137
135
  agent_id=sleeptime_agent_id,
@@ -150,7 +148,6 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
150
148
  response_messages=response_messages,
151
149
  last_processed_message_id=last_processed_message_id,
152
150
  run_id=run.id,
153
- use_assistant_message=use_assistant_message,
154
151
  ),
155
152
  label=f"participant_agent_step_{sleeptime_agent_id}",
156
153
  )
@@ -164,7 +161,6 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
164
161
  response_messages: list[Message],
165
162
  last_processed_message_id: str,
166
163
  run_id: str,
167
- use_assistant_message: bool = True,
168
164
  ) -> LettaResponse:
169
165
  try:
170
166
  # Update run status
@@ -200,7 +196,7 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
200
196
 
201
197
  # Load sleeptime agent
202
198
  sleeptime_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=sleeptime_agent_id, actor=self.actor)
203
- sleeptime_agent = LettaAgentV2(
199
+ sleeptime_agent = LettaAgentV3(
204
200
  agent_state=sleeptime_agent_state,
205
201
  actor=self.actor,
206
202
  )
@@ -209,7 +205,6 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
209
205
  result = await sleeptime_agent.step(
210
206
  input_messages=sleeptime_agent_messages,
211
207
  run_id=run_id,
212
- use_assistant_message=use_assistant_message,
213
208
  )
214
209
 
215
210
  # Update run status
@@ -0,0 +1,227 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator
3
+ from datetime import datetime, timezone
4
+
5
+ from letta.agents.letta_agent_v3 import LettaAgentV3
6
+ from letta.constants import DEFAULT_MAX_STEPS
7
+ from letta.groups.helpers import stringify_message
8
+ from letta.otel.tracing import trace_method
9
+ from letta.schemas.agent import AgentState
10
+ from letta.schemas.enums import JobStatus, RunStatus
11
+ from letta.schemas.group import Group, ManagerType
12
+ from letta.schemas.job import JobUpdate
13
+ from letta.schemas.letta_message import MessageType
14
+ from letta.schemas.letta_message_content import TextContent
15
+ from letta.schemas.letta_response import LettaResponse
16
+ from letta.schemas.message import Message, MessageCreate
17
+ from letta.schemas.run import Run, RunUpdate
18
+ from letta.schemas.user import User
19
+ from letta.services.group_manager import GroupManager
20
+ from letta.utils import safe_create_task
21
+
22
+
23
+ class SleeptimeMultiAgentV4(LettaAgentV3):
24
+ def __init__(
25
+ self,
26
+ agent_state: AgentState,
27
+ actor: User,
28
+ group: Group,
29
+ ):
30
+ super().__init__(agent_state, actor)
31
+ assert group.manager_type == ManagerType.sleeptime, f"Expected group type to be 'sleeptime', got {group.manager_type}"
32
+ self.group = group
33
+ self.run_ids = []
34
+
35
+ # Additional manager classes
36
+ self.group_manager = GroupManager()
37
+
38
+ @trace_method
39
+ async def step(
40
+ self,
41
+ input_messages: list[MessageCreate],
42
+ max_steps: int = DEFAULT_MAX_STEPS,
43
+ run_id: str | None = None,
44
+ use_assistant_message: bool = True,
45
+ include_return_message_types: list[MessageType] | None = None,
46
+ request_start_timestamp_ns: int | None = None,
47
+ ) -> LettaResponse:
48
+ self.run_ids = []
49
+
50
+ for i in range(len(input_messages)):
51
+ input_messages[i].group_id = self.group.id
52
+
53
+ response = await super().step(
54
+ input_messages=input_messages,
55
+ max_steps=max_steps,
56
+ run_id=run_id,
57
+ use_assistant_message=use_assistant_message,
58
+ include_return_message_types=include_return_message_types,
59
+ request_start_timestamp_ns=request_start_timestamp_ns,
60
+ )
61
+
62
+ await self.run_sleeptime_agents()
63
+ response.usage.run_ids = self.run_ids
64
+ return response
65
+
66
+ @trace_method
67
+ async def stream(
68
+ self,
69
+ input_messages: list[MessageCreate],
70
+ max_steps: int = DEFAULT_MAX_STEPS,
71
+ stream_tokens: bool = True,
72
+ run_id: str | None = None,
73
+ use_assistant_message: bool = True,
74
+ request_start_timestamp_ns: int | None = None,
75
+ include_return_message_types: list[MessageType] | None = None,
76
+ ) -> AsyncGenerator[str, None]:
77
+ self.run_ids = []
78
+
79
+ for i in range(len(input_messages)):
80
+ input_messages[i].group_id = self.group.id
81
+
82
+ # Perform foreground agent step
83
+ async for chunk in super().stream(
84
+ input_messages=input_messages,
85
+ max_steps=max_steps,
86
+ stream_tokens=stream_tokens,
87
+ run_id=run_id,
88
+ use_assistant_message=use_assistant_message,
89
+ include_return_message_types=include_return_message_types,
90
+ request_start_timestamp_ns=request_start_timestamp_ns,
91
+ ):
92
+ yield chunk
93
+
94
+ await self.run_sleeptime_agents()
95
+
96
+ @trace_method
97
+ async def run_sleeptime_agents(self):
98
+ # Get response messages
99
+ last_response_messages = self.response_messages
100
+
101
+ # Update turns counter
102
+ turns_counter = None
103
+ if self.group.sleeptime_agent_frequency is not None and self.group.sleeptime_agent_frequency > 0:
104
+ turns_counter = await self.group_manager.bump_turns_counter_async(group_id=self.group.id, actor=self.actor)
105
+
106
+ # Perform participant steps
107
+ if self.group.sleeptime_agent_frequency is None or (
108
+ turns_counter is not None and turns_counter % self.group.sleeptime_agent_frequency == 0
109
+ ):
110
+ last_processed_message_id = await self.group_manager.get_last_processed_message_id_and_update_async(
111
+ group_id=self.group.id, last_processed_message_id=last_response_messages[-1].id, actor=self.actor
112
+ )
113
+ for sleeptime_agent_id in self.group.agent_ids:
114
+ try:
115
+ sleeptime_run_id = await self._issue_background_task(
116
+ sleeptime_agent_id,
117
+ last_response_messages,
118
+ last_processed_message_id,
119
+ )
120
+ self.run_ids.append(sleeptime_run_id)
121
+ except Exception as e:
122
+ # Individual task failures
123
+ print(f"Sleeptime agent processing failed: {e!s}")
124
+ raise e
125
+
126
+ @trace_method
127
+ async def _issue_background_task(
128
+ self,
129
+ sleeptime_agent_id: str,
130
+ response_messages: list[Message],
131
+ last_processed_message_id: str,
132
+ ) -> str:
133
+ run = Run(
134
+ agent_id=sleeptime_agent_id,
135
+ status=RunStatus.created,
136
+ metadata={
137
+ "run_type": "sleeptime_agent_send_message_async", # is this right?
138
+ "agent_id": sleeptime_agent_id,
139
+ },
140
+ )
141
+ run = await self.run_manager.create_run(pydantic_run=run, actor=self.actor)
142
+
143
+ safe_create_task(
144
+ self._participant_agent_step(
145
+ foreground_agent_id=self.agent_state.id,
146
+ sleeptime_agent_id=sleeptime_agent_id,
147
+ response_messages=response_messages,
148
+ last_processed_message_id=last_processed_message_id,
149
+ run_id=run.id,
150
+ ),
151
+ label=f"participant_agent_step_{sleeptime_agent_id}",
152
+ )
153
+ return run.id
154
+
155
+ @trace_method
156
+ async def _participant_agent_step(
157
+ self,
158
+ foreground_agent_id: str,
159
+ sleeptime_agent_id: str,
160
+ response_messages: list[Message],
161
+ last_processed_message_id: str,
162
+ run_id: str,
163
+ ) -> LettaResponse:
164
+ try:
165
+ # Update run status
166
+ run_update = RunUpdate(status=RunStatus.running)
167
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
168
+
169
+ # Create conversation transcript
170
+ prior_messages = []
171
+ if self.group.sleeptime_agent_frequency:
172
+ try:
173
+ prior_messages = await self.message_manager.list_messages(
174
+ agent_id=foreground_agent_id,
175
+ actor=self.actor,
176
+ after=last_processed_message_id,
177
+ before=response_messages[0].id,
178
+ )
179
+ except Exception:
180
+ pass # continue with just latest messages
181
+
182
+ transcript_summary = [stringify_message(message) for message in prior_messages + response_messages]
183
+ transcript_summary = [summary for summary in transcript_summary if summary is not None]
184
+ message_text = "\n".join(transcript_summary)
185
+
186
+ sleeptime_agent_messages = [
187
+ MessageCreate(
188
+ role="user",
189
+ content=[TextContent(text=message_text)],
190
+ id=Message.generate_id(),
191
+ agent_id=sleeptime_agent_id,
192
+ group_id=self.group.id,
193
+ )
194
+ ]
195
+
196
+ # Load sleeptime agent
197
+ sleeptime_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=sleeptime_agent_id, actor=self.actor)
198
+ sleeptime_agent = LettaAgentV3(
199
+ agent_state=sleeptime_agent_state,
200
+ actor=self.actor,
201
+ )
202
+
203
+ # Perform sleeptime agent step
204
+ result = await sleeptime_agent.step(
205
+ input_messages=sleeptime_agent_messages,
206
+ run_id=run_id,
207
+ )
208
+
209
+ # Update run status
210
+ run_update = RunUpdate(
211
+ status=RunStatus.completed,
212
+ completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
213
+ metadata={
214
+ "result": result.model_dump(mode="json"),
215
+ "agent_id": sleeptime_agent_state.id,
216
+ },
217
+ )
218
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
219
+ return result
220
+ except Exception as e:
221
+ run_update = RunUpdate(
222
+ status=RunStatus.failed,
223
+ completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
224
+ metadata={"error": str(e)},
225
+ )
226
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
227
+ raise
@@ -8,6 +8,7 @@ from sqlalchemy import Dialect
8
8
  from letta.functions.mcp_client.types import StdioServerConfig
9
9
  from letta.schemas.embedding_config import EmbeddingConfig
10
10
  from letta.schemas.enums import ProviderType, ToolRuleType
11
+ from letta.schemas.letta_message import ApprovalReturn, MessageReturnType
11
12
  from letta.schemas.letta_message_content import (
12
13
  ImageContent,
13
14
  ImageSourceType,
@@ -180,6 +181,7 @@ def deserialize_tool_calls(data: Optional[List[Dict]]) -> List[OpenAIToolCall]:
180
181
 
181
182
  calls = []
182
183
  for item in data:
184
+ item.pop("requires_approval", None) # legacy field
183
185
  func_data = item.pop("function", None)
184
186
  tool_call_function = OpenAIFunction(**func_data)
185
187
  calls.append(OpenAIToolCall(function=tool_call_function, **item))
@@ -222,6 +224,49 @@ def deserialize_tool_returns(data: Optional[List[Dict]]) -> List[ToolReturn]:
222
224
  return tool_returns
223
225
 
224
226
 
227
+ # --------------------------
228
+ # Approvals Serialization
229
+ # --------------------------
230
+
231
+
232
+ def serialize_approvals(approvals: Optional[List[Union[ApprovalReturn, ToolReturn, dict]]]) -> List[Dict]:
233
+ """Convert a list of ToolReturn objects into JSON-serializable format."""
234
+ if not approvals:
235
+ return []
236
+
237
+ serialized_approvals = []
238
+ for approval in approvals:
239
+ if isinstance(approval, ApprovalReturn):
240
+ serialized_approvals.append(approval.model_dump(mode="json"))
241
+ elif isinstance(approval, ToolReturn):
242
+ serialized_approvals.append(approval.model_dump(mode="json"))
243
+ elif isinstance(approval, dict):
244
+ serialized_approvals.append(approval) # Already a dictionary, leave it as-is
245
+ else:
246
+ raise TypeError(f"Unexpected approval type: {type(approval)}")
247
+
248
+ return serialized_approvals
249
+
250
+
251
+ def deserialize_approvals(data: Optional[List[Dict]]) -> List[Union[ApprovalReturn, ToolReturn]]:
252
+ """Convert a JSON list back into ApprovalReturn and ToolReturn objects."""
253
+ if not data:
254
+ return []
255
+
256
+ approvals = []
257
+ for item in data:
258
+ if "type" in item and item.get("type") == MessageReturnType.approval:
259
+ approval_return = ApprovalReturn(**item)
260
+ approvals.append(approval_return)
261
+ elif "status" in item:
262
+ tool_return = ToolReturn(**item)
263
+ approvals.append(tool_return)
264
+ else:
265
+ continue
266
+
267
+ return approvals
268
+
269
+
225
270
  # ----------------------------
226
271
  # MessageContent Serialization
227
272
  # ----------------------------
@@ -459,14 +504,43 @@ def deserialize_response_format(data: Optional[Dict]) -> Optional[ResponseFormat
459
504
 
460
505
 
461
506
  def serialize_mcp_stdio_config(config: Union[Optional[StdioServerConfig], Dict]) -> Optional[Dict]:
462
- """Convert an StdioServerConfig object into a JSON-serializable dictionary."""
507
+ """Convert an StdioServerConfig object into a JSON-serializable dictionary.
508
+
509
+ Persist required fields for successful deserialization back into a
510
+ StdioServerConfig model (namely `server_name` and `type`). The
511
+ `to_dict()` helper intentionally omits these since they're not needed
512
+ by MCP transport, but our ORM deserializer reconstructs the pydantic
513
+ model and requires them.
514
+ """
463
515
  if config and isinstance(config, StdioServerConfig):
464
- return config.to_dict()
516
+ data = config.to_dict()
517
+ # Preserve required fields for pydantic reconstruction
518
+ data["server_name"] = config.server_name
519
+ # Store enum as its value; pydantic will coerce on load
520
+ data["type"] = config.type.value if hasattr(config.type, "value") else str(config.type)
521
+ return data
465
522
  return config
466
523
 
467
524
 
468
525
  def deserialize_mcp_stdio_config(data: Optional[Dict]) -> Optional[StdioServerConfig]:
469
- """Convert a dictionary back into an StdioServerConfig object."""
526
+ """Convert a dictionary back into an StdioServerConfig object.
527
+
528
+ Backwards-compatibility notes:
529
+ - Older rows may only include `transport`, `command`, `args`, `env`.
530
+ In that case, provide defaults for `server_name` and `type` to
531
+ satisfy the pydantic model requirements.
532
+ - If both `type` and `transport` are present, prefer `type`.
533
+ """
470
534
  if not data:
471
535
  return None
472
- return StdioServerConfig(**data)
536
+
537
+ payload = dict(data)
538
+ # Map legacy `transport` field to required `type` if missing
539
+ if "type" not in payload and "transport" in payload:
540
+ payload["type"] = payload["transport"]
541
+
542
+ # Ensure required field exists; use a sensible placeholder when unknown
543
+ if "server_name" not in payload:
544
+ payload["server_name"] = payload.get("name", "unknown")
545
+
546
+ return StdioServerConfig(**payload)
@@ -47,7 +47,9 @@ class CryptoUtils:
47
47
  master_key = settings.encryption_key
48
48
 
49
49
  if not master_key:
50
- raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
50
+ raise ValueError(
51
+ "No encryption key configured. Please set the LETTA_ENCRYPTION_KEY environment variable (not fully supported yet for Letta v0.12.1 and below)."
52
+ )
51
53
 
52
54
  # Generate random salt and IV
53
55
  salt = os.urandom(cls.SALT_SIZE)
@@ -91,7 +93,9 @@ class CryptoUtils:
91
93
  master_key = settings.encryption_key
92
94
 
93
95
  if not master_key:
94
- raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
96
+ raise ValueError(
97
+ "No encryption key configured. Please set the LETTA_ENCRYPTION_KEY environment variable (not fully supported yet for Letta v0.12.1 and below)."
98
+ )
95
99
 
96
100
  try:
97
101
  # Decode from base64
@@ -39,6 +39,7 @@ from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
39
39
  from letta.schemas.message import Message
40
40
  from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
41
41
  from letta.server.rest_api.json_parser import JSONParser, PydanticJSONParser
42
+ from letta.server.rest_api.utils import decrement_message_uuid
42
43
 
43
44
  logger = get_logger(__name__)
44
45
 
@@ -282,14 +283,12 @@ class SimpleAnthropicStreamingInterface:
282
283
  call_id = content.id
283
284
  # Initialize arguments from the start event's input (often {}) to avoid undefined in UIs
284
285
  if name in self.requires_approval_tools:
285
- if prev_message_type and prev_message_type != "approval_request_message":
286
- message_index += 1
287
286
  tool_call_msg = ApprovalRequestMessage(
288
- id=self.letta_message_id,
287
+ id=decrement_message_uuid(self.letta_message_id),
289
288
  # Do not emit placeholder arguments here to avoid UI duplicates
290
289
  tool_call=ToolCallDelta(name=name, tool_call_id=call_id),
291
290
  date=datetime.now(timezone.utc).isoformat(),
292
- otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
291
+ otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
293
292
  run_id=self.run_id,
294
293
  step_id=self.step_id,
295
294
  )
@@ -300,12 +299,13 @@ class SimpleAnthropicStreamingInterface:
300
299
  id=self.letta_message_id,
301
300
  # Do not emit placeholder arguments here to avoid UI duplicates
302
301
  tool_call=ToolCallDelta(name=name, tool_call_id=call_id),
302
+ tool_calls=ToolCallDelta(name=name, tool_call_id=call_id),
303
303
  date=datetime.now(timezone.utc).isoformat(),
304
304
  otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
305
305
  run_id=self.run_id,
306
306
  step_id=self.step_id,
307
307
  )
308
- prev_message_type = tool_call_msg.message_type
308
+ prev_message_type = tool_call_msg.message_type
309
309
  yield tool_call_msg
310
310
 
311
311
  elif isinstance(content, BetaThinkingBlock):
@@ -345,7 +345,6 @@ class SimpleAnthropicStreamingInterface:
345
345
 
346
346
  assistant_msg = AssistantMessage(
347
347
  id=self.letta_message_id,
348
- # content=[TextContent(text=delta.text)],
349
348
  content=delta.text,
350
349
  date=datetime.now(timezone.utc).isoformat(),
351
350
  otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
@@ -381,13 +380,11 @@ class SimpleAnthropicStreamingInterface:
381
380
  call_id = ctx.get("id")
382
381
 
383
382
  if name in self.requires_approval_tools:
384
- if prev_message_type and prev_message_type != "approval_request_message":
385
- message_index += 1
386
383
  tool_call_msg = ApprovalRequestMessage(
387
- id=self.letta_message_id,
384
+ id=decrement_message_uuid(self.letta_message_id),
388
385
  tool_call=ToolCallDelta(name=name, tool_call_id=call_id, arguments=delta.partial_json),
389
386
  date=datetime.now(timezone.utc).isoformat(),
390
- otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
387
+ otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
391
388
  run_id=self.run_id,
392
389
  step_id=self.step_id,
393
390
  )
@@ -397,12 +394,13 @@ class SimpleAnthropicStreamingInterface:
397
394
  tool_call_msg = ToolCallMessage(
398
395
  id=self.letta_message_id,
399
396
  tool_call=ToolCallDelta(name=name, tool_call_id=call_id, arguments=delta.partial_json),
397
+ tool_calls=ToolCallDelta(name=name, tool_call_id=call_id, arguments=delta.partial_json),
400
398
  date=datetime.now(timezone.utc).isoformat(),
401
399
  otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
402
400
  run_id=self.run_id,
403
401
  step_id=self.step_id,
404
402
  )
405
-
403
+ prev_message_type = tool_call_msg.message_type
406
404
  yield tool_call_msg
407
405
 
408
406
  elif isinstance(delta, BetaThinkingDelta):
@@ -235,7 +235,7 @@ class AnthropicStreamingInterface:
235
235
  except Exception as e:
236
236
  import traceback
237
237
 
238
- logger.error("Error processing stream: %s\n%s", e, traceback.format_exc())
238
+ logger.exception("Error processing stream: %s", e)
239
239
  if ttft_span:
240
240
  ttft_span.add_event(
241
241
  name="stop_reason",
@@ -454,7 +454,7 @@ class AnthropicStreamingInterface:
454
454
  message_index += 1
455
455
  assistant_msg = AssistantMessage(
456
456
  id=self.letta_message_id,
457
- content=[TextContent(text=send_message_diff)],
457
+ content=send_message_diff,
458
458
  date=datetime.now(timezone.utc).isoformat(),
459
459
  otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
460
460
  run_id=self.run_id,
@@ -734,7 +734,7 @@ class SimpleAnthropicStreamingInterface:
734
734
  except Exception as e:
735
735
  import traceback
736
736
 
737
- logger.error("Error processing stream: %s\n%s", e, traceback.format_exc())
737
+ logger.exception("Error processing stream: %s", e)
738
738
  if ttft_span:
739
739
  ttft_span.add_event(
740
740
  name="stop_reason",
@@ -836,7 +836,6 @@ class SimpleAnthropicStreamingInterface:
836
836
 
837
837
  assistant_msg = AssistantMessage(
838
838
  id=self.letta_message_id,
839
- # content=[TextContent(text=delta.text)],
840
839
  content=delta.text,
841
840
  date=datetime.now(timezone.utc).isoformat(),
842
841
  otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
@@ -26,6 +26,7 @@ from letta.schemas.letta_message_content import (
26
26
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
27
27
  from letta.schemas.message import Message
28
28
  from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
29
+ from letta.server.rest_api.utils import decrement_message_uuid
29
30
  from letta.utils import get_tool_call_id
30
31
 
31
32
  logger = get_logger(__name__)
@@ -138,7 +139,7 @@ class SimpleGeminiStreamingInterface:
138
139
  except Exception as e:
139
140
  import traceback
140
141
 
141
- logger.error("Error processing stream: %s\n%s", e, traceback.format_exc())
142
+ logger.exception("Error processing stream: %s", e)
142
143
  if ttft_span:
143
144
  ttft_span.add_event(
144
145
  name="stop_reason",
@@ -255,11 +256,9 @@ class SimpleGeminiStreamingInterface:
255
256
  self.tool_call_args = arguments
256
257
 
257
258
  if self.tool_call_name and self.tool_call_name in self.requires_approval_tools:
258
- if prev_message_type and prev_message_type != "approval_request_message":
259
- message_index += 1
260
259
  yield ApprovalRequestMessage(
261
- id=self.letta_message_id,
262
- otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
260
+ id=decrement_message_uuid(self.letta_message_id),
261
+ otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
263
262
  date=datetime.now(timezone.utc),
264
263
  tool_call=ToolCallDelta(
265
264
  name=name,
@@ -269,7 +268,6 @@ class SimpleGeminiStreamingInterface:
269
268
  run_id=self.run_id,
270
269
  step_id=self.step_id,
271
270
  )
272
- prev_message_type = "approval_request_message"
273
271
  else:
274
272
  if prev_message_type and prev_message_type != "tool_call_message":
275
273
  message_index += 1