letta-nightly 0.7.15.dev20250514104255__py3-none-any.whl → 0.7.16.dev20250515205957__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 (36) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +12 -0
  3. letta/agents/helpers.py +48 -5
  4. letta/agents/letta_agent.py +46 -18
  5. letta/agents/letta_agent_batch.py +44 -26
  6. letta/agents/voice_sleeptime_agent.py +6 -4
  7. letta/client/client.py +16 -1
  8. letta/constants.py +3 -0
  9. letta/functions/async_composio_toolset.py +1 -1
  10. letta/groups/sleeptime_multi_agent.py +1 -0
  11. letta/interfaces/anthropic_streaming_interface.py +40 -6
  12. letta/jobs/llm_batch_job_polling.py +6 -2
  13. letta/orm/agent.py +102 -1
  14. letta/orm/block.py +3 -0
  15. letta/orm/sqlalchemy_base.py +365 -133
  16. letta/schemas/agent.py +10 -2
  17. letta/schemas/block.py +3 -0
  18. letta/schemas/memory.py +7 -2
  19. letta/server/rest_api/routers/v1/agents.py +13 -13
  20. letta/server/rest_api/routers/v1/messages.py +6 -6
  21. letta/server/rest_api/routers/v1/tools.py +3 -3
  22. letta/server/server.py +74 -0
  23. letta/services/agent_manager.py +421 -7
  24. letta/services/block_manager.py +12 -8
  25. letta/services/helpers/agent_manager_helper.py +19 -0
  26. letta/services/job_manager.py +99 -0
  27. letta/services/llm_batch_manager.py +28 -27
  28. letta/services/message_manager.py +51 -19
  29. letta/services/tool_executor/tool_executor.py +19 -1
  30. letta/services/tool_manager.py +13 -3
  31. letta/types/__init__.py +0 -0
  32. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/METADATA +3 -3
  33. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/RECORD +36 -35
  34. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/LICENSE +0 -0
  35. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/WHEEL +0 -0
  36. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.7.15"
1
+ __version__ = "0.7.16"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
letta/agent.py CHANGED
@@ -179,6 +179,15 @@ class Agent(BaseAgent):
179
179
  raise ValueError(f"Invalid JSON format in message: {text_content}")
180
180
  return None
181
181
 
182
+ def ensure_read_only_block_not_modified(self, new_memory: Memory) -> None:
183
+ """
184
+ Throw an error if a read-only block has been modified
185
+ """
186
+ for label in self.agent_state.memory.list_block_labels():
187
+ if self.agent_state.memory.get_block(label).read_only:
188
+ if new_memory.get_block(label).value != self.agent_state.memory.get_block(label).value:
189
+ raise ValueError(READ_ONLY_BLOCK_EDIT_ERROR)
190
+
182
191
  def update_memory_if_changed(self, new_memory: Memory) -> bool:
183
192
  """
184
193
  Update internal memory object and system prompt if there have been modifications.
@@ -1277,6 +1286,9 @@ class Agent(BaseAgent):
1277
1286
  agent_state_copy = self.agent_state.__deepcopy__()
1278
1287
  function_args["agent_state"] = agent_state_copy # need to attach self to arg since it's dynamically linked
1279
1288
  function_response = callable_func(**function_args)
1289
+ self.ensure_read_only_block_not_modified(
1290
+ new_memory=agent_state_copy.memory
1291
+ ) # memory editing tools cannot edit read-only blocks
1280
1292
  self.update_memory_if_changed(agent_state_copy.memory)
1281
1293
  elif target_letta_tool.tool_type == ToolType.EXTERNAL_COMPOSIO:
1282
1294
  action_name = generate_composio_action_from_func_name(target_letta_tool.name)
letta/agents/helpers.py CHANGED
@@ -10,14 +10,18 @@ from letta.server.rest_api.utils import create_input_messages
10
10
  from letta.services.message_manager import MessageManager
11
11
 
12
12
 
13
- def _create_letta_response(new_in_context_messages: list[Message], use_assistant_message: bool) -> LettaResponse:
13
+ def _create_letta_response(
14
+ new_in_context_messages: list[Message], use_assistant_message: bool, usage: LettaUsageStatistics
15
+ ) -> LettaResponse:
14
16
  """
15
17
  Converts the newly created/persisted messages into a LettaResponse.
