letta-nightly 0.8.5.dev20250625104328__py3-none-any.whl → 0.8.6.dev20250625222533__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. letta/agent.py +16 -12
  2. letta/agents/base_agent.py +4 -1
  3. letta/agents/helpers.py +35 -3
  4. letta/agents/letta_agent.py +132 -106
  5. letta/agents/letta_agent_batch.py +4 -3
  6. letta/agents/voice_agent.py +12 -2
  7. letta/agents/voice_sleeptime_agent.py +12 -2
  8. letta/constants.py +24 -3
  9. letta/data_sources/redis_client.py +6 -0
  10. letta/errors.py +5 -0
  11. letta/functions/function_sets/files.py +10 -3
  12. letta/functions/function_sets/multi_agent.py +0 -32
  13. letta/groups/sleeptime_multi_agent_v2.py +6 -0
  14. letta/helpers/converters.py +4 -1
  15. letta/helpers/datetime_helpers.py +16 -23
  16. letta/helpers/message_helper.py +5 -2
  17. letta/helpers/tool_rule_solver.py +29 -2
  18. letta/interfaces/openai_streaming_interface.py +9 -2
  19. letta/llm_api/anthropic.py +11 -1
  20. letta/llm_api/anthropic_client.py +14 -3
  21. letta/llm_api/aws_bedrock.py +29 -15
  22. letta/llm_api/bedrock_client.py +74 -0
  23. letta/llm_api/google_ai_client.py +7 -3
  24. letta/llm_api/google_vertex_client.py +18 -4
  25. letta/llm_api/llm_client.py +7 -0
  26. letta/llm_api/openai_client.py +13 -0
  27. letta/orm/agent.py +5 -0
  28. letta/orm/block_history.py +1 -1
  29. letta/orm/enums.py +6 -25
  30. letta/orm/job.py +1 -2
  31. letta/orm/llm_batch_items.py +1 -1
  32. letta/orm/mcp_server.py +1 -1
  33. letta/orm/passage.py +7 -1
  34. letta/orm/sqlalchemy_base.py +7 -5
  35. letta/orm/tool.py +2 -1
  36. letta/schemas/agent.py +34 -10
  37. letta/schemas/enums.py +42 -1
  38. letta/schemas/job.py +6 -3
  39. letta/schemas/letta_request.py +4 -0
  40. letta/schemas/llm_batch_job.py +7 -2
  41. letta/schemas/memory.py +2 -2
  42. letta/schemas/providers.py +32 -6
  43. letta/schemas/run.py +1 -1
  44. letta/schemas/tool_rule.py +40 -12
  45. letta/serialize_schemas/pydantic_agent_schema.py +9 -2
  46. letta/server/rest_api/app.py +3 -2
  47. letta/server/rest_api/routers/v1/agents.py +25 -22
  48. letta/server/rest_api/routers/v1/runs.py +2 -3
  49. letta/server/rest_api/routers/v1/sources.py +31 -0
  50. letta/server/rest_api/routers/v1/voice.py +1 -0
  51. letta/server/rest_api/utils.py +38 -13
  52. letta/server/server.py +52 -21
  53. letta/services/agent_manager.py +58 -7
  54. letta/services/block_manager.py +1 -1
  55. letta/services/file_processor/chunker/line_chunker.py +2 -1
  56. letta/services/file_processor/file_processor.py +2 -9
  57. letta/services/files_agents_manager.py +177 -37
  58. letta/services/helpers/agent_manager_helper.py +77 -48
  59. letta/services/helpers/tool_parser_helper.py +2 -1
  60. letta/services/job_manager.py +33 -2
  61. letta/services/llm_batch_manager.py +1 -1
  62. letta/services/provider_manager.py +6 -4
  63. letta/services/tool_executor/core_tool_executor.py +1 -1
  64. letta/services/tool_executor/files_tool_executor.py +99 -30
  65. letta/services/tool_executor/multi_agent_tool_executor.py +1 -17
  66. letta/services/tool_executor/tool_execution_manager.py +6 -0
  67. letta/services/tool_executor/tool_executor_base.py +3 -0
  68. letta/services/tool_sandbox/base.py +39 -1
  69. letta/services/tool_sandbox/e2b_sandbox.py +7 -0
  70. letta/services/user_manager.py +3 -2
  71. letta/settings.py +8 -14
  72. letta/system.py +17 -17
  73. letta/templates/sandbox_code_file_async.py.j2 +59 -0
  74. {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/METADATA +3 -2
  75. {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/RECORD +78 -76
  76. {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/LICENSE +0 -0
  77. {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/WHEEL +0 -0
  78. {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/entry_points.txt +0 -0
@@ -18,14 +18,14 @@ from letta.local_llm.constants import INNER_THOUGHTS_KWARG
18
18
  from letta.log import get_logger
19
19
  from letta.orm.enums import ToolType
20
20
  from letta.otel.tracing import log_event, trace_method
21
- from letta.schemas.agent import AgentState, AgentStepState
21
+ from letta.schemas.agent import AgentState
22
22
  from letta.schemas.enums import AgentStepStatus, JobStatus, MessageStreamStatus, ProviderType
23
23
  from letta.schemas.job import JobUpdate
24
24
  from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
25
25
  from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
26
26
  from letta.schemas.letta_request import LettaBatchRequest
27
27
  from letta.schemas.letta_response import LettaBatchResponse, LettaResponse
28
- from letta.schemas.llm_batch_job import LLMBatchItem
28
+ from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem
29
29
  from letta.schemas.message import Message, MessageCreate
30
30
  from letta.schemas.openai.chat_completion_response import ToolCall as OpenAIToolCall
31
31
  from letta.schemas.sandbox_config import SandboxConfig, SandboxType
@@ -548,8 +548,9 @@ class LettaAgentBatch(BaseAgent):
548
548
  function_call_success=success_flag,
549
549
  function_response=tool_exec_result,
550
550
  tool_execution_result=tool_exec_result_obj,
551
+ timezone=agent_state.timezone,
551
552
  actor=self.actor,
552
- add_heartbeat_request_system_message=False,
553
+ continue_stepping=False,
553
554
  reasoning_content=reasoning_content,
554
555
  pre_computed_assistant_message_id=None,
555
556
  llm_batch_item_id=llm_batch_item_id,
@@ -38,6 +38,7 @@ from letta.server.rest_api.utils import (
38
38
  from letta.services.agent_manager import AgentManager
39
39
  from letta.services.block_manager import BlockManager
40
40
  from letta.services.helpers.agent_manager_helper import compile_system_message
41
+ from letta.services.job_manager import JobManager
41
42
  from letta.services.message_manager import MessageManager
42
43
  from letta.services.passage_manager import PassageManager
43
44
  from letta.services.summarizer.enums import SummarizationMode
@@ -64,6 +65,7 @@ class VoiceAgent(BaseAgent):
64
65
  message_manager: MessageManager,
65
66
  agent_manager: AgentManager,
66
67
  block_manager: BlockManager,
68
+ job_manager: JobManager,
67
69
  passage_manager: PassageManager,
68
70
  actor: User,
69
71
  ):
@@ -73,6 +75,7 @@ class VoiceAgent(BaseAgent):
73
75
 
74
76
  # Summarizer settings
75
77
  self.block_manager = block_manager
78
+ self.job_manager = job_manager
76
79
  self.passage_manager = passage_manager
77
80
  # TODO: This is not guaranteed to exist!
78
81
  self.summary_block_label = "human"
@@ -98,6 +101,7 @@ class VoiceAgent(BaseAgent):
98
101
  agent_manager=self.agent_manager,
99
102
  actor=self.actor,
100
103
  block_manager=self.block_manager,
104
+ job_manager=self.job_manager,
101
105
  passage_manager=self.passage_manager,
102
106
  target_block_label=self.summary_block_label,
103
107
  ),
@@ -146,10 +150,13 @@ class VoiceAgent(BaseAgent):
146
150
  system_prompt=agent_state.system,
147
151
  in_context_memory=agent_state.memory,
148
152
  in_context_memory_last_edit=memory_edit_timestamp,
153
+ timezone=agent_state.timezone,
149
154
  previous_message_count=self.num_messages,
150
155
  archival_memory_size=self.num_archival_memories,
151
156
  )
152
- letta_message_db_queue = create_input_messages(input_messages=input_messages, agent_id=agent_state.id, actor=self.actor)
157
+ letta_message_db_queue = create_input_messages(
158
+ input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, actor=self.actor
159
+ )
153
160
  in_memory_message_history = self.pre_process_input_message(input_messages)
154
161
 
155
162
  # TODO: Define max steps here
@@ -208,6 +215,7 @@ class VoiceAgent(BaseAgent):
208
215
  agent_id=agent_state.id,
209
216
  model=agent_state.llm_config.model,
210
217
  actor=self.actor,
218
+ timezone=agent_state.timezone,
211
219
  )
212
220
  letta_message_db_queue.extend(assistant_msgs)
213
221
 
@@ -268,8 +276,9 @@ class VoiceAgent(BaseAgent):
268
276
  function_call_success=success_flag,
269
277
  function_response=tool_result,
270
278
  tool_execution_result=tool_execution_result,
279
+ timezone=agent_state.timezone,
271
280
  actor=self.actor,
272
- add_heartbeat_request_system_message=True,
281
+ continue_stepping=True,
273
282
  )
274
283
  letta_message_db_queue.extend(tool_call_messages)
275
284
 
@@ -439,6 +448,7 @@ class VoiceAgent(BaseAgent):
439
448
  message_manager=self.message_manager,
440
449
  agent_manager=self.agent_manager,
441
450
  block_manager=self.block_manager,
451
+ job_manager=self.job_manager,
442
452
  passage_manager=self.passage_manager,
443
453
  sandbox_env_vars=sandbox_env_vars,
444
454
  actor=self.actor,
@@ -15,6 +15,7 @@ from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRul
15
15
  from letta.schemas.user import User
16
16
  from letta.services.agent_manager import AgentManager
17
17
  from letta.services.block_manager import BlockManager
18
+ from letta.services.job_manager import JobManager
18
19
  from letta.services.message_manager import MessageManager
19
20
  from letta.services.passage_manager import PassageManager
20
21
  from letta.services.summarizer.enums import SummarizationMode
@@ -34,6 +35,7 @@ class VoiceSleeptimeAgent(LettaAgent):
34
35
  message_manager: MessageManager,
35
36
  agent_manager: AgentManager,
36
37
  block_manager: BlockManager,
38
+ job_manager: JobManager,
37
39
  passage_manager: PassageManager,
38
40
  target_block_label: str,
39
41
  actor: User,
@@ -43,6 +45,7 @@ class VoiceSleeptimeAgent(LettaAgent):
43
45
  message_manager=message_manager,
44
46
  agent_manager=agent_manager,
45
47
  block_manager=block_manager,
48
+ job_manager=job_manager,
46
49
  passage_manager=passage_manager,
47
50
  actor=actor,
48
51
  )
