letta-nightly 0.12.1.dev20251023104211__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.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251023104211.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.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
letta/__init__.py CHANGED
@@ -5,7 +5,7 @@ try:
5
5
  __version__ = version("letta")
6
6
  except PackageNotFoundError:
7
7
  # Fallback for development installations
8
- __version__ = "0.12.1"
8
+ __version__ = "0.13.0"
9
9
 
10
10
  if os.environ.get("LETTA_VERSION"):
11
11
  __version__ = os.environ["LETTA_VERSION"]
@@ -28,8 +28,7 @@ from letta.schemas.embedding_config import EmbeddingConfig
28
28
  from letta.schemas.enums import JobStatus
29
29
  from letta.schemas.file import FileMetadata
30
30
  from letta.schemas.job import Job
31
- from letta.schemas.letta_message import LettaMessage
32
- from letta.schemas.letta_ping import LettaPing
31
+ from letta.schemas.letta_message import LettaMessage, LettaPing
33
32
  from letta.schemas.letta_stop_reason import LettaStopReason
34
33
  from letta.schemas.llm_config import LLMConfig
35
34
  from letta.schemas.memory import ArchivalMemorySummary, BasicBlockMemory, ChatMemory, Memory, RecallMemorySummary
@@ -30,6 +30,7 @@ class LettaLLMAdapter(ABC):
30
30
  self.reasoning_content: list[TextContent | ReasoningContent | RedactedReasoningContent] | None = None
31
31
  self.content: list[TextContent | ReasoningContent | RedactedReasoningContent] | None = None
32
32
  self.tool_call: ToolCall | None = None
33
+ self.tool_calls: list[ToolCall] = []
33
34
  self.usage: LettaUsageStatistics = LettaUsageStatistics()
34
35
  self.telemetry_manager: TelemetryManager = TelemetryManager()
35
36
  self.llm_request_finish_timestamp_ns: int | None = None
@@ -38,7 +38,11 @@ class SimpleLLMRequestAdapter(LettaLLMRequestAdapter):
38
38
  self.request_data = request_data
39
39
 
40
40
  # Make the blocking LLM request
41
- self.response_data = await self.llm_client.request_async(request_data, self.llm_config)
41
+ try:
42
+ self.response_data = await self.llm_client.request_async(request_data, self.llm_config)
43
+ except Exception as e:
44
+ raise self.llm_client.handle_llm_error(e)
45
+
42
46
  self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns()
43
47
 
44
48
  # Convert response to chat completion format
@@ -71,10 +75,9 @@ class SimpleLLMRequestAdapter(LettaLLMRequestAdapter):
71
75
  self.content = self.reasoning_content + (self.content or [])
72
76
 
73
77
  # Extract tool call
74
- if self.chat_completions_response.choices[0].message.tool_calls:
75
- self.tool_call = self.chat_completions_response.choices[0].message.tool_calls[0]
76
- else:
77
- self.tool_call = None
78
+ tool_calls = self.chat_completions_response.choices[0].message.tool_calls or []
79
+ self.tool_calls = list(tool_calls)
80
+ self.tool_call = self.tool_calls[0] if self.tool_calls else None
78
81
 
79
82
  # Extract usage statistics
80
83
  self.usage.step_count = 1
@@ -25,6 +25,24 @@ class SimpleLLMStreamAdapter(LettaLLMStreamAdapter):
25
25
  specific streaming formats.