16
18
  """
17
- response_messages = []
18
- for msg in new_in_context_messages:
19
- response_messages.extend(msg.to_letta_messages(use_assistant_message=use_assistant_message))
20
- return LettaResponse(messages=response_messages, usage=LettaUsageStatistics())
19
+ # NOTE: hacky solution to avoid returning heartbeat messages and the original user message
20
+ filter_user_messages = [m for m in new_in_context_messages if m.role != "user"]
21
+ response_messages = Message.to_letta_messages_from_list(
22
+ messages=filter_user_messages, use_assistant_message=use_assistant_message, reverse=False
23
+ )
24
+ return LettaResponse(messages=response_messages, usage=usage)
21
25
 
22
26
 
23
27
  def _prepare_in_context_messages(
@@ -56,6 +60,45 @@ def _prepare_in_context_messages(
56
60
  return current_in_context_messages, new_in_context_messages
57
61
 
58
62
 
63
+ async def _prepare_in_context_messages_async(
64
+ input_messages: List[MessageCreate],
65
+ agent_state: AgentState,
66
+ message_manager: MessageManager,
67
+ actor: User,
68
+ ) -> Tuple[List[Message], List[Message]]:
69
+ """
70
+ Prepares in-context messages for an agent, based on the current state and a new user input.
71
+ Async version of _prepare_in_context_messages.
72
+
73
+ Args:
74
+ input_messages (List[MessageCreate]): The new user input messages to process.
75
+ agent_state (AgentState): The current state of the agent, including message buffer config.
76
+ message_manager (MessageManager): The manager used to retrieve and create messages.
77
+ actor (User): The user performing the action, used for access control and attribution.
78
+
79
+ Returns:
80
+ Tuple[List[Message], List[Message]]: A tuple containing:
81
+ - The current in-context messages (existing context for the agent).
82
+ - The new in-context messages (messages created from the new input).
83
+ """
84
+
85
+ if agent_state.message_buffer_autoclear:
86
+ # If autoclear is enabled, only include the most recent system message (usually at index 0)
87
+ current_in_context_messages = [
88
+ (await message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor))[0]
89
+ ]
90
+ else:
91
+ # Otherwise, include the full list of messages by ID for context
92
+ current_in_context_messages = await message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor)
93
+
94
+ # Create a new user message from the input and store it
95
+ new_in_context_messages = await message_manager.create_many_messages_async(
96
+ create_input_messages(input_messages=input_messages, agent_id=agent_state.id, actor=actor), actor=actor
97
+ )
98
+
99
+ return current_in_context_messages, new_in_context_messages
100
+
101
+
59
102
  def serialize_message_history(messages: List[str], context: str) -> str:
60
103
  """
61
104
  Produce an XML document like:
@@ -4,6 +4,7 @@ import uuid
4
4
  from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
5
5
 
6
6
  from openai import AsyncStream
7
+ from openai.types import CompletionUsage
7
8
  from openai.types.chat import ChatCompletion, ChatCompletionChunk
8
9
 
9
10
  from letta.agents.base_agent import BaseAgent
@@ -23,6 +24,7 @@ from letta.schemas.letta_message_content import OmittedReasoningContent, Reasoni
23
24
  from letta.schemas.letta_response import LettaResponse
24
25
  from letta.schemas.message import Message, MessageCreate
25
26
  from letta.schemas.openai.chat_completion_response import ToolCall
27
+ from letta.schemas.usage import LettaUsageStatistics
26
28
  from letta.schemas.user import User
27
29
  from letta.server.rest_api.utils import create_letta_messages_from_llm_response
28
30
  from letta.services.agent_manager import AgentManager
@@ -47,7 +49,6 @@ class LettaAgent(BaseAgent):
47
49
  block_manager: BlockManager,
48
50
  passage_manager: PassageManager,
49
51
  actor: User,
50
- use_assistant_message: bool = True,
51
52
  ):
52
53
  super().__init__(agent_id=agent_id, openai_client=None, message_manager=message_manager, agent_manager=agent_manager, actor=actor)
53
54
 
@@ -55,26 +56,31 @@ class LettaAgent(BaseAgent):
55
56
  # Summarizer settings
56
57
  self.block_manager = block_manager
57
58
  self.passage_manager = passage_manager
58
- self.use_assistant_message = use_assistant_message
59
59
  self.response_messages: List[Message] = []
60
60
 
