letta-nightly 0.7.21.dev20250522104246__py3-none-any.whl → 0.7.22.dev20250523081403__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 (50) hide show
  1. letta/__init__.py +2 -2
  2. letta/agents/base_agent.py +4 -2
  3. letta/agents/letta_agent.py +3 -10
  4. letta/agents/letta_agent_batch.py +6 -6
  5. letta/cli/cli.py +0 -316
  6. letta/cli/cli_load.py +0 -52
  7. letta/client/client.py +2 -1554
  8. letta/data_sources/connectors.py +4 -2
  9. letta/functions/ast_parsers.py +33 -43
  10. letta/groups/sleeptime_multi_agent_v2.py +49 -13
  11. letta/jobs/llm_batch_job_polling.py +3 -3
  12. letta/jobs/scheduler.py +20 -19
  13. letta/llm_api/anthropic_client.py +3 -0
  14. letta/llm_api/google_vertex_client.py +5 -0
  15. letta/llm_api/openai_client.py +5 -0
  16. letta/main.py +2 -362
  17. letta/server/db.py +5 -0
  18. letta/server/rest_api/routers/v1/agents.py +72 -43
  19. letta/server/rest_api/routers/v1/llms.py +2 -2
  20. letta/server/rest_api/routers/v1/messages.py +5 -3
  21. letta/server/rest_api/routers/v1/sandbox_configs.py +18 -18
  22. letta/server/rest_api/routers/v1/sources.py +49 -36
  23. letta/server/server.py +53 -22
  24. letta/services/agent_manager.py +797 -124
  25. letta/services/block_manager.py +14 -62
  26. letta/services/group_manager.py +37 -0
  27. letta/services/identity_manager.py +9 -0
  28. letta/services/job_manager.py +17 -0
  29. letta/services/llm_batch_manager.py +88 -64
  30. letta/services/message_manager.py +19 -0
  31. letta/services/organization_manager.py +10 -0
  32. letta/services/passage_manager.py +13 -0
  33. letta/services/per_agent_lock_manager.py +4 -0
  34. letta/services/provider_manager.py +34 -0
  35. letta/services/sandbox_config_manager.py +130 -0
  36. letta/services/source_manager.py +59 -44
  37. letta/services/step_manager.py +8 -1
  38. letta/services/tool_manager.py +21 -0
  39. letta/services/tool_sandbox/e2b_sandbox.py +4 -2
  40. letta/services/tool_sandbox/local_sandbox.py +7 -3
  41. letta/services/user_manager.py +16 -0
  42. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/METADATA +1 -1
  43. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/RECORD +46 -50
  44. letta/__main__.py +0 -3
  45. letta/benchmark/benchmark.py +0 -98
  46. letta/benchmark/constants.py +0 -14
  47. letta/cli/cli_config.py +0 -227
  48. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/LICENSE +0 -0
  49. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/WHEEL +0 -0
  50. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523081403.dist-info}/entry_points.txt +0 -0
letta/main.py CHANGED
@@ -1,374 +1,14 @@
1
1
  import os
2
- import sys
3
- import traceback
4
2
 
5
- import questionary
6
- import requests
7
3
  import typer
8
- from rich.console import Console
9
4
 
10
- import letta.agent as agent
11
- import letta.errors as errors
12
- import letta.system as system
13
-
14
- # import benchmark
15
- from letta import create_client
16
- from letta.benchmark.benchmark import bench
17
- from letta.cli.cli import delete_agent, open_folder, run, server, version
18
- from letta.cli.cli_config import add, add_tool, configure, delete, list, list_tools
5
+ from letta.cli.cli import server
19
6
  from letta.cli.cli_load import app as load_app
20
- from letta.config import LettaConfig
21
- from letta.constants import FUNC_FAILED_HEARTBEAT_MESSAGE, REQ_HEARTBEAT_MESSAGE
22
-
23
- # from letta.interface import CLIInterface as interface # for printing to terminal
24
- from letta.streaming_interface import AgentRefreshStreamingInterface
25
-
26
- # interface = interface()
27
7
 
28
8
  # disable composio print on exit
29
9
  os.environ["COMPOSIO_DISABLE_VERSION_CHECK"] = "true"
30
10
 
31
11
  app = typer.Typer(pretty_exceptions_enable=False)