26
26
  """
27
27
 
28
+ def _extract_tool_calls(self) -> list:
29
+ """extract tool calls from interface, trying parallel API first then single API"""
30
+ # try multi-call api if available
31
+ if hasattr(self.interface, "get_tool_call_objects"):
32
+ try:
33
+ calls = self.interface.get_tool_call_objects()
34
+ if calls:
35
+ return calls
36
+ except Exception:
37
+ pass
38
+
39
+ # fallback to single-call api
40
+ try:
41
+ single = self.interface.get_tool_call_object()
42
+ return [single] if single else []
43
+ except Exception:
44
+ return []
45
+
28
46
  async def invoke_llm(
29
47
  self,
30
48
  request_data: dict,
@@ -102,12 +120,10 @@ class SimpleLLMStreamAdapter(LettaLLMStreamAdapter):
102
120
  # After streaming completes, extract the accumulated data
103
121
  self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns()
104
122
 
105
- # Extract tool call from the interface
106
- try:
107
- self.tool_call = self.interface.get_tool_call_object()
108
- except ValueError as e:
109
- # No tool call, handle upstream
110
- self.tool_call = None
123
+ # extract tool calls from interface (supports both single and parallel calls)
124
+ self.tool_calls = self._extract_tool_calls()
125
+ # preserve legacy single-call field for existing consumers
126
+ self.tool_call = self.tool_calls[-1] if self.tool_calls else None
111
127
 
112
128
  # Extract reasoning content from the interface
113
129
  # TODO this should probably just be called "content"?
@@ -4,6 +4,7 @@ from letta.agents.base_agent_v2 import BaseAgentV2
4
4
  from letta.agents.letta_agent_v2 import LettaAgentV2
5
5
  from letta.agents.letta_agent_v3 import LettaAgentV3
6
6
  from letta.groups.sleeptime_multi_agent_v3 import SleeptimeMultiAgentV3
7
+ from letta.groups.sleeptime_multi_agent_v4 import SleeptimeMultiAgentV4
7
8
  from letta.schemas.agent import AgentState
8
9
  from letta.schemas.enums import AgentType
9
10
 
@@ -16,13 +17,19 @@ class AgentLoop:
16
17
 
17
18
  @staticmethod
18
19
  def load(agent_state: AgentState, actor: "User") -> BaseAgentV2:
19
- if agent_state.enable_sleeptime and agent_state.agent_type != AgentType.voice_convo_agent:
20
- return SleeptimeMultiAgentV3(agent_state=agent_state, actor=actor, group=agent_state.multi_agent_group)
21
- elif agent_state.agent_type == AgentType.letta_v1_agent:
20
+ if agent_state.agent_type == AgentType.letta_v1_agent:
21
+ if agent_state.enable_sleeptime:
22
+ return SleeptimeMultiAgentV4(
23
+ agent_state=agent_state,
24
+ actor=actor,
25
+ group=agent_state.multi_agent_group,
26
+ )
22
27
  return LettaAgentV3(
23
28
  agent_state=agent_state,
24
29
  actor=actor,
25
30
  )
31
+ elif agent_state.enable_sleeptime and agent_state.agent_type != AgentType.voice_convo_agent:
32
+ return SleeptimeMultiAgentV3(agent_state=agent_state, actor=actor, group=agent_state.multi_agent_group)
26
33
  else:
27
34
  return LettaAgentV2(
28
35
  agent_state=agent_state,
@@ -140,7 +140,10 @@ class BaseAgent(ABC):
140
140
 
141
141
  # generate just the memory string with current state for comparison
142
142
  curr_memory_str = agent_state.memory.compile(
143
- tool_usage_rules=tool_constraint_block, sources=agent_state.sources, max_files_open=agent_state.max_files_open
143
+ tool_usage_rules=tool_constraint_block,
144
+ sources=agent_state.sources,
145
+ max_files_open=agent_state.max_files_open,
146
+ llm_config=agent_state.llm_config,
144
147
  )
145
148
  new_dynamic_section = extract_dynamic_section(curr_memory_str)
146
149
 
letta/agents/helpers.py CHANGED
@@ -13,7 +13,7 @@ from letta.schemas.letta_message import MessageType
13
13
  from letta.schemas.letta_message_content import TextContent
14
14
  from letta.schemas.letta_response import LettaResponse
15
15
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
16
- from letta.schemas.message import Message, MessageCreate, MessageCreateBase
16
+ from letta.schemas.message import ApprovalCreate, Message, MessageCreate, MessageCreateBase
17
17
  from letta.schemas.tool_execution_result import ToolExecutionResult
18
18
  from letta.schemas.usage import LettaUsageStatistics
19
19
  from letta.schemas.user import User
@@ -135,6 +135,24 @@ async def _prepare_in_context_messages_async(
135
135
  return current_in_context_messages, new_in_context_messages
136
136
 
137
137
 
138
+ def validate_approval_tool_call_ids(approval_request_message: Message, approval_response_message: ApprovalCreate):
139
+ approval_requests = approval_request_message.tool_calls
140
+ approval_request_tool_call_ids = [approval_request.id for approval_request in approval_requests]
141
+
142
+ approval_responses = approval_response_message.approvals
143
+ approval_response_tool_call_ids = [approval_response.tool_call_id for approval_response in approval_responses]
144
+
145
+ request_response_diff = set(approval_request_tool_call_ids).symmetric_difference(set(approval_response_tool_call_ids))
146
+ if request_response_diff:
147
+ if len(approval_request_tool_call_ids) == 1 and approval_response_tool_call_ids[0] == approval_request_message.id:
148
+ # legacy case where we used to use message id instead of tool call id
149
+ return
150
+
151
+ raise ValueError(
152
+ f"Invalid tool call IDs. Expected '{approval_request_tool_call_ids}', but received '{approval_response_tool_call_ids}'."
153
+ )
154
+
155
+
138
156
  async def _prepare_in_context_messages_no_persist_async(
139
157
  input_messages: List[MessageCreateBase],
140
158
  agent_state: AgentState,
@@ -168,20 +186,18 @@ async def _prepare_in_context_messages_no_persist_async(
168
186
  # Check for approval-related message validation
169
187
  if len(input_messages) == 1 and input_messages[0].type == "approval":
170
188
  # User is trying to send an approval response
171
- if current_in_context_messages[-1].role != "approval":
189
+ if current_in_context_messages and current_in_context_messages[-1].role != "approval":
172
190
  raise ValueError(
173
191
  "Cannot process approval response: No tool call is currently awaiting approval. "
174
192
  "Please send a regular message to interact with the agent."
175
193
  )
176
- if input_messages[0].approval_request_id != current_in_context_messages[-1].id:
177
- raise ValueError(
178
- f"Invalid approval request ID. Expected '{current_in_context_messages[-1].id}' "
179
- f"but received '{input_messages[0].approval_request_id}'."
180
- )
181
- new_in_context_messages = create_approval_response_message_from_input(agent_state=agent_state, input_message=input_messages[0])
194
+ validate_approval_tool_call_ids(current_in_context_messages[-1], input_messages[0])
195
+ new_in_context_messages = create_approval_response_message_from_input(
196
+ agent_state=agent_state, input_message=input_messages[0], run_id=run_id
197
+ )
182
198
  else:
183
199
  # User is trying to send a regular message
184
- if current_in_context_messages[-1].role == "approval":
200
+ if current_in_context_messages and current_in_context_messages[-1].role == "approval":
185
201
  raise PendingApprovalError(pending_request_id=current_in_context_messages[-1].id)
186
202
 
187
203
  # Create a new user message from the input but dont store it yet
@@ -400,3 +416,19 @@ def _maybe_get_approval_messages(messages: list[Message]) -> Tuple[Message | Non
400
416
  if maybe_approval_request.role == "approval" and maybe_approval_response.role == "approval":
401
417
  return maybe_approval_request, maybe_approval_response
402
418
  return None, None
419
+
420
+
421
+ def _maybe_get_pending_tool_call_message(messages: list[Message]) -> Message | None:
422
+ """
423
+ Only used in the case where hitl is invoked with parallel tool calling,
424
+ where agent calls some tools that require approval, and others that don't.
425
+ """
426
+ if len(messages) >= 3:
427
+ maybe_tool_call_message = messages[-3]
428
+ if (
429
+ maybe_tool_call_message.role == "assistant"
430
+ and maybe_tool_call_message.tool_calls is not None
431
+ and len(maybe_tool_call_message.tool_calls) > 0
432
+ ):
433
+ return maybe_tool_call_message
434
+ return None
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import uuid
2
3
  from collections.abc import AsyncGenerator
3
4
  from datetime import datetime
@@ -18,7 +19,7 @@ from letta.agents.helpers import (
18
19
  _safe_load_tool_call_str,
19
20
  generate_step_id,
20
21
  )
21
- from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX
22
+ from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM
22
23
  from letta.errors import ContextWindowExceededError
23
24
  from letta.helpers import ToolRulesSolver
24
25
  from letta.helpers.datetime_helpers import AsyncTimer, get_utc_time, get_utc_timestamp_ns, ns_to_ms
@@ -41,7 +42,7 @@ from letta.schemas.letta_response import LettaResponse
41
42
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
42
43
  from letta.schemas.llm_config import LLMConfig
43
44
  from letta.schemas.message import Message, MessageCreateBase
44
- from letta.schemas.openai.chat_completion_response import ToolCall, UsageStatistics
45
+ from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall, UsageStatistics
45
46
  from letta.schemas.provider_trace import ProviderTraceCreate
46
47
  from letta.schemas.step import StepProgression
47
48
  from letta.schemas.step_metrics import StepMetrics
@@ -1698,19 +1699,18 @@ class LettaAgent(BaseAgent):
1698
1699
  request_heartbeat=request_heartbeat,
1699
1700
  )
1700
1701
  if not is_approval and tool_rules_solver.is_requires_approval_tool(tool_call_name):
1701
- approval_message = create_approval_request_message_from_llm_response(
1702
+ tool_args[REQUEST_HEARTBEAT_PARAM] = request_heartbeat
1703
+ approval_messages = create_approval_request_message_from_llm_response(
1702
1704
  agent_id=agent_state.id,
1703
1705
  model=agent_state.llm_config.model,
1704
- function_name=tool_call_name,
1705
- function_arguments=tool_args,
1706
- tool_call_id=tool_call_id,
1707
- actor=self.actor,
1708
- continue_stepping=request_heartbeat,
1706
+ requested_tool_calls=[
1707
+ ToolCall(id=tool_call_id, function=FunctionCall(name=tool_call_name, arguments=json.dumps(tool_args)))
1708
+ ],
1709
1709
  reasoning_content=reasoning_content,
1710
1710
  pre_computed_assistant_message_id=pre_computed_assistant_message_id,
1711
1711
  step_id=step_id,
1712
1712
  )
1713
- messages_to_persist = (initial_messages or []) + [approval_message]
1713
+ messages_to_persist = (initial_messages or []) + approval_messages
1714
1714
  continue_stepping = False
1715
1715
  stop_reason = LettaStopReason(stop_reason=StopReasonType.requires_approval.value)
1716
1716
  else:
@@ -1868,7 +1868,8 @@ class LettaAgent(BaseAgent):
1868
1868
  start_time = get_utc_timestamp_ns()
1869
1869
  agent_step_span.add_event(name="tool_execution_started")
1870
1870
 
1871
- sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
1871
+ # Decrypt environment variable values
1872
+ sandbox_env_vars = {var.key: var.get_value_secret().get_plaintext() for var in agent_state.secrets}
1872
1873
  tool_execution_manager = ToolExecutionManager(
1873
1874
  agent_state=agent_state,
1874
1875
  message_manager=self.message_manager,
@@ -1,4 +1,4 @@
1
- import asyncio
1
+ import json
2
2
  import uuid
3
3
  from datetime import datetime
4
4
  from typing import AsyncGenerator, Optional, Tuple
@@ -19,7 +19,7 @@ from letta.agents.helpers import (
19
19
  _safe_load_tool_call_str,
20
20
  generate_step_id,
21
21
  )
22
- from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX
22
+ from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM
23
23
  from letta.errors import ContextWindowExceededError, LLMError
24
24
  from letta.helpers import ToolRulesSolver
25
25
  from letta.helpers.datetime_helpers import get_utc_time, get_utc_timestamp_ns, ns_to_ms
@@ -37,9 +37,10 @@ from letta.schemas.letta_message_content import OmittedReasoningContent, Reasoni
37
37
  from letta.schemas.letta_response import LettaResponse
38
38
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
39
39
  from letta.schemas.message import Message, MessageCreate, MessageUpdate
40
- from letta.schemas.openai.chat_completion_response import ToolCall, UsageStatistics
40
+ from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall, UsageStatistics
41
41
  from letta.schemas.step import Step, StepProgression
42
42
  from letta.schemas.step_metrics import StepMetrics
43
+ from letta.schemas.tool import Tool
43
44
  from letta.schemas.tool_execution_result import ToolExecutionResult
44
45
  from letta.schemas.usage import LettaUsageStatistics
45
46
  from letta.schemas.user import User
@@ -470,7 +471,7 @@ class LettaAgentV2(BaseAgentV2):
470
471
  # Handle the AI response with the extracted data
471
472
  if tool_call is None and llm_adapter.tool_call is None:
472
473
  self.stop_reason = LettaStopReason(stop_reason=StopReasonType.no_tool_call.value)
473
- raise ValueError("No tool calls found in response, model must make a tool call")
474
+ raise LLMError("No tool calls found in response, model must make a tool call")
474
475
 
475
476
  # TODO: how should be associate input messages with runs?
476
477
  ## Set run_id on input messages before persisting
@@ -535,7 +536,7 @@ class LettaAgentV2(BaseAgentV2):
535
536
  )
536
537
  step_progression, step_metrics = await self._step_checkpoint_finish(step_metrics, agent_step_span, logged_step)
537
538
  except Exception as e:
538
- self.logger.error(f"Error during step processing: {e}")
539
+ self.logger.warning(f"Error during step processing: {e}")
539
540
  self.job_update_metadata = {"error": str(e)}
540
541
 
541
542
  # This indicates we failed after we decided to stop stepping, which indicates a bug with our flow.
@@ -699,7 +700,10 @@ class LettaAgentV2(BaseAgentV2):
699
700
 
700
701
  # generate just the memory string with current state for comparison
701
702
  curr_memory_str = agent_state.memory.compile(
702
- tool_usage_rules=tool_constraint_block, sources=agent_state.sources, max_files_open=agent_state.max_files_open
703
+ tool_usage_rules=tool_constraint_block,
704
+ sources=agent_state.sources,
705
+ max_files_open=agent_state.max_files_open,
706
+ llm_config=agent_state.llm_config,
703
707
  )
704
708
  new_dynamic_section = extract_dynamic_section(curr_memory_str)
705
709
 
@@ -933,20 +937,19 @@ class LettaAgentV2(BaseAgentV2):
933
937
  )
934
938
 
935
939
  if not is_approval and tool_rules_solver.is_requires_approval_tool(tool_call_name):
936
- approval_message = create_approval_request_message_from_llm_response(
940
+ tool_args[REQUEST_HEARTBEAT_PARAM] = request_heartbeat
941
+ approval_messages = create_approval_request_message_from_llm_response(
937
942
  agent_id=agent_state.id,
938
943
  model=agent_state.llm_config.model,
939
- function_name=tool_call_name,
940
- function_arguments=tool_args,
941
- tool_call_id=tool_call_id,
942
- actor=self.actor,
943
- continue_stepping=request_heartbeat,
944
+ requested_tool_calls=[
945
+ ToolCall(id=tool_call_id, function=FunctionCall(name=tool_call_name, arguments=json.dumps(tool_args)))
946
+ ],
944
947
  reasoning_content=reasoning_content,
945
948
  pre_computed_assistant_message_id=pre_computed_assistant_message_id,
946
949
  step_id=step_id,
947
950
  run_id=run_id,
948
951
  )
949
- messages_to_persist = (initial_messages or []) + [approval_message]
952
+ messages_to_persist = (initial_messages or []) + approval_messages
950
953
  continue_stepping = False
951
954
  stop_reason = LettaStopReason(stop_reason=StopReasonType.requires_approval.value)
952
955
  else:
@@ -957,8 +960,10 @@ class LettaAgentV2(BaseAgentV2):
957
960
  else:
958
961
  # Track tool execution time
959
962
  tool_start_time = get_utc_timestamp_ns()
963
+ target_tool = next((x for x in agent_state.tools if x.name == tool_call_name), None)
964
+
960
965
  tool_execution_result = await self._execute_tool(
961
- tool_name=tool_call_name,
966
+ target_tool=target_tool,
962
967
  tool_args=tool_args,
963
968
  agent_state=agent_state,
964
969
  agent_step_span=agent_step_span,
@@ -1079,20 +1084,20 @@ class LettaAgentV2(BaseAgentV2):
1079
1084
  @trace_method
1080
1085
  async def _execute_tool(
1081
1086
  self,
1082
- tool_name: str,
1087
+ target_tool: Tool,
1083
1088
  tool_args: JsonDict,
1084
1089
  agent_state: AgentState,
1085
1090
  agent_step_span: Span | None = None,
1086
1091
  step_id: str | None = None,
1087
- run_id: str = None,
1088
1092
  ) -> "ToolExecutionResult":
1089
1093
  """