61
- self.last_function_response = self._load_last_function_response()
61
+ self.last_function_response = None
62
+
63
+ # Cached archival memory/message size
64
+ self.num_messages = self.message_manager.size(actor=self.actor, agent_id=agent_id)
65
+ self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_id)
62
66
 
63
67
  # Cached archival memory/message size
64
68
  self.num_messages = self.message_manager.size(actor=self.actor, agent_id=agent_id)
65
69
  self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_id)
66
70
 
67
71
  @trace_method
68
- async def step(self, input_messages: List[MessageCreate], max_steps: int = 10) -> LettaResponse:
69
- agent_state = self.agent_manager.get_agent_by_id(self.agent_id, actor=self.actor)
70
- current_in_context_messages, new_in_context_messages = await self._step(
72
+ async def step(self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True) -> LettaResponse:
73
+ agent_state = await self.agent_manager.get_agent_by_id_async(self.agent_id, actor=self.actor)
74
+ current_in_context_messages, new_in_context_messages, usage = await self._step(
71
75
  agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
72
76
  )
73
- return _create_letta_response(new_in_context_messages=new_in_context_messages, use_assistant_message=self.use_assistant_message)
77
+ return _create_letta_response(
78
+ new_in_context_messages=new_in_context_messages, use_assistant_message=use_assistant_message, usage=usage
79
+ )
74
80
 
75
81
  async def _step(
76
82
  self, agent_state: AgentState, input_messages: List[MessageCreate], max_steps: int = 10
77
- ) -> Tuple[List[Message], List[Message]]:
83
+ ) -> Tuple[List[Message], List[Message], CompletionUsage]:
78
84
  current_in_context_messages, new_in_context_messages = _prepare_in_context_messages(
79
85
  input_messages, agent_state, self.message_manager, self.actor
80
86
  )
@@ -84,6 +90,7 @@ class LettaAgent(BaseAgent):
84
90
  put_inner_thoughts_first=True,
85
91
  actor=self.actor,
86
92
  )
93
+ usage = LettaUsageStatistics()
87
94
  for _ in range(max_steps):
88
95
  response = await self._get_ai_reply(
89
96
  llm_client=llm_client,
@@ -95,11 +102,21 @@ class LettaAgent(BaseAgent):
95
102
  )
96
103
 
97
104
  tool_call = response.choices[0].message.tool_calls[0]
105
+ reasoning = [TextContent(text=response.choices[0].message.content)] # reasoning placed into content for legacy reasons
98
106
 
99
- persisted_messages, should_continue = await self._handle_ai_response(tool_call, agent_state, tool_rules_solver)
107
+ persisted_messages, should_continue = await self._handle_ai_response(
108
+ tool_call, agent_state, tool_rules_solver, reasoning_content=reasoning
109
+ )
100
110
  self.response_messages.extend(persisted_messages)
101
111
  new_in_context_messages.extend(persisted_messages)
102
112
 
113
+ # update usage
114
+ # TODO: add run_id
115
+ usage.step_count += 1
116
+ usage.completion_tokens += response.usage.completion_tokens
117
+ usage.prompt_tokens += response.usage.prompt_tokens
118
+ usage.total_tokens += response.usage.total_tokens
119
+
103
120
  if not should_continue:
104
121
  break
105
122
 
@@ -108,17 +125,17 @@ class LettaAgent(BaseAgent):
108
125
  message_ids = [m.id for m in (current_in_context_messages + new_in_context_messages)]
109
126
  self.agent_manager.set_in_context_messages(agent_id=self.agent_id, message_ids=message_ids, actor=self.actor)
110
127
 
111
- return current_in_context_messages, new_in_context_messages
128
+ return current_in_context_messages, new_in_context_messages, usage
112
129
 
113
130
  @trace_method
114
131
  async def step_stream(
115
- self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = False
132
+ self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True
116
133
  ) -> AsyncGenerator[str, None]:
117
134
  """
118
135
  Main streaming loop that yields partial tokens.
119
136
  Whenever we detect a tool call, we yield from _handle_ai_response as well.
120
137
  """
121
- agent_state = self.agent_manager.get_agent_by_id(self.agent_id, actor=self.actor)
138
+ agent_state = await self.agent_manager.get_agent_by_id_async(self.agent_id, actor=self.actor)
122
139
  current_in_context_messages, new_in_context_messages = _prepare_in_context_messages(
123
140
  input_messages, agent_state, self.message_manager, self.actor
124
141
  )