32
- app.command(name="run")(run)
33
- app.command(name="version")(version)
34
- app.command(name="configure")(configure)
35
- app.command(name="list")(list)
36
- app.command(name="add")(add)
37
- app.command(name="add-tool")(add_tool)
38
- app.command(name="list-tools")(list_tools)
39
- app.command(name="delete")(delete)
40
12
  app.command(name="server")(server)
41
- app.command(name="folder")(open_folder)
42
- # load data commands
43
- app.add_typer(load_app, name="load")
44
- # benchmark command
45
- app.command(name="benchmark")(bench)
46
- # delete agents
47
- app.command(name="delete-agent")(delete_agent)
48
-
49
-
50
- def clear_line(console, strip_ui=False):
51
- if strip_ui:
52
- return
53
- if os.name == "nt": # for windows
54
- console.print("\033[A\033[K", end="")
55
- else: # for linux
56
- sys.stdout.write("\033[2K\033[G")
57
- sys.stdout.flush()
58
-
59
-
60
- def run_agent_loop(
61
- letta_agent: agent.Agent,
62
- config: LettaConfig,
63
- first: bool,
64
- no_verify: bool = False,
65
- strip_ui: bool = False,
66
- stream: bool = False,
67
- ):
68
- if isinstance(letta_agent.interface, AgentRefreshStreamingInterface):
69
- # letta_agent.interface.toggle_streaming(on=stream)
70
- if not stream:
71
- letta_agent.interface = letta_agent.interface.nonstreaming_interface
72
-
73
- if hasattr(letta_agent.interface, "console"):
74
- console = letta_agent.interface.console
75
- else:
76
- console = Console()
77
-
78
- counter = 0
79
- user_input = None
80
- skip_next_user_input = False
81
- user_message = None
82
- USER_GOES_FIRST = first
83
-
84
- if not USER_GOES_FIRST:
85
- console.input("[bold cyan]Hit enter to begin (will request first Letta message)[/bold cyan]\n")
86
- clear_line(console, strip_ui=strip_ui)
87
- print()
88
-
89
- multiline_input = False
90
-
91
- # create client
92
- client = create_client()
93
-
94
- # run loops
95
- while True:
96
- if not skip_next_user_input and (counter > 0 or USER_GOES_FIRST):
97
- # Ask for user input
98
- if not stream:
99
- print()
100
- user_input = questionary.text(
101
- "Enter your message:",
102
- multiline=multiline_input,
103
- qmark=">",
104
- ).ask()
105
- clear_line(console, strip_ui=strip_ui)
106
- if not stream:
107
- print()
108
-
109
- # Gracefully exit on Ctrl-C/D
110
- if user_input is None:
111
- user_input = "/exit"
112
-
113
- user_input = user_input.rstrip()
114
-
115
- if user_input.startswith("!"):
116
- print(f"Commands for CLI begin with '/' not '!'")
117
- continue
118
-
119
- if user_input == "":
120
- # no empty messages allowed
121
- print("Empty input received. Try again!")
122
- continue
123
-
124
- # Handle CLI commands
125
- # Commands to not get passed as input to Letta
126
- if user_input.startswith("/"):
127
- # updated agent save functions
128
- if user_input.lower() == "/exit":
129
- # letta_agent.save()
130
- agent.save_agent(letta_agent)
131
- break
132
- elif user_input.lower() == "/save" or user_input.lower() == "/savechat":
133
- # letta_agent.save()
134
- agent.save_agent(letta_agent)
135
- continue
136
- elif user_input.lower() == "/attach":
137
- # TODO: check if agent already has it
138
-
139
- # TODO: check to ensure source embedding dimentions/model match agents, and disallow attachment if not
140
- # TODO: alternatively, only list sources with compatible embeddings, and print warning about non-compatible sources
141
-
142
- sources = client.list_sources()
143
- if len(sources) == 0:
144
- typer.secho(
145
- 'No sources available. You must load a souce with "letta load ..." before running /attach.',
146
- fg=typer.colors.RED,
147
- bold=True,
148
- )
149
- continue
150
-
151
- # determine what sources are valid to be attached to this agent
152
- valid_options = []
153
- invalid_options = []
154
- for source in sources:
155
- if source.embedding_config == letta_agent.agent_state.embedding_config:
156
- valid_options.append(source.name)
157
- else:
158
- # print warning about invalid sources
159
- typer.secho(
160
- f"Source {source.name} exists but has embedding dimentions {source.embedding_dim} from model {source.embedding_model}, while the agent uses embedding dimentions {letta_agent.agent_state.embedding_config.embedding_dim} and model {letta_agent.agent_state.embedding_config.embedding_model}",
161
- fg=typer.colors.YELLOW,
162
- )
163
- invalid_options.append(source.name)
164
-
165
- # prompt user for data source selection
166
- data_source = questionary.select("Select data source", choices=valid_options).ask()
167
-
168
- # attach new data
169
- client.attach_source_to_agent(agent_id=letta_agent.agent_state.id, source_name=data_source)
170
13
 