@@ -64,7 +67,9 @@ class VoiceSleeptimeAgent(LettaAgent):
64
67
  self,
65
68
  input_messages: List[MessageCreate],
66
69
  max_steps: int = DEFAULT_MAX_STEPS,
70
+ run_id: Optional[str] = None,
67
71
  use_assistant_message: bool = True,
72
+ request_start_timestamp_ns: Optional[int] = None,
68
73
  include_return_message_types: Optional[List[MessageType]] = None,
69
74
  ) -> LettaResponse:
70
75
  """
@@ -82,7 +87,7 @@ class VoiceSleeptimeAgent(LettaAgent):
82
87
  ]
83
88
 
84
89
  # Summarize
85
- current_in_context_messages, new_in_context_messages, usage, stop_reason = await super()._step(
90
+ current_in_context_messages, new_in_context_messages, stop_reason, usage = await super()._step(
86
91
  agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
87
92
  )
88
93
  new_in_context_messages, updated = self.summarizer.summarize(
@@ -172,7 +177,12 @@ class VoiceSleeptimeAgent(LettaAgent):
172
177
  return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
173
178
 
174
179
  async def step_stream(
175
- self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS, use_assistant_message: bool = True
180
+ self,
181
+ input_messages: List[MessageCreate],
182
+ max_steps: int = DEFAULT_MAX_STEPS,
183
+ use_assistant_message: bool = True,
184
+ request_start_timestamp_ns: Optional[int] = None,
185
+ include_return_message_types: Optional[List[MessageType]] = None,
176
186
  ) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]:
177
187
  """