@@ -128,6 +145,7 @@ class LettaAgent(BaseAgent):
128
145
  put_inner_thoughts_first=True,
129
146
  actor=self.actor,
130
147
  )
148
+ usage = LettaUsageStatistics()
131
149
 
132
150
  for _ in range(max_steps):
133
151
  stream = await self._get_ai_reply(
@@ -137,7 +155,6 @@ class LettaAgent(BaseAgent):
137
155
  tool_rules_solver=tool_rules_solver,
138
156
  stream=True,
139
157
  )
140
-
141
158
  # TODO: THIS IS INCREDIBLY UGLY
142
159
  # TODO: THERE ARE MULTIPLE COPIES OF THE LLM_CONFIG EVERYWHERE THAT ARE GETTING MANIPULATED
143
160
  interface = AnthropicStreamingInterface(
@@ -146,6 +163,12 @@ class LettaAgent(BaseAgent):
146
163
  async for chunk in interface.process(stream):
147
164
  yield f"data: {chunk.model_dump_json()}\n\n"
148
165
 
166
+ # update usage
167
+ usage.step_count += 1
168
+ usage.completion_tokens += interface.output_tokens
169
+ usage.prompt_tokens += interface.input_tokens
170
+ usage.total_tokens += interface.input_tokens + interface.output_tokens
171
+
149
172
  # Process resulting stream content
150
173
  tool_call = interface.get_tool_call_object()
151
174
  reasoning_content = interface.get_reasoning_content()
@@ -160,6 +183,10 @@ class LettaAgent(BaseAgent):
160
183
  self.response_messages.extend(persisted_messages)
161
184
  new_in_context_messages.extend(persisted_messages)
162
185
 
186
+ if not use_assistant_message or should_continue:
187
+ tool_return = [msg for msg in persisted_messages if msg.role == "tool"][-1].to_letta_messages()[0]
188
+ yield f"data: {tool_return.model_dump_json()}\n\n"
189
+
163
190
  if not should_continue:
164
191
  break
165
192
 
@@ -174,7 +201,7 @@ class LettaAgent(BaseAgent):
174
201
  self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_state.id)
175
202
 
176
203
  # TODO: Also yield out a letta usage stats SSE
177
-
204
+ yield f"data: {usage.model_dump_json()}\n\n"
178
205
  yield f"data: {MessageStreamStatus.done.model_dump_json()}\n\n"
179
206
 
180
207
  @trace_method
@@ -214,6 +241,8 @@ class LettaAgent(BaseAgent):
214
241
  ]
215
242
 
216
243
  # Mirror the sync agent loop: get allowed tools or allow all if none are allowed
244
+ if self.last_function_response is None:
245
+ self.last_function_response = await self._load_last_function_response_async()
217
246
  valid_tool_names = tool_rules_solver.get_allowed_tool_names(
218
247
  available_tools=set([t.name for t in tools]),
219
248
  last_function_response=self.last_function_response,
@@ -307,7 +336,7 @@ class LettaAgent(BaseAgent):
307
336
  pre_computed_assistant_message_id=pre_computed_assistant_message_id,
308
337
  pre_computed_tool_message_id=pre_computed_tool_message_id,
309
338
  )
310
- persisted_messages = self.message_manager.create_many_messages(tool_call_messages, actor=self.actor)
339
+ persisted_messages = await self.message_manager.create_many_messages_async(tool_call_messages, actor=self.actor)
311
340
  self.last_function_response = function_response
312
341
 
313
342
  return persisted_messages, continue_stepping
@@ -359,7 +388,6 @@ class LettaAgent(BaseAgent):
359
388
  block_manager=self.block_manager,
360
389
  passage_manager=self.passage_manager,
361
390
  actor=self.actor,
362
- use_assistant_message=True,
363
391
  )
364
392
 
365
393
  augmented_message = (
@@ -394,9 +422,9 @@ class LettaAgent(BaseAgent):
394
422
  results = await asyncio.gather(*tasks)
395
423
  return results
396
424
 
397
- def _load_last_function_response(self):
425
+ async def _load_last_function_response_async(self):
398
426
  """Load the last function response from message history"""
399
- in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_id, actor=self.actor)
427
+ in_context_messages = await self.agent_manager.get_in_context_messages_async(agent_id=self.agent_id, actor=self.actor)
400
428
  for msg in reversed(in_context_messages):