171
- continue
172
-
173
- elif user_input.lower() == "/dump" or user_input.lower().startswith("/dump "):
174
- # Check if there's an additional argument that's an integer
175
- command = user_input.strip().split()
176
- amount = int(command[1]) if len(command) > 1 and command[1].isdigit() else 0
177
- if amount == 0:
178
- letta_agent.interface.print_messages(letta_agent._messages, dump=True)
179
- else:
180
- letta_agent.interface.print_messages(letta_agent._messages[-min(amount, len(letta_agent.messages)) :], dump=True)
181
- continue
182
-
183
- elif user_input.lower() == "/dumpraw":
184
- letta_agent.interface.print_messages_raw(letta_agent._messages)
185
- continue
186
-
187
- elif user_input.lower() == "/memory":
188
- print(f"\nDumping memory contents:\n")
189
- print(f"{letta_agent.agent_state.memory.compile()}")
190
- print(f"{letta_agent.archival_memory.compile()}")
191
- continue
192
-
193
- elif user_input.lower() == "/model":
194
- print(f"Current model: {letta_agent.agent_state.llm_config.model}")
195
- continue
196
-
197
- elif user_input.lower() == "/summarize":
198
- try:
199
- letta_agent.summarize_messages_inplace()
200
- typer.secho(
201
- f"/summarize succeeded",
202
- fg=typer.colors.GREEN,
203
- bold=True,
204
- )
205
- except (errors.LLMError, requests.exceptions.HTTPError) as e:
206
- typer.secho(
207
- f"/summarize failed:\n{e}",
208
- fg=typer.colors.RED,
209
- bold=True,
210
- )
211
- continue
212
-
213
- elif user_input.lower() == "/tokens":
214
- tokens = letta_agent.count_tokens()
215
- typer.secho(
216
- f"{tokens}/{letta_agent.agent_state.llm_config.context_window}",
217
- fg=typer.colors.GREEN,
218
- bold=True,
219
- )
220
- continue
221
-
222
- elif user_input.lower().startswith("/add_function"):
223
- try:
224
- if len(user_input) < len("/add_function "):
225
- print("Missing function name after the command")
226
- continue
227
- function_name = user_input[len("/add_function ") :].strip()
228
- result = letta_agent.add_function(function_name)
229
- typer.secho(
230
- f"/add_function succeeded: {result}",
231
- fg=typer.colors.GREEN,
232
- bold=True,
233
- )
234
- except ValueError as e:
235
- typer.secho(
236
- f"/add_function failed:\n{e}",
237
- fg=typer.colors.RED,
238
- bold=True,
239
- )
240
- continue
241
- elif user_input.lower().startswith("/remove_function"):
242
- try:
243
- if len(user_input) < len("/remove_function "):
244
- print("Missing function name after the command")
245
- continue
246
- function_name = user_input[len("/remove_function ") :].strip()
247
- result = letta_agent.remove_function(function_name)
248
- typer.secho(
249
- f"/remove_function succeeded: {result}",
250
- fg=typer.colors.GREEN,
251
- bold=True,
252
- )
253
- except ValueError as e:
254
- typer.secho(
255
- f"/remove_function failed:\n{e}",
256
- fg=typer.colors.RED,
257
- bold=True,
258
- )
259
- continue
260
-
261
- # No skip options
262
- elif user_input.lower() == "/wipe":
263
- letta_agent = agent.Agent(letta_agent.interface)
264
- user_message = None
265
-
266
- elif user_input.lower() == "/heartbeat":
267
- user_message = system.get_heartbeat()
268
-
269
- elif user_input.lower() == "/memorywarning":
270
- user_message = system.get_token_limit_warning()
271
-
272
- elif user_input.lower() == "//":
273
- multiline_input = not multiline_input
274
- continue
275
-
276
- elif user_input.lower() == "/" or user_input.lower() == "/help":
277
- questionary.print("CLI commands", "bold")
278
- for cmd, desc in USER_COMMANDS:
279
- questionary.print(cmd, "bold")
280
- questionary.print(f" {desc}")
281
- continue
282
- else:
283
- print(f"Unrecognized command: {user_input}")
284
- continue
285
-
286
- else:
287
- # If message did not begin with command prefix, pass inputs to Letta
288
- # Handle user message and append to messages
289
- user_message = str(user_input)
290
-
291
- skip_next_user_input = False
292
-
293
- def process_agent_step(user_message, no_verify):
294
- # TODO(charles): update to use agent.step() instead of inner_step()
295
-
296
- if user_message is None:
297
- step_response = letta_agent.inner_step(
298
- messages=[],
299
- first_message=False,
300
- skip_verify=no_verify,
301
- stream=stream,
302
- )
303
- else:
304
- step_response = letta_agent.step_user_message(
305
- user_message_str=user_message,
306
- first_message=False,
307
- skip_verify=no_verify,
308
- stream=stream,
309
- )
310
- new_messages = step_response.messages
311
- heartbeat_request = step_response.heartbeat_request
312
- function_failed = step_response.function_failed
313
- token_warning = step_response.in_context_memory_warning
314
- step_response.usage
315
-
316
- agent.save_agent(letta_agent)
317
- skip_next_user_input = False
318
- if token_warning:
319
- user_message = system.get_token_limit_warning()
320
- skip_next_user_input = True
321
- elif function_failed:
322
- user_message = system.get_heartbeat(FUNC_FAILED_HEARTBEAT_MESSAGE)
323
- skip_next_user_input = True
324
- elif heartbeat_request:
325
- user_message = system.get_heartbeat(REQ_HEARTBEAT_MESSAGE)
326
- skip_next_user_input = True
327
-
328
- return new_messages, user_message, skip_next_user_input
329
-
330
- while True:
331
- try:
332
- if strip_ui:
333
- _, user_message, skip_next_user_input = process_agent_step(user_message, no_verify)
334
- break
335
- else:
336
- if stream:
337
- # Don't display the "Thinking..." if streaming
338
- _, user_message, skip_next_user_input = process_agent_step(user_message, no_verify)
339
- else:
340
- with console.status("[bold cyan]Thinking...") as status:
341
- _, user_message, skip_next_user_input = process_agent_step(user_message, no_verify)
342
- break
343
- except KeyboardInterrupt:
344
- print("User interrupt occurred.")
345
- retry = questionary.confirm("Retry agent.step()?").ask()
346
- if not retry:
347
- break
348
- except Exception:
349
- print("An exception occurred when running agent.step(): ")
350
- traceback.print_exc()
351
- retry = questionary.confirm("Retry agent.step()?").ask()
352
- if not retry:
353
- break
354
-
355
- counter += 1
356
-
357
- print("Finished.")
358
-
359
-
360
- USER_COMMANDS = [
361
- ("//", "toggle multiline input mode"),
362
- ("/exit", "exit the CLI"),
363
- ("/save", "save a checkpoint of the current agent/conversation state"),
364
- ("/load", "load a saved checkpoint"),
365
- ("/dump <count>", "view the last <count> messages (all if <count> is omitted)"),
366
- ("/memory", "print the current contents of agent memory"),
367
- ("/pop <count>", "undo <count> messages in the conversation (default is 3)"),
368
- ("/retry", "pops the last answer and tries to get another one"),
369
- ("/rethink <text>", "changes the inner thoughts of the last agent message"),
370
- ("/rewrite <text>", "changes the reply of the last agent message"),
371
- ("/heartbeat", "send a heartbeat system message to the agent"),
372
- ("/memorywarning", "send a memory warning system message to the agent"),
373
- ("/attach", "attach data source to agent"),
374
- ]
14
+ app.add_typer(load_app, name="load")
letta/server/db.py CHANGED
@@ -13,6 +13,9 @@ from sqlalchemy.orm import sessionmaker
13
13
  from letta.config import LettaConfig