1090
1094
  Executes a tool and returns the ToolExecutionResult.
1091
1095
  """
1092
1096
  from letta.schemas.tool_execution_result import ToolExecutionResult
1093
1097
 
1098
+ tool_name = target_tool.name
1099
+
1094
1100
  # Special memory case
1095
- target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
1096
1101
  if not target_tool:
1097
1102
  # TODO: fix this error message
1098
1103
  return ToolExecutionResult(
@@ -1106,7 +1111,8 @@ class LettaAgentV2(BaseAgentV2):
1106
1111
  start_time = get_utc_timestamp_ns()
1107
1112
  agent_step_span.add_event(name="tool_execution_started")
1108
1113
 
1109
- sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
1114
+ # Decrypt environment variable values
1115
+ sandbox_env_vars = {var.key: var.get_value_secret().get_plaintext() for var in agent_state.secrets}
1110
1116
  tool_execution_manager = ToolExecutionManager(
1111
1117
  agent_state=agent_state,
1112
1118
  message_manager=self.message_manager,
@@ -1158,25 +1164,29 @@ class LettaAgentV2(BaseAgentV2):
1158
1164
  # TODO: This can be broken by bad configs, e.g. lower bound too high, initial messages too fat, etc.
1159
1165
  # TODO: `force` and `clear` seem to no longer be used, we should remove
1160
1166
  if not skip_summarization:
1161
- if force or (total_tokens and total_tokens > self.agent_state.llm_config.context_window):
1162
- self.logger.warning(
1163
- f"Total tokens {total_tokens} exceeds configured max tokens {self.agent_state.llm_config.context_window}, forcefully clearing message history."
1164
- )
1165
- new_in_context_messages, updated = await self.summarizer.summarize(
1166
- in_context_messages=in_context_messages,
1167
- new_letta_messages=new_letta_messages,
1168
- force=True,
1169
- clear=True,
1170
- )
1171
- else:
1172
- # NOTE (Sarah): Seems like this is doing nothing?
1173
- self.logger.info(
1174
- f"Total tokens {total_tokens} does not exceed configured max tokens {self.agent_state.llm_config.context_window}, passing summarizing w/o force."
1175
- )
1176
- new_in_context_messages, updated = await self.summarizer.summarize(
1177
- in_context_messages=in_context_messages,
1178
- new_letta_messages=new_letta_messages,
1179
- )
1167
+ try:
1168
+ if force or (total_tokens and total_tokens > self.agent_state.llm_config.context_window):
1169
+ self.logger.warning(
1170
+ f"Total tokens {total_tokens} exceeds configured max tokens {self.agent_state.llm_config.context_window}, forcefully clearing message history."
1171
+ )
1172
+ new_in_context_messages, updated = await self.summarizer.summarize(
1173
+ in_context_messages=in_context_messages,
1174
+ new_letta_messages=new_letta_messages,
1175
+ force=True,
1176
+ clear=True,
1177
+ )
1178
+ else:
1179
+ # NOTE (Sarah): Seems like this is doing nothing?
1180
+ self.logger.info(
1181
+ f"Total tokens {total_tokens} does not exceed configured max tokens {self.agent_state.llm_config.context_window}, passing summarizing w/o force."
1182
+ )
1183
+ new_in_context_messages, updated = await self.summarizer.summarize(
1184
+ in_context_messages=in_context_messages,
1185
+ new_letta_messages=new_letta_messages,
1186
+ )
1187
+ except Exception as e:
1188
+ self.logger.error(f"Failed to summarize conversation history: {e}")
1189
+ new_in_context_messages = in_context_messages + new_letta_messages
1180
1190
  else:
1181
1191
  new_in_context_messages = in_context_messages + new_letta_messages
1182
1192