401
429
  if msg.role == MessageRole.tool and msg.content and len(msg.content) == 1 and isinstance(msg.content[0], TextContent):
402
430
  text_content = msg.content[0].text
@@ -7,7 +7,7 @@ from aiomultiprocess import Pool
7
7
  from anthropic.types.beta.messages import BetaMessageBatchCanceledResult, BetaMessageBatchErroredResult, BetaMessageBatchSucceededResult
8
8
 
9
9
  from letta.agents.base_agent import BaseAgent
10
- from letta.agents.helpers import _prepare_in_context_messages
10
+ from letta.agents.helpers import _prepare_in_context_messages_async
11
11
  from letta.helpers import ToolRulesSolver
12
12
  from letta.helpers.datetime_helpers import get_utc_time
13
13
  from letta.helpers.tool_execution_helper import enable_strict_mode
@@ -107,7 +107,6 @@ class LettaAgentBatch(BaseAgent):
107
107
  sandbox_config_manager: SandboxConfigManager,
108
108
  job_manager: JobManager,
109
109
  actor: User,
110
- use_assistant_message: bool = True,
111
110
  max_steps: int = 10,
112
111
  ):
113
112
  self.message_manager = message_manager
@@ -117,7 +116,6 @@ class LettaAgentBatch(BaseAgent):
117
116
  self.batch_manager = batch_manager
118
117
  self.sandbox_config_manager = sandbox_config_manager
119
118
  self.job_manager = job_manager
120
- self.use_assistant_message = use_assistant_message
121
119
  self.actor = actor
122
120
  self.max_steps = max_steps
123
121
 
@@ -128,6 +126,7 @@ class LettaAgentBatch(BaseAgent):
128
126
  letta_batch_job_id: str,
129
127
  agent_step_state_mapping: Optional[Dict[str, AgentStepState]] = None,
130
128
  ) -> LettaBatchResponse:
129
+ """Carry out agent steps until the LLM request is sent."""
131
130
  log_event(name="validate_inputs")
132
131
  if not batch_requests:
133
132
  raise ValueError("Empty list of batch_requests passed in!")
@@ -135,15 +134,26 @@ class LettaAgentBatch(BaseAgent):
135
134
  agent_step_state_mapping = {}
136
135
 
137
136
  log_event(name="load_and_prepare_agents")
138
- agent_messages_mapping: Dict[str, List[Message]] = {}
139
- agent_tools_mapping: Dict[str, List[dict]] = {}
137
+ # prepares (1) agent states, (2) step states, (3) LLMBatchItems (4) message batch_item_ids (5) messages per agent (6) tools per agent
138
+
139
+ agent_messages_mapping: dict[str, list[Message]] = {}
140
+ agent_tools_mapping: dict[str, list[dict]] = {}
140
141
  # TODO: This isn't optimal, moving fast - prone to bugs because we pass around this half formed pydantic object
141
- agent_batch_item_mapping: Dict[str, LLMBatchItem] = {}
142
+ agent_batch_item_mapping: dict[str, LLMBatchItem] = {}
143
+
144
+ # fetch agent states in batch
145
+ agent_mapping = {
146
+ agent_state.id: agent_state
147
+ for agent_state in await self.agent_manager.get_agents_by_ids_async(
148
+ agent_ids=[request.agent_id for request in batch_requests], actor=self.actor
149
+ )
150
+ }
151
+
142
152
  agent_states = []
143
153
  for batch_request in batch_requests:
144
154
  agent_id = batch_request.agent_id
145
- agent_state = self.agent_manager.get_agent_by_id(agent_id, actor=self.actor)
146
- agent_states.append(agent_state)
155
+ agent_state = agent_mapping[agent_id]
156
+ agent_states.append(agent_state) # keeping this to maintain ordering, but may not be necessary
147
157
 
148
158
  if agent_id not in agent_step_state_mapping:
149
159
  agent_step_state_mapping[agent_id] = AgentStepState(
@@ -164,7 +174,7 @@ class LettaAgentBatch(BaseAgent):
164
174
  for msg in batch_request.messages:
165
175
  msg.batch_item_id = llm_batch_item.id
166
176
 
167
- agent_messages_mapping[agent_id] = self._prepare_in_context_messages_per_agent(
177
+ agent_messages_mapping[agent_id] = await self._prepare_in_context_messages_per_agent_async(
168
178
  agent_state=agent_state, input_messages=batch_request.messages
169
179
  )
170
180
 
@@ -186,7 +196,7 @@ class LettaAgentBatch(BaseAgent):
186
196
  )
187
197
 
188
198
  log_event(name="persist_llm_batch_job")
189
- llm_batch_job = self.batch_manager.create_llm_batch_job(
199
+ llm_batch_job = await self.batch_manager.create_llm_batch_job_async(
190
200
  llm_provider=ProviderType.anthropic, # TODO: Expand to more providers
191
201
  create_batch_response=batch_response,
192
202
  actor=self.actor,
@@ -204,7 +214,7 @@ class LettaAgentBatch(BaseAgent):
204
214
 
205
215
  if batch_items:
206
216
  log_event(name="bulk_create_batch_items")
207
- batch_items_persisted = self.batch_manager.create_llm_batch_items_bulk(batch_items, actor=self.actor)
217
+ batch_items_persisted = await self.batch_manager.create_llm_batch_items_bulk_async(batch_items, actor=self.actor)
208
218
 
209
219
  log_event(name="return_batch_response")
210
220
  return LettaBatchResponse(
@@ -219,7 +229,7 @@ class LettaAgentBatch(BaseAgent):
219
229
  @trace_method
220
230
  async def resume_step_after_request(self, letta_batch_id: str, llm_batch_id: str) -> LettaBatchResponse:
221
231
  log_event(name="load_context")
222
- llm_batch_job = self.batch_manager.get_llm_batch_job_by_id(llm_batch_id=llm_batch_id, actor=self.actor)
232
+ llm_batch_job = await self.batch_manager.get_llm_batch_job_by_id_async(llm_batch_id=llm_batch_id, actor=self.actor)
223
233
  ctx = await self._collect_resume_context(llm_batch_id)
224
234
 
225
235
  log_event(name="update_statuses")
@@ -229,7 +239,7 @@ class LettaAgentBatch(BaseAgent):
229
239
  exec_results = await self._execute_tools(ctx)
230
240
 
231
241
  log_event(name="persist_messages")
232
- msg_map = self._persist_tool_messages(exec_results, ctx)
242
+ msg_map = await self._persist_tool_messages(exec_results, ctx)
233
243
 
234
244
  log_event(name="mark_steps_done")
235
245
  self._mark_steps_complete(llm_batch_id, ctx.agent_ids)
@@ -237,7 +247,9 @@ class LettaAgentBatch(BaseAgent):
237
247
  log_event(name="prepare_next")
238
248
  next_reqs, next_step_state = self._prepare_next_iteration(exec_results, ctx, msg_map)
239
249
  if len(next_reqs) == 0:
240
- self.job_manager.update_job_by_id(job_id=letta_batch_id, job_update=JobUpdate(status=JobStatus.completed), actor=self.actor)
250
+ await self.job_manager.update_job_by_id_async(
251
+ job_id=letta_batch_id, job_update=JobUpdate(status=JobStatus.completed), actor=self.actor
252
+ )
241
253
  return LettaBatchResponse(
242
254
  letta_batch_id=llm_batch_job.letta_batch_job_id,
243
255
  last_llm_batch_id=llm_batch_job.id,
@@ -256,18 +268,22 @@ class LettaAgentBatch(BaseAgent):
256
268
  @trace_method
257
269
  async def _collect_resume_context(self, llm_batch_id: str) -> _ResumeContext:
258
270
  # NOTE: We only continue for items with successful results
259
- batch_items = self.batch_manager.list_llm_batch_items(llm_batch_id=llm_batch_id, request_status=JobStatus.completed)
271
+ batch_items = await self.batch_manager.list_llm_batch_items_async(llm_batch_id=llm_batch_id, request_status=JobStatus.completed)
260
272
 
261
- agent_ids, agent_state_map = [], {}
262
- provider_results, name_map, args_map, cont_map = {}, {}, {}, {}
273
+ agent_ids = []
274
+ provider_results = {}
263
275
  request_status_updates: List[RequestStatusUpdateInfo] = []
264
276
 
265
277
  for item in batch_items:
266
278
  aid = item.agent_id
267
279
  agent_ids.append(aid)
268
- agent_state_map[aid] = self.agent_manager.get_agent_by_id(aid, actor=self.actor)
269
280
  provider_results[aid] = item.batch_request_result.result
270
281
 
282
+ agent_states = await self.agent_manager.get_agents_by_ids_async(agent_ids, actor=self.actor)
283
+ agent_state_map = {agent.id: agent for agent in agent_states}
284
+
285
+ name_map, args_map, cont_map = {}, {}, {}
286
+ for aid in agent_ids:
271
287
  # status bookkeeping
272
288
  pr = provider_results[aid]
273
289
  status = (
@@ -344,14 +360,14 @@ class LettaAgentBatch(BaseAgent):
344
360
  tool_params.append(param)
345
361
 
346
362
  if rethink_memory_params:
347
- return self._bulk_rethink_memory(rethink_memory_params)
363
+ return await self._bulk_rethink_memory_async(rethink_memory_params)
348
364
 
349
365
  if tool_params:
350
366
  async with Pool() as pool:
351
367
  return await pool.map(execute_tool_wrapper, tool_params)
352
368
 
353
369
  @trace_method
354
- def _bulk_rethink_memory(self, params: List[ToolExecutionParams]) -> Sequence[Tuple[str, Tuple[str, bool]]]:
370
+ async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[Tuple[str, Tuple[str, bool]]]:
355
371
  updates = {}
356
372
  result = []
357
373
  for param in params:
@@ -372,11 +388,11 @@ class LettaAgentBatch(BaseAgent):
372
388
  # TODO: This is quite ugly and confusing - this is mostly to align with the returns of other tools
373
389
  result.append((param.agent_id, ("", True)))
374
390
 
375
- self.block_manager.bulk_update_block_values(updates=updates, actor=self.actor)
391
+ await self.block_manager.bulk_update_block_values_async(updates=updates, actor=self.actor)
376
392
 
377
393
  return result
378
394
 
379
- def _persist_tool_messages(
395
+ async def _persist_tool_messages(
380
396
  self,
381
397
  exec_results: Sequence[Tuple[str, Tuple[str, bool]]],
382
398
  ctx: _ResumeContext,
@@ -398,7 +414,7 @@ class LettaAgentBatch(BaseAgent):
398
414
  )
399
415
  msg_map[aid] = msgs
400
416
  # flatten & persist
401
- self.message_manager.create_many_messages([m for msgs in msg_map.values() for m in msgs], actor=self.actor)
417
+ await self.message_manager.create_many_messages_async([m for msgs in msg_map.values() for m in msgs], actor=self.actor)
402
418
  return msg_map
403
419
 
404
420
  def _mark_steps_complete(self, llm_batch_id: str, agent_ids: List[str]) -> None:
@@ -530,12 +546,14 @@ class LettaAgentBatch(BaseAgent):
530
546
  valid_tool_names = tool_rules_solver.get_allowed_tool_names(available_tools=set([t.name for t in tools]))
531
547
  return [enable_strict_mode(t.json_schema) for t in tools if t.name in set(valid_tool_names)]
532
548
 
533
- def _prepare_in_context_messages_per_agent(self, agent_state: AgentState, input_messages: List[MessageCreate]) -> List[Message]:
534
- current_in_context_messages, new_in_context_messages = _prepare_in_context_messages(
549
+ async def _prepare_in_context_messages_per_agent_async(
550
+ self, agent_state: AgentState, input_messages: List[MessageCreate]
551
+ ) -> List[Message]:
552
+ current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_async(
535
553
  input_messages, agent_state, self.message_manager, self.actor
536
554
  )
537
555
 
538
- in_context_messages = self._rebuild_memory(current_in_context_messages + new_in_context_messages, agent_state)
556
+ in_context_messages = await self._rebuild_memory_async(current_in_context_messages + new_in_context_messages, agent_state)
539
557
  return in_context_messages
540
558
 
541
559
  # TODO: Make this a bullk function
@@ -58,7 +58,7 @@ class VoiceSleeptimeAgent(LettaAgent):
58
58
  def update_message_transcript(self, message_transcripts: List[str]):
59
59
  self.message_transcripts = message_transcripts
60
60
 
61
- async def step(self, input_messages: List[MessageCreate], max_steps: int = 20) -> LettaResponse:
61
+ async def step(self, input_messages: List[MessageCreate], max_steps: int = 20, use_assistant_message: bool = True) -> LettaResponse:
62
62
  """
63
63
  Process the user's input message, allowing the model to call memory-related tools
64
64
  until it decides to stop and provide a final response.
@@ -74,7 +74,7 @@ class VoiceSleeptimeAgent(LettaAgent):
74
74
  ]
75
75
 
76
76
  # Summarize
77
- current_in_context_messages, new_in_context_messages = await super()._step(
77
+ current_in_context_messages, new_in_context_messages, usage = await super()._step(
78
78
  agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
79
79
  )
80
80
  new_in_context_messages, updated = self.summarizer.summarize(
@@ -84,7 +84,9 @@ class VoiceSleeptimeAgent(LettaAgent):
84
84
  agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor
85
85
  )
86
86
 
87
- return _create_letta_response(new_in_context_messages=new_in_context_messages, use_assistant_message=self.use_assistant_message)
87
+ return _create_letta_response(
88
+ new_in_context_messages=new_in_context_messages, use_assistant_message=use_assistant_message, usage=usage
89
+ )
88
90
 
89
91
  @trace_method
90
92
  async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
@@ -146,7 +148,7 @@ class VoiceSleeptimeAgent(LettaAgent):
146
148
  return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
147
149
 
148
150
  async def step_stream(
149
- self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = False
151
+ self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True
150
152
  ) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]:
151
153
  """
152
154
  This agent is synchronous-only. If called in an async context, raise an error.
letta/client/client.py CHANGED
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import logging
2
3
  import sys
3
4
  import time
@@ -3055,7 +3056,21 @@ class LocalClient(AbstractClient):
3055
3056
  Returns:
3056
3057
  tools (List[Tool]): List of tools
3057
3058
  """
3058
- return self.server.tool_manager.list_tools(after=after, limit=limit, actor=self.user)
3059
+ # Get the current event loop or create a new one if there isn't one
3060
+ try:
3061
+ loop = asyncio.get_event_loop()
3062
+ if loop.is_running():
3063
+ # We're in an async context but can't await - use a new loop via run_coroutine_threadsafe
3064
+ concurrent_future = asyncio.run_coroutine_threadsafe(
3065
+ self.server.tool_manager.list_tools_async(actor=self.user, after=after, limit=limit), loop
3066
+ )
3067
+ return concurrent_future.result()
3068
+ else:
3069
+ # We have a loop but it's not running - we can just run the coroutine
3070
+ return loop.run_until_complete(self.server.tool_manager.list_tools_async(actor=self.user, after=after, limit=limit))
3071
+ except RuntimeError:
3072
+ # No running event loop - create a new one with asyncio.run
3073
+ return asyncio.run(self.server.tool_manager.list_tools_async(actor=self.user, after=after, limit=limit))
3059
3074
 
3060
3075
  def get_tool(self, id: str) -> Optional[Tool]:
3061
3076
  """
letta/constants.py CHANGED
@@ -195,6 +195,9 @@ DATA_SOURCE_ATTACH_ALERT = (
195
195
  "[ALERT] New data was just uploaded to archival memory. You can view this data by calling the archival_memory_search tool."
196
196
  )
197
197
 
198
+ # Throw an error message when a read-only block is edited
199
+ READ_ONLY_BLOCK_EDIT_ERROR = f"{ERROR_MESSAGE_PREFIX} This block is read-only and cannot be edited."
200
+
198
201
  # The ackknowledgement message used in the summarize sequence
199
202
  MESSAGE_SUMMARY_REQUEST_ACK = "Understood, I will respond with a summary of the message (and only the summary, nothing else) once I receive the conversation history. I'm ready."
200
203
 
@@ -12,7 +12,7 @@ from composio.exceptions import (
12
12
  )
13
13
 
14
14
 
15
- class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta"):
15
+ class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta", description_char_limit=1024):
16
16
  """
17
17
  Async version of ComposioToolSet client for interacting with Composio API
18
18
  Used to asynchronously hit the execute action endpoint
@@ -42,6 +42,7 @@ class SleeptimeMultiAgent(Agent):
42
42
  self.group_manager = GroupManager()
43
43
  self.message_manager = MessageManager()
44
44
  self.job_manager = JobManager()
45
+ self.mcp_clients = mcp_clients
45
46
 
46
47
  def _run_async_in_new_thread(self, coro):
47
48
  """Run an async coroutine in a new thread with its own event loop"""