14
14
  from letta.log import get_logger
15
15
  from letta.settings import settings
16
+ from letta.tracing import trace_method
17
+
18
+ logger = get_logger(__name__)
16
19
 
17
20
  logger = get_logger(__name__)
18
21
 
@@ -202,6 +205,7 @@ class DatabaseRegistry:
202
205
  self.initialize_async()
203
206
  return self._async_session_factories.get(name)
204
207
 
208
+ @trace_method
205
209
  @contextmanager
206
210
  def session(self, name: str = "default") -> Generator[Any, None, None]:
207
211
  """Context manager for database sessions."""
@@ -215,6 +219,7 @@ class DatabaseRegistry:
215
219
  finally:
216
220
  session.close()
217
221
 
222
+ @trace_method
218
223
  @asynccontextmanager
219
224
  async def async_session(self, name: str = "default") -> AsyncGenerator[AsyncSession, None]:
220
225
  """Async context manager for database sessions."""
@@ -13,10 +13,11 @@ from starlette.responses import Response, StreamingResponse
13
13
 
14
14
  from letta.agents.letta_agent import LettaAgent
15
15
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
16
+ from letta.groups.sleeptime_multi_agent_v2 import SleeptimeMultiAgentV2
16
17
  from letta.helpers.datetime_helpers import get_utc_timestamp_ns