178
188
  This agent is synchronous-only. If called in an async context, raise an error.
letta/constants.py CHANGED
@@ -6,6 +6,7 @@ LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
6
6
  LETTA_TOOL_EXECUTION_DIR = os.path.join(LETTA_DIR, "tool_execution_dir")
7
7
 
8
8
  LETTA_MODEL_ENDPOINT = "https://inference.letta.com"
9
+ DEFAULT_TIMEZONE = "UTC"
9
10
 
10
11
  ADMIN_PREFIX = "/v1/admin"
11
12
  API_PREFIX = "/v1"
@@ -113,7 +114,7 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
113
114
  "finish_rethinking_memory",
114
115
  ]
115
116
  # Multi agent tools
116
- MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
117
+ MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags"]
117
118
 
118
119
  # Used to catch if line numbers are pushed in
119
120
  # MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(r"^Line \d+: ", re.MULTILINE)
@@ -130,6 +131,11 @@ BUILTIN_TOOLS = ["run_code", "web_search"]
130
131
  # Built in tools
131
132
  FILES_TOOLS = ["open_file", "close_file", "grep", "search_files"]
132
133
 
134
+ FILE_MEMORY_EXISTS_MESSAGE = "The following files are currently accessible in memory:"
135
+ FILE_MEMORY_EMPTY_MESSAGE = (
136
+ "There are no files currently available in memory. Files will appear here once they are uploaded directly to your system."
137
+ )
138
+
133
139
  # Set of all built-in Letta tools