17
18
  from letta.log import get_logger
18
19
  from letta.orm.errors import NoResultFound
19
- from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgent
20
+ from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
20
21
  from letta.schemas.block import Block, BlockUpdate
21
22
  from letta.schemas.group import Group
22
23
  from letta.schemas.job import JobStatus, JobUpdate, LettaRequestConfig
@@ -212,7 +213,7 @@ async def retrieve_agent_context_window(
212
213
  """
213
214
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
214
215
  try:
215
- return await server.get_agent_context_window_async(agent_id=agent_id, actor=actor)
216
+ return await server.agent_manager.get_context_window(agent_id=agent_id, actor=actor)
216
217
  except Exception as e:
217
218
  traceback.print_exc()
218
219
  raise e
@@ -297,7 +298,7 @@ def detach_tool(
297
298
 
298
299
 
299
300
  @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent")
300
- def attach_source(
301
+ async def attach_source(
301
302
  agent_id: str,
302
303
  source_id: str,
303
304
  background_tasks: BackgroundTasks,
@@ -310,7 +311,7 @@ def attach_source(
310
311
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
311
312
  agent = server.agent_manager.attach_source(agent_id=agent_id, source_id=source_id, actor=actor)
312
313
  if agent.enable_sleeptime:
313
- source = server.source_manager.get_source_by_id(source_id=source_id)
314
+ source = await server.source_manager.get_source_by_id_async(source_id=source_id)
314
315
  background_tasks.add_task(server.sleeptime_document_ingest, agent, source, actor)
315
316
  return agent
316
317
 
@@ -355,7 +356,7 @@ async def retrieve_agent(
355
356
 
356
357
 
357
358
  @router.delete("/{agent_id}", response_model=None, operation_id="delete_agent")
358
- def delete_agent(
359
+ async def delete_agent(
359
360
  agent_id: str,
360
361
  server: "SyncServer" = Depends(get_letta_server),
361
362
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -363,9 +364,9 @@ def delete_agent(
363
364
  """
364
365
  Delete an agent.
365
366
  """
366
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
367
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
367
368
  try:
368
- server.agent_manager.delete_agent(agent_id=agent_id, actor=actor)
369
+ await server.agent_manager.delete_agent_async(agent_id=agent_id, actor=actor)
369
370
  return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Agent id={agent_id} successfully deleted"})
370
371
  except NoResultFound:
371
372
  raise HTTPException(status_code=404, detail=f"Agent agent_id={agent_id} not found for user_id={actor.id}.")
@@ -386,7 +387,7 @@ async def list_agent_sources(
386
387
 
387
388
  # TODO: remove? can also get with agent blocks
388
389
  @router.get("/{agent_id}/core-memory", response_model=Memory, operation_id="retrieve_agent_memory")
389
- def retrieve_agent_memory(
390
+ async def retrieve_agent_memory(
390
391
  agent_id: str,
391
392
  server: "SyncServer" = Depends(get_letta_server),
392
393
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -395,13 +396,13 @@ def retrieve_agent_memory(
395
396
  Retrieve the memory state of a specific agent.
396
397
  This endpoint fetches the current memory state of the agent identified by the user ID and agent ID.
397
398
  """
398
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
399
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
399
400
 
400
- return server.get_agent_memory(agent_id=agent_id, actor=actor)
401
+ return await server.get_agent_memory_async(agent_id=agent_id, actor=actor)
401
402
 
402
403
 
403
404
  @router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="retrieve_core_memory_block")
404
- def retrieve_block(
405
+ async def retrieve_block(
405
406
  agent_id: str,
406
407
  block_label: str,
407
408
  server: "SyncServer" = Depends(get_letta_server),
@@ -410,10 +411,10 @@ def retrieve_block(
410
411
  """
411
412
  Retrieve a core memory block from an agent.
412
413
  """
413
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
414
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
414
415
 
415
416
  try:
416
- return server.agent_manager.get_block_with_label(agent_id=agent_id, block_label=block_label, actor=actor)
417
+ return await server.agent_manager.get_block_with_label_async(agent_id=agent_id, block_label=block_label, actor=actor)
417
418
  except NoResultFound as e:
418
419
  raise HTTPException(status_code=404, detail=str(e))
419
420
 
@@ -453,13 +454,13 @@ async def modify_block(
453
454
  )
454
455
 
455
456
  # This should also trigger a system prompt change in the agent
456
- server.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True, update_timestamp=False)
457
+ await server.agent_manager.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True, update_timestamp=False)
457
458
 
458
459
  return block
459
460
 
460
461
 
461
462
  @router.patch("/{agent_id}/core-memory/blocks/attach/{block_id}", response_model=AgentState, operation_id="attach_core_memory_block")
462
- def attach_block(
463
+ async def attach_block(
463
464
  agent_id: str,
464
465
  block_id: str,
465
466
  server: "SyncServer" = Depends(get_letta_server),
@@ -468,12 +469,12 @@ def attach_block(
468
469
  """
469
470
  Attach a core memoryblock to an agent.
470
471
  """
471
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
472
- return server.agent_manager.attach_block(agent_id=agent_id, block_id=block_id, actor=actor)
472
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
473
+ return await server.agent_manager.attach_block_async(agent_id=agent_id, block_id=block_id, actor=actor)
473
474
 
474
475
 
475
476
  @router.patch("/{agent_id}/core-memory/blocks/detach/{block_id}", response_model=AgentState, operation_id="detach_core_memory_block")
476
- def detach_block(
477
+ async def detach_block(
477
478
  agent_id: str,
478
479
  block_id: str,
479
480
  server: "SyncServer" = Depends(get_letta_server),
@@ -482,8 +483,8 @@ def detach_block(
482
483
  """
483
484
  Detach a core memory block from an agent.
484
485
  """
485
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
486
- return server.agent_manager.detach_block(agent_id=agent_id, block_id=block_id, actor=actor)
486
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
487
+ return await server.agent_manager.detach_block_async(agent_id=agent_id, block_id=block_id, actor=actor)
487
488
 
488
489
 
489
490
  @router.get("/{agent_id}/archival-memory", response_model=List[Passage], operation_id="list_passages")
@@ -637,22 +638,35 @@ async def send_message(
637
638
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
638
639
  # TODO: This is redundant, remove soon
639
640
  agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
640
- agent_eligible = not agent.enable_sleeptime and not agent.multi_agent_group and agent.agent_type != AgentType.sleeptime_agent
641
+ agent_eligible = agent.enable_sleeptime or not agent.multi_agent_group
641
642
  experimental_header = request_obj.headers.get("X-EXPERIMENTAL") or "false"
642
643
  feature_enabled = settings.use_experimental or experimental_header.lower() == "true"
643
644
  model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "together", "google_ai", "google_vertex"]
644
645
 
645
646
  if agent_eligible and feature_enabled and model_compatible:
646
- experimental_agent = LettaAgent(
647
- agent_id=agent_id,
648
- message_manager=server.message_manager,
649
- agent_manager=server.agent_manager,
650
- block_manager=server.block_manager,
651
- passage_manager=server.passage_manager,
652
- actor=actor,
653
- step_manager=server.step_manager,
654
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
655
- )
647
+ if agent.enable_sleeptime:
648
+ experimental_agent = SleeptimeMultiAgentV2(
649
+ agent_id=agent_id,
650
+ message_manager=server.message_manager,
651
+ agent_manager=server.agent_manager,
652
+ block_manager=server.block_manager,
653
+ passage_manager=server.passage_manager,
654
+ group_manager=server.group_manager,
655
+ job_manager=server.job_manager,
656
+ actor=actor,
657
+ group=agent.multi_agent_group,
658
+ )
659
+ else:
660
+ experimental_agent = LettaAgent(
661
+ agent_id=agent_id,
662
+ message_manager=server.message_manager,
663
+ agent_manager=server.agent_manager,
664
+ block_manager=server.block_manager,
665
+ passage_manager=server.passage_manager,
666
+ actor=actor,
667
+ step_manager=server.step_manager,
668
+ telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
669
+ )
656
670
 
657
671
  result = await experimental_agent.step(request.messages, max_steps=10, use_assistant_message=request.use_assistant_message)
658
672
  else:
@@ -697,23 +711,38 @@ async def send_message_streaming(
697
711
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
698
712
  # TODO: This is redundant, remove soon
699
713
  agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
700
- agent_eligible = not agent.enable_sleeptime and not agent.multi_agent_group and agent.agent_type != AgentType.sleeptime_agent
714
+ agent_eligible = agent.enable_sleeptime or not agent.multi_agent_group
701
715
  experimental_header = request_obj.headers.get("X-EXPERIMENTAL") or "false"
702
716
  feature_enabled = settings.use_experimental or experimental_header.lower() == "true"
703
717
  model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "together", "google_ai", "google_vertex"]
704
718
  model_compatible_token_streaming = agent.llm_config.model_endpoint_type in ["anthropic", "openai"]
705
719
 
706
- if agent_eligible and feature_enabled and model_compatible and request.stream_tokens:
707
- experimental_agent = LettaAgent(
708
- agent_id=agent_id,
709
- message_manager=server.message_manager,
710
- agent_manager=server.agent_manager,
711
- block_manager=server.block_manager,
712
- passage_manager=server.passage_manager,
713
- actor=actor,
714
- step_manager=server.step_manager,
715
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
716
- )
720
+ if agent_eligible and feature_enabled and model_compatible:
721
+ if agent.enable_sleeptime:
722
+ experimental_agent = SleeptimeMultiAgentV2(
723
+ agent_id=agent_id,
724
+ message_manager=server.message_manager,
725
+ agent_manager=server.agent_manager,
726
+ block_manager=server.block_manager,
727
+ passage_manager=server.passage_manager,
728
+ group_manager=server.group_manager,
729
+ job_manager=server.job_manager,
730
+ actor=actor,
731
+ step_manager=server.step_manager,
732
+ telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
733
+ group=agent.multi_agent_group,
734
+ )
735
+ else:
736
+ experimental_agent = LettaAgent(
737
+ agent_id=agent_id,
738
+ message_manager=server.message_manager,
739
+ agent_manager=server.agent_manager,
740
+ block_manager=server.block_manager,
741
+ passage_manager=server.passage_manager,
742
+ actor=actor,
743
+ step_manager=server.step_manager,
744
+ telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
745
+ )
717
746
  from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode
718
747
 
719
748
  if request.stream_tokens and model_compatible_token_streaming:
@@ -23,7 +23,7 @@ async def list_llm_models(
23
23
  # Extract user_id from header, default to None if not present
24
24
  ):
25
25
  """List available LLM models using the asynchronous implementation for improved performance"""
26
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
26
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
27
27
 
28
28
  models = await server.list_llm_models_async(
29
29
  provider_category=provider_category,
@@ -42,7 +42,7 @@ async def list_embedding_models(
42
42
  # Extract user_id from header, default to None if not present
43
43
  ):
44
44
  """List available embedding models using the asynchronous implementation for improved performance"""
45
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
45
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
46
46
  models = await server.list_embedding_models_async(actor=actor)
47
47
 
48
48
  return models