134
140
  LETTA_TOOL_SET = set(
135
141
  BASE_TOOLS
@@ -192,10 +198,21 @@ CORE_MEMORY_LINE_NUMBER_WARNING = (
192
198
  # Constants to do with summarization / conversation length window
193
199
  # The max amount of tokens supported by the underlying model (eg 8k for gpt-4 and Mistral 7B)
194
200
  LLM_MAX_TOKENS = {
195
- "DEFAULT": 8192,
201
+ "DEFAULT": 30000,
202
+ # deepseek
196
203
  "deepseek-chat": 64000,
197
204
  "deepseek-reasoner": 64000,
198
205
  ## OpenAI models: https://platform.openai.com/docs/models/overview
206
+ # reasoners
207
+ "o1": 200000,
208
+ # "o1-pro": 200000, # responses API only
209
+ "o1-2024-12-17": 200000,
210
+ "o3": 200000,
211
+ "o3-2025-04-16": 200000,
212
+ "o3-mini": 200000,
213
+ "o3-mini-2025-01-31": 200000,
214
+ # "o3-pro": 200000, # responses API only
215
+ # "o3-pro-2025-06-10": 200000,
199
216
  "gpt-4.1": 1047576,
200
217
  "gpt-4.1-2025-04-14": 1047576,
201
218
  "gpt-4.1-mini": 1047576,
@@ -209,6 +226,7 @@ LLM_MAX_TOKENS = {
209
226
  "chatgpt-4o-latest": 128000,
210
227
  # "o1-preview-2024-09-12
211
228
  "gpt-4o-2024-08-06": 128000,
229
+ "gpt-4o-2024-11-20": 128000,
212
230
  "gpt-4-turbo-preview": 128000,
213
231
  "gpt-4o": 128000,
214
232
  "gpt-3.5-turbo-instruct": 16385,
@@ -218,7 +236,7 @@ LLM_MAX_TOKENS = {
218
236
  # "davinci-002": 128000,
219
237
  "gpt-4-turbo-2024-04-09": 128000,
220
238
  # "gpt-4o-realtime-preview-2024-10-01
221
- "gpt-4-turbo": 8192,
239
+ "gpt-4-turbo": 128000,
222
240
  "gpt-4o-2024-05-13": 128000,
223
241
  # "o1-mini
224
242
  # "o1-mini-2024-09-12
@@ -338,3 +356,6 @@ REDIS_INCLUDE = "include"
338
356
  REDIS_EXCLUDE = "exclude"
339
357
  REDIS_SET_DEFAULT_VAL = "None"
340
358
  REDIS_DEFAULT_CACHE_PREFIX = "letta_cache"
359
+
360
+ # TODO: This is temporary, eventually use token-based eviction
361
+ MAX_FILES_OPEN = 5
@@ -283,6 +283,12 @@ class NoopAsyncRedisClient(AsyncRedisClient):
283
283
  async def scard(self, key: str) -> int:
284
284
  return 0
285
285
 
286
+ async def smembers(self, key: str) -> Set[str]:
287
+ return set()
288
+
289
+ async def srem(self, key: str, *members: Union[str, int, float]) -> int:
290
+ return 0
291
+
286
292
 
287
293
  async def get_redis_client() -> AsyncRedisClient:
288
294
  global _client_instance
letta/errors.py CHANGED
@@ -17,6 +17,7 @@ class ErrorCode(Enum):
17
17
  INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
18
18
  CONTEXT_WINDOW_EXCEEDED = "CONTEXT_WINDOW_EXCEEDED"
19
19
  RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
20
+ TIMEOUT = "TIMEOUT"
20
21
 
21
22
 
22
23
  class LettaError(Exception):
@@ -101,6 +102,10 @@ class LLMServerError(LLMError):
101
102
  while processing the request."""
102
103
 
103
104
 
105
+ class LLMTimeoutError(LLMError):
106
+ """Error when LLM request times out"""
107
+
108
+
104
109
  class BedrockPermissionError(LettaError):
105
110
  """Exception raised for errors in the Bedrock permission process."""
106
111
 
@@ -32,16 +32,23 @@ async def close_file(agent_state: "AgentState", file_name: str) -> str:
32
32
  raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
33
33
 
34
34
 
35
- async def grep(agent_state: "AgentState", pattern: str, include: Optional[str] = None) -> str:
35
+ async def grep(
36
+ agent_state: "AgentState",
37
+ pattern: str,
38
+ include: Optional[str] = None,
39
+ context_lines: Optional[int] = 3,
40
+ ) -> str:
36
41
  """
37
- Grep tool to search files across data sources with a keyword or regex pattern.
42
+ Grep tool to search files across data sources using a keyword or regex pattern.
38
43
 
39
44
  Args:
40
45
  pattern (str): Keyword or regex pattern to search within file contents.
41
46
  include (Optional[str]): Optional keyword or regex pattern to filter filenames to include in the search.
47
+ context_lines (Optional[int]): Number of lines of context to show before and after each match.
48
+ Equivalent to `-C` in grep. Defaults to 3.
42
49
 
43
50
  Returns:
44
- str: Matching lines or summary output.
51
+ str: Matching lines with optional surrounding context or a summary output.
45
52
  """
46
53
  raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
47
54
 
@@ -7,7 +7,6 @@ from letta.functions.helpers import (
7
7
  _send_message_to_all_agents_in_group_async,
8
8
  execute_send_message_to_agent,
9
9
  extract_send_message_from_steps_messages,
10
- fire_and_forget_send_to_agent,
11
10
  )
12
11
  from letta.schemas.enums import MessageRole
13
12
  from letta.schemas.message import MessageCreate
@@ -44,37 +43,6 @@ def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_
44
43
  )
45
44
 
46
45
 
47
- def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str) -> str:
48
- """
49
- Sends a message to a specific Letta agent within the same organization. The sender's identity is automatically included, so no explicit introduction is required in the message. This function does not expect a response from the target agent, making it suitable for notifications or one-way communication.
50
-
51
- Args:
52
- message (str): The content of the message to be sent to the target agent.
53
- other_agent_id (str): The unique identifier of the target Letta agent.
54
-
55
- Returns:
56
- str: A confirmation message indicating the message was successfully sent.
57
- """
58
- message = (
59
- f"[Incoming message from agent with ID '{self.agent_state.id}' - to reply to this message, "
60
- f"make sure to use the 'send_message_to_agent_async' tool, or the agent will not receive your message] "
61
- f"{message}"
62
- )
63
- messages = [MessageCreate(role=MessageRole.system, content=message, name=self.agent_state.name)]
64
-
65
- # Do the actual fire-and-forget
66
- fire_and_forget_send_to_agent(
67
- sender_agent=self,
68
- messages=messages,
69
- other_agent_id=other_agent_id,
70
- log_prefix="[send_message_to_agent_async]",
71
- use_retries=False, # or True if you want to use _async_send_message_with_retries
72
- )
73
-
74
- # Immediately return to caller
75
- return "Successfully sent message"
76
-
77
-
78
46
  def send_message_to_agents_matching_tags(self: "Agent", message: str, match_all: List[str], match_some: List[str]) -> List[str]:
79
47
  """
80
48
  Sends a message to all agents within the same organization that match the specified tag criteria. Agents must possess *all* of the tags in `match_all` and *at least one* of the tags in `match_some` to receive the message.
@@ -63,6 +63,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
63
63
  self,
64
64
  input_messages: List[MessageCreate],
65
65
  max_steps: int = DEFAULT_MAX_STEPS,
66
+ run_id: Optional[str] = None,
66
67
  use_assistant_message: bool = True,
67
68
  request_start_timestamp_ns: Optional[int] = None,
68
69
  include_return_message_types: Optional[List[MessageType]] = None,
@@ -83,6 +84,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
83
84
  message_manager=self.message_manager,
84
85
  agent_manager=self.agent_manager,
85
86
  block_manager=self.block_manager,
87
+ job_manager=self.job_manager,
86
88
  passage_manager=self.passage_manager,
87
89
  actor=self.actor,
88
90
  step_manager=self.step_manager,
@@ -92,6 +94,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
92
94
  response = await foreground_agent.step(
93
95
  input_messages=new_messages,
94
96
  max_steps=max_steps,
97
+ run_id=run_id,
95
98
  use_assistant_message=use_assistant_message,
96
99
  include_return_message_types=include_return_message_types,
97
100
  )
@@ -170,6 +173,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
170
173
  message_manager=self.message_manager,
171
174
  agent_manager=self.agent_manager,
172
175
  block_manager=self.block_manager,
176
+ job_manager=self.job_manager,
173
177
  passage_manager=self.passage_manager,
174
178
  actor=self.actor,
175
179
  step_manager=self.step_manager,
@@ -283,6 +287,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
283
287
  message_manager=self.message_manager,
284
288
  agent_manager=self.agent_manager,
285
289
  block_manager=self.block_manager,
290
+ job_manager=self.job_manager,
286
291
  passage_manager=self.passage_manager,
287
292
  actor=self.actor,
288
293
  step_manager=self.step_manager,
@@ -296,6 +301,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
296
301
  result = await sleeptime_agent.step(
297
302
  input_messages=sleeptime_agent_messages,
298
303
  use_assistant_message=use_assistant_message,
304
+ run_id=run_id,
299
305
  )
300
306
 
301
307
  # Update job status
@@ -8,7 +8,6 @@ from openai.types.chat.chat_completion_message_tool_call import Function as Open
8
8
  from sqlalchemy import Dialect
9
9
 
10
10
  from letta.functions.mcp_client.types import StdioServerConfig
11
- from letta.schemas.agent import AgentStepState
12
11
  from letta.schemas.embedding_config import EmbeddingConfig
13
12
  from letta.schemas.enums import ProviderType, ToolRuleType
14
13
  from letta.schemas.letta_message_content import (
@@ -23,6 +22,7 @@ from letta.schemas.letta_message_content import (
23
22
  ToolCallContent,
24
23
  ToolReturnContent,
25
24
  )
25
+ from letta.schemas.llm_batch_job import AgentStepState
26
26
  from letta.schemas.llm_config import LLMConfig
27
27
  from letta.schemas.message import ToolReturn
28
28
  from letta.schemas.response_format import (
@@ -39,6 +39,7 @@ from letta.schemas.tool_rule import (
39
39
  InitToolRule,
40
40
  MaxCountPerStepToolRule,
41
41
  ParentToolRule,
42
+ RequiredBeforeExitToolRule,
42
43
  TerminalToolRule,
43
44
  ToolRule,
44
45
  )
@@ -131,6 +132,8 @@ def deserialize_tool_rule(
131
132
  return MaxCountPerStepToolRule(**data)
132
133
  elif rule_type == ToolRuleType.parent_last_tool:
133
134
  return ParentToolRule(**data)
135
+ elif rule_type == ToolRuleType.required_before_exit:
136
+ return RequiredBeforeExitToolRule(**data)
134
137
  raise ValueError(f"Unknown ToolRule type: {rule_type}")
135
138
 
136
139
 
@@ -2,11 +2,12 @@ import re
2
2
  import time
3
3
  from datetime import datetime, timedelta
4
4
  from datetime import timezone as dt_timezone
5
- from time import strftime
6
5
  from typing import Callable
7
6
 
8
7
  import pytz
9
8
 
9
+ from letta.constants import DEFAULT_TIMEZONE
10
+
10
11
 
11
12
  def parse_formatted_time(formatted_time):
12
13
  # parse times returned by letta.utils.get_formatted_time()
@@ -18,33 +19,22 @@ def datetime_to_timestamp(dt):
18
19
  return int(dt.timestamp())
19
20
 
20
21
 
21
- def get_local_time_military():
22
- # Get the current time in UTC
22
+ def get_local_time_fast(timezone):
23
+ # Get current UTC time and convert to the specified timezone
24
+ if not timezone:
25
+ return datetime.now().strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
23
26
  current_time_utc = datetime.now(pytz.utc)
24
-
25
- # Convert to San Francisco's time zone (PST/PDT)
26
- sf_time_zone = pytz.timezone("America/Los_Angeles")
27
- local_time = current_time_utc.astimezone(sf_time_zone)
28
-
29
- # You may format it as you desire
30
- formatted_time = local_time.strftime("%Y-%m-%d %H:%M:%S %Z%z")
31
-
32
- return formatted_time
33
-
34
-
35
- def get_local_time_fast():
36
- formatted_time = strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
27
+ local_time = current_time_utc.astimezone(pytz.timezone(timezone))
28
+ formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
37
29
 
38
30
  return formatted_time
39
31
 
40
32
 
41
- def get_local_time_timezone(timezone="America/Los_Angeles"):
33
+ def get_local_time_timezone(timezone=DEFAULT_TIMEZONE):
42
34
  # Get the current time in UTC
43
35
  current_time_utc = datetime.now(pytz.utc)
44
36
 
45
- # Convert to San Francisco's time zone (PST/PDT)
46
- sf_time_zone = pytz.timezone(timezone)
47
- local_time = current_time_utc.astimezone(sf_time_zone)
37
+ local_time = current_time_utc.astimezone(pytz.timezone(timezone))
48
38
 
49
39
  # You may format it as you desire, including AM/PM
50
40
  formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
@@ -52,7 +42,7 @@ def get_local_time_timezone(timezone="America/Los_Angeles"):
52
42
  return formatted_time
53
43
 
54
44
 
55
- def get_local_time(timezone=None):
45
+ def get_local_time(timezone=DEFAULT_TIMEZONE):
56
46
  if timezone is not None:
57
47
  time_str = get_local_time_timezone(timezone)
58
48
  else:
@@ -89,8 +79,11 @@ def timestamp_to_datetime(timestamp_seconds: int) -> datetime:
89
79
  return datetime.fromtimestamp(timestamp_seconds, tz=dt_timezone.utc)
90
80
 
91
81
 
92
- def format_datetime(dt):
93
- return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
82
+ def format_datetime(dt, timezone):
83
+ if not timezone:
84
+ # use local timezone
85
+ return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
86
+ return dt.astimezone(pytz.timezone(timezone)).strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
94
87
 
95
88
 
96
89
  def validate_date_format(date_str):
@@ -12,6 +12,7 @@ from letta.schemas.message import Message, MessageCreate
12
12
  def convert_message_creates_to_messages(
13
13
  message_creates: list[MessageCreate],
14
14
  agent_id: str,
15
+ timezone: str,
15
16
  wrap_user_message: bool = True,
16
17
  wrap_system_message: bool = True,
17
18
  ) -> list[Message]:
@@ -19,6 +20,7 @@ def convert_message_creates_to_messages(
19
20
  _convert_message_create_to_message(
20
21
  message_create=create,
21
22
  agent_id=agent_id,
23
+ timezone=timezone,
22
24
  wrap_user_message=wrap_user_message,
23
25
  wrap_system_message=wrap_system_message,
24
26
  )
@@ -29,6 +31,7 @@ def convert_message_creates_to_messages(
29
31
  def _convert_message_create_to_message(
30
32
  message_create: MessageCreate,
31
33
  agent_id: str,
34
+ timezone: str,
32
35
  wrap_user_message: bool = True,
33
36
  wrap_system_message: bool = True,
34
37
  ) -> Message:
@@ -50,9 +53,9 @@ def _convert_message_create_to_message(
50
53
  if isinstance(content, TextContent):
51
54
  # Apply wrapping if needed
52
55
  if message_create.role == MessageRole.user and wrap_user_message:
53
- content.text = system.package_user_message(user_message=content.text)
56
+ content.text = system.package_user_message(user_message=content.text, timezone=timezone)
54
57
  elif message_create.role == MessageRole.system and wrap_system_message:
55
- content.text = system.package_system_message(system_message=content.text)
58
+ content.text = system.package_system_message(system_message=content.text, timezone=timezone)
56
59
  elif isinstance(content, ImageContent):
57
60
  if content.source.type == ImageSourceType.url:
58
61
  # Convert URL image to Base64Image if needed
@@ -12,6 +12,7 @@ from letta.schemas.tool_rule import (
12
12
  InitToolRule,
13
13
  MaxCountPerStepToolRule,
14
14
  ParentToolRule,
15
+ RequiredBeforeExitToolRule,
15
16
  TerminalToolRule,
16
17
  )
17
18
 
@@ -41,6 +42,9 @@ class ToolRulesSolver(BaseModel):
41
42
  terminal_tool_rules: List[TerminalToolRule] = Field(
42
43
  default_factory=list, description="Terminal tool rules that end the agent loop if called."
43
44
  )
45
+ required_before_exit_tool_rules: List[RequiredBeforeExitToolRule] = Field(
46
+ default_factory=list, description="Tool rules that must be called before the agent can exit."
47
+ )
44
48
  tool_call_history: List[str] = Field(default_factory=list, description="History of tool calls, updated with each tool call.")
45
49
 
46
50
  def __init__(
@@ -51,6 +55,7 @@ class ToolRulesSolver(BaseModel):
51
55
  child_based_tool_rules: Optional[List[Union[ChildToolRule, ConditionalToolRule, MaxCountPerStepToolRule]]] = None,
52
56
  parent_tool_rules: Optional[List[ParentToolRule]] = None,
53
57
  terminal_tool_rules: Optional[List[TerminalToolRule]] = None,
58
+ required_before_exit_tool_rules: Optional[List[RequiredBeforeExitToolRule]] = None,
54
59
  tool_call_history: Optional[List[str]] = None,
55
60
  **kwargs,
56
61
  ):
@@ -60,6 +65,7 @@ class ToolRulesSolver(BaseModel):
60
65
  child_based_tool_rules=child_based_tool_rules or [],
61
66
  parent_tool_rules=parent_tool_rules or [],
62
67
  terminal_tool_rules=terminal_tool_rules or [],
68
+ required_before_exit_tool_rules=required_before_exit_tool_rules or [],
63
69
  tool_call_history=tool_call_history or [],
64
70
  **kwargs,
65
71
  )
@@ -88,6 +94,9 @@ class ToolRulesSolver(BaseModel):
88
94
  elif rule.type == ToolRuleType.parent_last_tool:
89
95
  assert isinstance(rule, ParentToolRule)
90
96
  self.parent_tool_rules.append(rule)
97
+ elif rule.type == ToolRuleType.required_before_exit:
98
+ assert isinstance(rule, RequiredBeforeExitToolRule)
99
+ self.required_before_exit_tool_rules.append(rule)
91
100
 
92
101
  def register_tool_call(self, tool_name: str):
93
102
  """Update the internal state to track tool call history."""
@@ -131,7 +140,7 @@ class ToolRulesSolver(BaseModel):
131
140
  return list(final_allowed_tools)
132
141
 
133
142
  def is_terminal_tool(self, tool_name: str) -> bool:
134
- """Check if the tool is defined as a terminal tool in the terminal tool rules."""
143
+ """Check if the tool is defined as a terminal tool in the terminal tool rules or required-before-exit tool rules."""
135
144
  return any(rule.tool_name == tool_name for rule in self.terminal_tool_rules)
136
145
 
137
146
  def has_children_tools(self, tool_name):
@@ -142,6 +151,24 @@ class ToolRulesSolver(BaseModel):
142
151
  """Check if the tool is defined as a continue tool in the tool rules."""
143
152
  return any(rule.tool_name == tool_name for rule in self.continue_tool_rules)
144
153
 
154
+ def has_required_tools_been_called(self) -> bool:
155
+ """Check if all required-before-exit tools have been called."""
156
+ return len(self.get_uncalled_required_tools()) == 0
157
+
158
+ def get_uncalled_required_tools(self) -> List[str]:
159
+ """Get the list of required-before-exit tools that have not been called yet."""
160
+ if not self.required_before_exit_tool_rules:
161
+ return [] # No required tools means no uncalled tools
162
+
163
+ required_tool_names = {rule.tool_name for rule in self.required_before_exit_tool_rules}
164
+ called_tool_names = set(self.tool_call_history)
165
+
166
+ return list(required_tool_names - called_tool_names)
167
+
168
+ def get_ending_tool_names(self) -> List[str]:
169
+ """Get the names of tools that are required before exit."""
170
+ return [rule.tool_name for rule in self.required_before_exit_tool_rules]
171
+
145
172
  def compile_tool_rule_prompts(self) -> Optional[Block]:
146
173
  """
147
174
  Compile prompt templates from all tool rules into an ephemeral Block.
@@ -168,7 +195,7 @@ class ToolRulesSolver(BaseModel):
168
195
  return Block(
169
196
  label="tool_usage_rules",
170
197
  value="\n".join(compiled_prompts),
171
- description="The following constraints define rules for tool usage and guide desired behavior. These rules must be followed to ensure proper tool execution and workflow.",
198
+ description="The following constraints define rules for tool usage and guide desired behavior. These rules must be followed to ensure proper tool execution and workflow. A single response may contain multiple tool calls.",
172
199
  )
173
200
  return None
174
201
 
@@ -6,11 +6,12 @@ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
6
6
 
7
7
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
8
8
  from letta.helpers.datetime_helpers import get_utc_timestamp_ns, ns_to_ms
9
+ from letta.llm_api.openai_client import is_openai_reasoning_model
9
10
  from letta.log import get_logger
10
11
  from letta.otel.context import get_ctx_attributes
11
12
  from letta.otel.metric_registry import MetricRegistry
12
13
  from letta.schemas.letta_message import AssistantMessage, LettaMessage, ReasoningMessage, ToolCallDelta, ToolCallMessage
13
- from letta.schemas.letta_message_content import TextContent
14
+ from letta.schemas.letta_message_content import OmittedReasoningContent, TextContent
14
15
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
15
16
  from letta.schemas.message import Message
16
17
  from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
@@ -61,7 +62,13 @@ class OpenAIStreamingInterface:
61
62
 
62
63
  def get_reasoning_content(self) -> List[TextContent]:
63
64
  content = "".join(self.reasoning_messages).strip()
64
- return [TextContent(text=content)]
65
+
66
+ # Right now we assume that all models omit reasoning content for OAI,
67
+ # if this changes, we should return the reasoning content
68
+ if is_openai_reasoning_model(self.model):
69
+ return [OmittedReasoningContent()]
70
+ else:
71
+ return [TextContent(text=content)]
65
72
 
66
73
  def get_tool_call_object(self) -> ToolCall:
67
74
  """Useful for agent loop"""