letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -31,10 +31,10 @@ from letta.otel.metric_registry import MetricRegistry
31
31
  from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
32
32
  from letta.schemas.agent_file import AgentFileSchema
33
33
  from letta.schemas.block import Block, BlockUpdate
34
- from letta.schemas.enums import JobType
34
+ from letta.schemas.enums import AgentType, RunStatus
35
35
  from letta.schemas.file import AgentFileAttachment, PaginatedAgentFiles
36
36
  from letta.schemas.group import Group
37
- from letta.schemas.job import JobStatus, JobUpdate, LettaRequestConfig
37
+ from letta.schemas.job import LettaRequestConfig
38
38
  from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion, MessageType
39
39
  from letta.schemas.letta_request import LettaAsyncRequest, LettaRequest, LettaStreamingRequest
40
40
  from letta.schemas.letta_response import LettaResponse
@@ -48,7 +48,7 @@ from letta.schemas.memory import (
48
48
  )
49
49
  from letta.schemas.message import MessageCreate, MessageSearchRequest, MessageSearchResult
50
50
  from letta.schemas.passage import Passage
51
- from letta.schemas.run import Run
51
+ from letta.schemas.run import Run as PydanticRun, RunUpdate
52
52
  from letta.schemas.source import Source
53
53
  from letta.schemas.tool import Tool
54
54
  from letta.schemas.user import User
@@ -56,6 +56,8 @@ from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
56
56
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
57
57
  from letta.server.rest_api.redis_stream_manager import create_background_stream_processor, redis_sse_stream_generator
58
58
  from letta.server.server import SyncServer
59
+ from letta.services.lettuce import LettuceClient
60
+ from letta.services.run_manager import RunManager
59
61
  from letta.settings import settings
60
62
  from letta.utils import safe_create_shielded_task, safe_create_task, truncate_file_visible_content
61
63
 
@@ -188,28 +190,16 @@ async def export_agent(
188
190
  - Legacy format (use_legacy_format=true): Single agent with inline tools/blocks
189
191
  - New format (default): Multi-entity format with separate agents, tools, blocks, files, etc.
190
192
  """
191
- actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
193
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
192
194
 
193
195
  if use_legacy_format:
194
196
  # Use the legacy serialization method
195
- try:
196
- agent = server.agent_manager.serialize(agent_id=agent_id, actor=actor, max_steps=max_steps)
197
- return agent.model_dump()
198
- except NoResultFound:
199
- raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
197
+ agent = await server.agent_manager.serialize(agent_id=agent_id, actor=actor, max_steps=max_steps)
198
+ return agent.model_dump()
200
199
  else:
201
200
  # Use the new multi-entity export format
202
- try:
203
- agent_file_schema = await server.agent_serialization_manager.export(agent_ids=[agent_id], actor=actor)
204
- return agent_file_schema.model_dump()
205
- except AgentNotFoundForExportError:
206
- raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
207
- except AgentExportIdMappingError as e:
208
- raise HTTPException(
209
- status_code=500, detail=f"Internal error during export: ID mapping failed for {e.entity_type} ID '{e.db_id}'"
210
- )
211
- except AgentExportProcessingError as e:
212
- raise HTTPException(status_code=500, detail=f"Export processing failed: {str(e.original_error)}")
201
+ agent_file_schema = await server.agent_serialization_manager.export(agent_ids=[agent_id], actor=actor)
202
+ return agent_file_schema.model_dump()
213
203
 
214
204
 
215
205
  class ImportedAgentsResponse(BaseModel):
@@ -231,33 +221,19 @@ def import_agent_legacy(
231
221
  """
232
222
  Import an agent using the legacy AgentSchema format.
233
223
  """
234
- try:
235
- # Validate the JSON against AgentSchema before passing it to deserialize
236
- agent_schema = AgentSchema.model_validate(agent_json)
237
-
238
- new_agent = server.agent_manager.deserialize(
239
- serialized_agent=agent_schema, # Ensure we're passing a validated AgentSchema
240
- actor=actor,
241
- append_copy_suffix=append_copy_suffix,
242
- override_existing_tools=override_existing_tools,
243
- project_id=project_id,
244
- strip_messages=strip_messages,
245
- env_vars=env_vars,
246
- )
247
- return [new_agent.id]
224
+ # Validate the JSON against AgentSchema before passing it to deserialize
225
+ agent_schema = AgentSchema.model_validate(agent_json)
248
226
 
249
- except ValidationError as e:
250
- raise HTTPException(status_code=422, detail=f"Invalid agent schema: {e!s}")
251
-
252
- except IntegrityError as e:
253
- raise HTTPException(status_code=409, detail=f"Database integrity error: {e!s}")
254
-
255
- except OperationalError as e:
256
- raise HTTPException(status_code=503, detail=f"Database connection error. Please try again later: {e!s}")
257
-
258
- except Exception as e:
259
- traceback.print_exc()
260
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred while uploading the agent: {e!s}")
227
+ new_agent = server.agent_manager.deserialize(
228
+ serialized_agent=agent_schema, # Ensure we're passing a validated AgentSchema
229
+ actor=actor,
230
+ append_copy_suffix=append_copy_suffix,
231
+ override_existing_tools=override_existing_tools,
232
+ project_id=project_id,
233
+ strip_messages=strip_messages,
234
+ env_vars=env_vars,
235
+ )
236
+ return [new_agent.id]
261
237
 
262
238
 
263
239
  async def _import_agent(
@@ -275,46 +251,29 @@ async def _import_agent(
275
251
  """
276
252
  Import an agent using the new AgentFileSchema format.
277
253
  """
278
- try:
279
- agent_schema = AgentFileSchema.model_validate(agent_file_json)
280
- except ValidationError as e:
281
- raise HTTPException(status_code=422, detail=f"Invalid agent file schema: {e!s}")
282
-
283
- try:
284
- if override_embedding_handle:
285
- embedding_config_override = await server.get_cached_embedding_config_async(actor=actor, handle=override_embedding_handle)
286
- else:
287
- embedding_config_override = None
254
+ agent_schema = AgentFileSchema.model_validate(agent_file_json)
288
255
 
289
- import_result = await server.agent_serialization_manager.import_file(
290
- schema=agent_schema,
291
- actor=actor,
292
- append_copy_suffix=append_copy_suffix,
293
- override_existing_tools=override_existing_tools,
294
- env_vars=env_vars,
295
- override_embedding_config=embedding_config_override,
296
- project_id=project_id,
297
- )
298
-
299
- if not import_result.success:
300
- raise HTTPException(
301
- status_code=500, detail=f"Import failed: {import_result.message}. Errors: {', '.join(import_result.errors)}"
302
- )
303
-
304
- return import_result.imported_agent_ids
256
+ if override_embedding_handle:
257
+ embedding_config_override = await server.get_cached_embedding_config_async(actor=actor, handle=override_embedding_handle)
258
+ else:
259
+ embedding_config_override = None
305
260
 
306
- except AgentFileImportError as e:
307
- raise HTTPException(status_code=400, detail=f"Agent file import error: {str(e)}")
261
+ import_result = await server.agent_serialization_manager.import_file(
262
+ schema=agent_schema,
263
+ actor=actor,
264
+ append_copy_suffix=append_copy_suffix,
265
+ override_existing_tools=override_existing_tools,
266
+ env_vars=env_vars,
267
+ override_embedding_config=embedding_config_override,
268
+ project_id=project_id,
269
+ )
308
270
 
309
- except IntegrityError as e:
310
- raise HTTPException(status_code=409, detail=f"Database integrity error: {e!s}")
271
+ if not import_result.success:
272
+ from letta.errors import AgentFileImportError
311
273
 
312
- except OperationalError as e:
313
- raise HTTPException(status_code=503, detail=f"Database connection error. Please try again later: {e!s}")
274
+ raise AgentFileImportError(f"Import failed: {import_result.message}. Errors: {', '.join(import_result.errors)}")
314
275
 
315
- except Exception as e:
316
- traceback.print_exc()
317
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred while importing agents: {e!s}")
276
+ return import_result.imported_agent_ids
318
277
 
319
278
 
320
279
  @router.post("/import", response_model=ImportedAgentsResponse, operation_id="import_agent")
@@ -345,7 +304,7 @@ async def import_agent(
345
304
  Import a serialized agent file and recreate the agent(s) in the system.
346
305
  Returns the IDs of all imported agents.
347
306
  """
348
- actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
307
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
349
308
 
350
309
  try:
351
310
  serialized_data = file.file.read()
@@ -384,15 +343,9 @@ async def import_agent(
384
343
  )
385
344
  else:
386
345
  # This is a legacy AgentSchema
387
- agent_ids = import_agent_legacy(
388
- agent_json=agent_json,
389
- server=server,
390
- actor=actor,
391
- append_copy_suffix=append_copy_suffix,
392
- override_existing_tools=override_existing_tools,
393
- project_id=project_id,
394
- strip_messages=strip_messages,
395
- env_vars=env_vars,
346
+ raise HTTPException(
347
+ status_code=400,
348
+ detail="Legacy AgentSchema format is deprecated. Please use the new AgentFileSchema format with 'agents' field.",
396
349
  )
397
350
 
398
351
  return ImportedAgentsResponse(agent_ids=agent_ids)
@@ -408,11 +361,7 @@ async def retrieve_agent_context_window(
408
361
  Retrieve the context window of a specific agent.
409
362
  """
410
363
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
411
- try:
412
- return await server.agent_manager.get_context_window(agent_id=agent_id, actor=actor)
413
- except Exception as e:
414
- traceback.print_exc()
415
- raise e
364
+ return await server.agent_manager.get_context_window(agent_id=agent_id, actor=actor)
416
365
 
417
366
 
418
367
  class CreateAgentRequest(CreateAgent):
@@ -436,12 +385,10 @@ async def create_agent(
436
385
  """
437
386
  Create an agent.
438
387
  """
439
- try:
440
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
441
- return await server.create_agent_async(agent, actor=actor)
442
- except Exception as e:
443
- traceback.print_exc()
444
- raise HTTPException(status_code=500, detail=str(e))
388
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
389
+ if headers.experimental_params.letta_v1_agent and agent.agent_type == AgentType.memgpt_v2_agent and not agent.enable_sleeptime:
390
+ agent.agent_type = AgentType.letta_v1_agent
391
+ return await server.create_agent_async(agent, actor=actor)
445
392
 
446
393
 
447
394
  @router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
@@ -461,10 +408,28 @@ async def list_agent_tools(
461
408
  agent_id: str,
462
409
  server: "SyncServer" = Depends(get_letta_server),
463
410
  headers: HeaderParams = Depends(get_headers),
411
+ before: Optional[str] = Query(
412
+ None, description="Tool ID cursor for pagination. Returns tools that come before this tool ID in the specified sort order"
413
+ ),
414
+ after: Optional[str] = Query(
415
+ None, description="Tool ID cursor for pagination. Returns tools that come after this tool ID in the specified sort order"
416
+ ),
417
+ limit: Optional[int] = Query(10, description="Maximum number of tools to return"),
418
+ order: Literal["asc", "desc"] = Query(
419
+ "desc", description="Sort order for tools by creation time. 'asc' for oldest first, 'desc' for newest first"
420
+ ),
421
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
464
422
  ):
465
423
  """Get tools from an existing agent"""
466
424
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
467
- return await server.agent_manager.list_attached_tools_async(agent_id=agent_id, actor=actor)
425
+ return await server.agent_manager.list_attached_tools_async(
426
+ agent_id=agent_id,
427
+ actor=actor,
428
+ before=before,
429
+ after=after,
430
+ limit=limit,
431
+ ascending=(order == "asc"),
432
+ )
468
433
 
469
434
 
470
435
  @router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=AgentState, operation_id="attach_tool")
@@ -666,12 +631,9 @@ async def open_file(
666
631
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
667
632
 
668
633
  # Get the agent to access files configuration
669
- try:
670
- per_file_view_window_char_limit, max_files_open = await server.agent_manager.get_agent_files_config_async(
671
- agent_id=agent_id, actor=actor
672
- )
673
- except ValueError:
674
- raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found")
634
+ per_file_view_window_char_limit, max_files_open = await server.agent_manager.get_agent_files_config_async(
635
+ agent_id=agent_id, actor=actor
636
+ )
675
637
 
676
638
  # Get file metadata
677
639
  file_metadata = await server.file_manager.get_file_by_id(file_id=file_id, actor=actor, include_content=True)
@@ -717,16 +679,13 @@ async def close_file(
717
679
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
718
680
 
719
681
  # Use update_file_agent_by_id to close the file
720
- try:
721
- await server.file_agent_manager.update_file_agent_by_id(
722
- agent_id=agent_id,
723
- file_id=file_id,
724
- actor=actor,
725
- is_open=False,
726
- )
727
- return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"File id={file_id} successfully closed"})
728
- except NoResultFound:
729
- raise HTTPException(status_code=404, detail=f"File association for file_id={file_id} and agent_id={agent_id} not found")
682
+ await server.file_agent_manager.update_file_agent_by_id(
683
+ agent_id=agent_id,
684
+ file_id=file_id,
685
+ actor=actor,
686
+ is_open=False,
687
+ )
688
+ return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"File id={file_id} successfully closed"})
730
689
 
731
690
 
732
691
  @router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
@@ -752,10 +711,7 @@ async def retrieve_agent(
752
711
 
753
712
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
754
713
 
755
- try:
756
- return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, include_relationships=include_relationships, actor=actor)
757
- except NoResultFound as e:
758
- raise HTTPException(status_code=404, detail=str(e))
714
+ return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, include_relationships=include_relationships, actor=actor)
759
715
 
760
716
 
761
717
  @router.delete("/{agent_id}", response_model=None, operation_id="delete_agent")
@@ -768,11 +724,8 @@ async def delete_agent(
768
724
  Delete an agent.
769
725
  """
770
726
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
771
- try:
772
- await server.agent_manager.delete_agent_async(agent_id=agent_id, actor=actor)
773
- return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Agent id={agent_id} successfully deleted"})
774
- except NoResultFound:
775
- raise HTTPException(status_code=404, detail=f"Agent agent_id={agent_id} not found for user_id={actor.id}.")
727
+ await server.agent_manager.delete_agent_async(agent_id=agent_id, actor=actor)
728
+ return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Agent id={agent_id} successfully deleted"})
776
729
 
777
730
 
778
731
  @router.get("/{agent_id}/sources", response_model=list[Source], operation_id="list_agent_sources")
@@ -780,12 +733,30 @@ async def list_agent_sources(
780
733
  agent_id: str,
781
734
  server: "SyncServer" = Depends(get_letta_server),
782
735
  headers: HeaderParams = Depends(get_headers),
736
+ before: Optional[str] = Query(
737
+ None, description="Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order"
738
+ ),
739
+ after: Optional[str] = Query(
740
+ None, description="Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order"
741
+ ),
742
+ limit: Optional[int] = Query(100, description="Maximum number of sources to return"),
743
+ order: Literal["asc", "desc"] = Query(
744
+ "desc", description="Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first"
745
+ ),
746
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
783
747
  ):
784
748
  """
785
749
  Get the sources associated with an agent.
786
750
  """
787
751
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
788
- return await server.agent_manager.list_attached_sources_async(agent_id=agent_id, actor=actor)
752
+ return await server.agent_manager.list_attached_sources_async(
753
+ agent_id=agent_id,
754
+ actor=actor,
755
+ before=before,
756
+ after=after,
757
+ limit=limit,
758
+ ascending=(order == "asc"),
759
+ )
789
760
 
790
761
 
791
762
  @router.get("/{agent_id}/folders", response_model=list[Source], operation_id="list_agent_folders")
@@ -793,19 +764,49 @@ async def list_agent_folders(
793
764
  agent_id: str,
794
765
  server: "SyncServer" = Depends(get_letta_server),
795
766
  headers: HeaderParams = Depends(get_headers),
767
+ before: Optional[str] = Query(
768
+ None, description="Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order"
769
+ ),
770
+ after: Optional[str] = Query(
771
+ None, description="Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order"
772
+ ),
773
+ limit: Optional[int] = Query(100, description="Maximum number of sources to return"),
774
+ order: Literal["asc", "desc"] = Query(
775
+ "desc", description="Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first"
776
+ ),
777
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
796
778
  ):
797
779
  """
798
780
  Get the folders associated with an agent.
799
781
  """
800
782
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
801
- return await server.agent_manager.list_attached_sources_async(agent_id=agent_id, actor=actor)
783
+ return await server.agent_manager.list_attached_sources_async(
784
+ agent_id=agent_id,
785
+ actor=actor,
786
+ before=before,
787
+ after=after,
788
+ limit=limit,
789
+ ascending=(order == "asc"),
790
+ )
802
791
 
803
792
 
804
793
  @router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_agent_files")
805
794
  async def list_agent_files(
806
795
  agent_id: str,
807
- cursor: Optional[str] = Query(None, description="Pagination cursor from previous response"),
808
- limit: int = Query(20, ge=1, le=100, description="Number of items to return (1-100)"),
796
+ before: Optional[str] = Query(
797
+ None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order"
798
+ ),
799
+ after: Optional[str] = Query(
800
+ None, description="File ID cursor for pagination. Returns files that come after this file ID in the specified sort order"
801
+ ),
802
+ limit: Optional[int] = Query(100, description="Maximum number of files to return"),
803
+ order: Literal["asc", "desc"] = Query(
804
+ "desc", description="Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first"
805
+ ),
806
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
807
+ cursor: Optional[str] = Query(
808
+ None, description="Pagination cursor from previous response (deprecated, use before/after)", deprecated=True
809
+ ),
809
810
  is_open: Optional[bool] = Query(None, description="Filter by open status (true for open files, false for closed files)"),
810
811
  server: "SyncServer" = Depends(get_letta_server),
811
812
  headers: HeaderParams = Depends(get_headers),
@@ -815,9 +816,18 @@ async def list_agent_files(
815
816
  """
816
817
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
817
818
 
819
+ effective_limit = limit or 20
820
+
818
821
  # get paginated file-agent relationships for this agent
819
822
  file_agents, next_cursor, has_more = await server.file_agent_manager.list_files_for_agent_paginated(
820
- agent_id=agent_id, actor=actor, cursor=cursor, limit=limit, is_open=is_open
823
+ agent_id=agent_id,
824
+ actor=actor,
825
+ cursor=cursor, # keep for backwards compatibility
826
+ limit=effective_limit,
827
+ is_open=is_open,
828
+ before=before,
829
+ after=after,
830
+ ascending=(order == "asc"),
821
831
  )
822
832
 
823
833
  # enrich with file and source metadata
@@ -872,10 +882,7 @@ async def retrieve_block(
872
882
  """
873
883
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
874
884
 
875
- try:
876
- return await server.agent_manager.get_block_with_label_async(agent_id=agent_id, block_label=block_label, actor=actor)
877
- except NoResultFound as e:
878
- raise HTTPException(status_code=404, detail=str(e))
885
+ return await server.agent_manager.get_block_with_label_async(agent_id=agent_id, block_label=block_label, actor=actor)
879
886
 
880
887
 
881
888
  @router.get("/{agent_id}/core-memory/blocks", response_model=list[Block], operation_id="list_core_memory_blocks")
@@ -883,16 +890,31 @@ async def list_blocks(
883
890
  agent_id: str,
884
891
  server: "SyncServer" = Depends(get_letta_server),
885
892
  headers: HeaderParams = Depends(get_headers),
893
+ before: Optional[str] = Query(
894
+ None, description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order"
895
+ ),
896
+ after: Optional[str] = Query(
897
+ None, description="Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order"
898
+ ),
899
+ limit: Optional[int] = Query(100, description="Maximum number of blocks to return"),
900
+ order: Literal["asc", "desc"] = Query(
901
+ "desc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first"
902
+ ),
903
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
886
904
  ):
887
905
  """
888
906
  Retrieve the core memory blocks of a specific agent.
889
907
  """
890
908
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
891
- try:
892
- agent = await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory"], actor=actor)
893
- return agent.memory.blocks
894
- except NoResultFound as e:
895
- raise HTTPException(status_code=404, detail=str(e))
909
+
910
+ return await server.agent_manager.list_agent_blocks_async(
911
+ agent_id=agent_id,
912
+ actor=actor,
913
+ before=before,
914
+ after=after,
915
+ limit=limit,
916
+ ascending=(order == "asc"),
917
+ )
896
918
 
897
919
 
898
920
  @router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block")
@@ -1015,34 +1037,26 @@ async def search_archival_memory(
1015
1037
  """
1016
1038
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1017
1039
 
1018
- try:
1019
- # convert datetime to string in ISO 8601 format
1020
- start_datetime = start_datetime.isoformat() if start_datetime else None
1021
- end_datetime = end_datetime.isoformat() if end_datetime else None
1040
+ # convert datetime to string in ISO 8601 format
1041
+ start_datetime = start_datetime.isoformat() if start_datetime else None
1042
+ end_datetime = end_datetime.isoformat() if end_datetime else None
1022
1043
 
1023
- # Use the shared agent manager method
1024
- formatted_results = await server.agent_manager.search_agent_archival_memory_async(
1025
- agent_id=agent_id,
1026
- actor=actor,
1027
- query=query,
1028
- tags=tags,
1029
- tag_match_mode=tag_match_mode,
1030
- top_k=top_k,
1031
- start_datetime=start_datetime,
1032
- end_datetime=end_datetime,
1033
- )
1034
-
1035
- # Convert to proper response schema
1036
- search_results = [ArchivalMemorySearchResult(**result) for result in formatted_results]
1044
+ # Use the shared agent manager method
1045
+ formatted_results = await server.agent_manager.search_agent_archival_memory_async(
1046
+ agent_id=agent_id,
1047
+ actor=actor,
1048
+ query=query,
1049
+ tags=tags,
1050
+ tag_match_mode=tag_match_mode,
1051
+ top_k=top_k,
1052
+ start_datetime=start_datetime,
1053
+ end_datetime=end_datetime,
1054
+ )
1037
1055
 
1038
- return ArchivalMemorySearchResponse(results=search_results, count=len(formatted_results))
1056
+ # Convert to proper response schema
1057
+ search_results = [ArchivalMemorySearchResult(**result) for result in formatted_results]
1039
1058
 
1040
- except NoResultFound as e:
1041
- raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
1042
- except ValueError as e:
1043
- raise HTTPException(status_code=400, detail=str(e))
1044
- except Exception as e:
1045
- raise HTTPException(status_code=500, detail=f"Internal server error during archival memory search: {str(e)}")
1059
+ return ArchivalMemorySearchResponse(results=search_results, count=len(formatted_results))
1046
1060
 
1047
1061
 
1048
1062
  # TODO(ethan): query or path parameter for memory_id?
@@ -1073,9 +1087,17 @@ AgentMessagesResponse = Annotated[
1073
1087
  async def list_messages(
1074
1088
  agent_id: str,
1075
1089
  server: "SyncServer" = Depends(get_letta_server),
1076
- after: str | None = Query(None, description="Message after which to retrieve the returned messages."),
1077
- before: str | None = Query(None, description="Message before which to retrieve the returned messages."),
1078
- limit: int = Query(10, description="Maximum number of messages to retrieve."),
1090
+ before: Optional[str] = Query(
1091
+ None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
1092
+ ),
1093
+ after: Optional[str] = Query(
1094
+ None, description="Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order"
1095
+ ),
1096
+ limit: Optional[int] = Query(100, description="Maximum number of messages to return"),
1097
+ order: Literal["asc", "desc"] = Query(
1098
+ "desc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
1099
+ ),
1100
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
1079
1101
  group_id: str | None = Query(None, description="Group ID to filter messages by."),
1080
1102
  use_assistant_message: bool = Query(True, description="Whether to use assistant messages"),
1081
1103
  assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool."),
@@ -1096,7 +1118,7 @@ async def list_messages(
1096
1118
  before=before,
1097
1119
  limit=limit,
1098
1120
  group_id=group_id,
1099
- reverse=True,
1121
+ reverse=(order == "desc"),
1100
1122
  return_message_object=False,
1101
1123
  use_assistant_message=use_assistant_message,
1102
1124
  assistant_message_tool_name=assistant_message_tool_name,
@@ -1107,7 +1129,7 @@ async def list_messages(
1107
1129
 
1108
1130
 
1109
1131
  @router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_message")
1110
- def modify_message(
1132
+ async def modify_message(
1111
1133
  agent_id: str,
1112
1134
  message_id: str,
1113
1135
  request: LettaMessageUpdateUnion = Body(...),
@@ -1118,8 +1140,10 @@ def modify_message(
1118
1140
  Update the details of a message associated with an agent.
1119
1141
  """
1120
1142
  # TODO: support modifying tool calls/returns
1121
- actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
1122
- return server.message_manager.update_message_by_letta_message(message_id=message_id, letta_message_update=request, actor=actor)
1143
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1144
+ return await server.message_manager.update_message_by_letta_message_async(
1145
+ message_id=message_id, letta_message_update=request, actor=actor
1146
+ )
1123
1147
 
1124
1148
 
1125
1149
  # noinspection PyInconsistentReturns
@@ -1166,32 +1190,26 @@ async def send_message(
1166
1190
 
1167
1191
  # Create a new run for execution tracking
1168
1192
  if settings.track_agent_run:
1169
- job_status = JobStatus.created
1170
- run = await server.job_manager.create_job_async(
1171
- pydantic_job=Run(
1172
- user_id=actor.id,
1173
- status=job_status,
1193
+ runs_manager = RunManager()
1194
+ run = await runs_manager.create_run(
1195
+ pydantic_run=PydanticRun(
1196
+ agent_id=agent_id,
1197
+ background=False,
1174
1198
  metadata={
1175
- "job_type": "send_message",
1176
- "agent_id": agent_id,
1199
+ "run_type": "send_message",
1177
1200
  },
1178
- request_config=LettaRequestConfig(
1179
- use_assistant_message=request.use_assistant_message,
1180
- assistant_message_tool_name=request.assistant_message_tool_name,
1181
- assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
1182
- include_return_message_types=request.include_return_message_types,
1183
- ),
1201
+ request_config=LettaRequestConfig.from_letta_request(request),
1184
1202
  ),
1185
1203
  actor=actor,
1186
1204
  )
1187
1205
  else:
1188
1206
  run = None
1189
1207
 
1190
- job_update_metadata = None
1191
1208
  # TODO (cliandy): clean this up
1192
1209
  redis_client = await get_redis_client()
1193
1210
  await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
1194
1211
 
1212
+ run_update_metadata = None
1195
1213
  try:
1196
1214
  result = None
1197
1215
  if agent_eligible and model_compatible:
@@ -1217,17 +1235,17 @@ async def send_message(
1217
1235
  assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
1218
1236
  include_return_message_types=request.include_return_message_types,
1219
1237
  )
1220
- job_status = result.stop_reason.stop_reason.run_status
1238
+ run_status = result.stop_reason.stop_reason.run_status
1221
1239
  return result
1222
1240
  except PendingApprovalError as e:
1223
- job_update_metadata = {"error": str(e)}
1224
- job_status = JobStatus.failed
1241
+ run_update_metadata = {"error": str(e)}
1242
+ run_status = RunStatus.failed
1225
1243
  raise HTTPException(
1226
1244
  status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
1227
1245
  )
1228
1246
  except Exception as e:
1229
- job_update_metadata = {"error": str(e)}
1230
- job_status = JobStatus.failed
1247
+ run_update_metadata = {"error": str(e)}
1248
+ run_status = RunStatus.failed
1231
1249
  raise
1232
1250
  finally:
1233
1251
  if settings.track_agent_run:
@@ -1236,12 +1254,14 @@ async def send_message(
1236
1254
  else:
1237
1255
  # NOTE: we could also consider this an error?
1238
1256
  stop_reason = None
1239
- await server.job_manager.safe_update_job_status_async(
1240
- job_id=run.id,
1241
- new_status=job_status,
1257
+ await server.run_manager.update_run_by_id_async(
1258
+ run_id=run.id,
1259
+ update=RunUpdate(
1260
+ status=run_status,
1261
+ metadata=run_update_metadata,
1262
+ stop_reason=stop_reason,
1263
+ ),
1242
1264
  actor=actor,
1243
- metadata=job_update_metadata,
1244
- stop_reason=stop_reason,
1245
1265
  )
1246
1266
 
1247
1267
 
@@ -1297,29 +1317,24 @@ async def send_message_streaming(
1297
1317
  "deepseek",
1298
1318
  ]
1299
1319
  model_compatible_token_streaming = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "bedrock", "deepseek"]
1320
+ if agent.agent_type == AgentType.letta_v1_agent and agent.llm_config.model_endpoint_type in ["google_ai", "google_vertex"]:
1321
+ model_compatible_token_streaming = True
1300
1322
 
1301
- # Create a new job for execution tracking
1323
+ # Create a new run for execution tracking
1302
1324
  if settings.track_agent_run:
1303
- job_status = JobStatus.created
1304
- run = await server.job_manager.create_job_async(
1305
- pydantic_job=Run(
1306
- user_id=actor.id,
1307
- status=job_status,
1325
+ runs_manager = RunManager()
1326
+ run = await runs_manager.create_run(
1327
+ pydantic_run=PydanticRun(
1328
+ agent_id=agent_id,
1329
+ background=request.background or False,
1308
1330
  metadata={
1309
- "job_type": "send_message_streaming",
1310
- "agent_id": agent_id,
1311
- "background": request.background or False,
1331
+ "run_type": "send_message_streaming",
1312
1332
  },
1313
- request_config=LettaRequestConfig(
1314
- use_assistant_message=request.use_assistant_message,
1315
- assistant_message_tool_name=request.assistant_message_tool_name,
1316
- assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
1317
- include_return_message_types=request.include_return_message_types,
1318
- ),
1333
+ request_config=LettaRequestConfig.from_letta_request(request),
1319
1334
  ),
1320
1335
  actor=actor,
1321
1336
  )
1322
- job_update_metadata = None
1337
+ run_update_metadata = None
1323
1338
  await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
1324
1339
  else:
1325
1340
  run = None
@@ -1345,6 +1360,16 @@ async def send_message_streaming(
1345
1360
  async for chunk in stream:
1346
1361
  yield chunk
1347
1362
 
1363
+ if run:
1364
+ runs_manager = RunManager()
1365
+ from letta.schemas.enums import RunStatus
1366
+
1367
+ await runs_manager.update_run_by_id_async(
1368
+ run_id=run.id,
1369
+ update=RunUpdate(status=RunStatus.completed, stop_reason=agent_loop.stop_reason.stop_reason.value),
1370
+ actor=actor,
1371
+ )
1372
+
1348
1373
  except LLMTimeoutError as e:
1349
1374
  error_data = {
1350
1375
  "error": {"type": "llm_timeout", "message": "The LLM request timed out. Please try again.", "detail": str(e)}
@@ -1395,7 +1420,7 @@ async def send_message_streaming(
1395
1420
  stream_generator=raw_stream,
1396
1421
  redis_client=redis_client,
1397
1422
  run_id=run.id,
1398
- job_manager=server.job_manager,
1423
+ run_manager=server.run_manager,
1399
1424
  actor=actor,
1400
1425
  ),
1401
1426
  label=f"background_stream_processor_{run.id}",
@@ -1431,24 +1456,24 @@ async def send_message_streaming(
1431
1456
  include_return_message_types=request.include_return_message_types,
1432
1457
  )
1433
1458
  if settings.track_agent_run:
1434
- job_status = JobStatus.running
1459
+ run_status = RunStatus.running
1435
1460
  return result
1436
1461
  except PendingApprovalError as e:
1437
1462
  if settings.track_agent_run:
1438
- job_update_metadata = {"error": str(e)}
1439
- job_status = JobStatus.failed
1463
+ run_update_metadata = {"error": str(e)}
1464
+ run_status = RunStatus.failed
1440
1465
  raise HTTPException(
1441
1466
  status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
1442
1467
  )
1443
1468
  except Exception as e:
1444
1469
  if settings.track_agent_run:
1445
- job_update_metadata = {"error": str(e)}
1446
- job_status = JobStatus.failed
1470
+ run_update_metadata = {"error": str(e)}
1471
+ run_status = RunStatus.failed
1447
1472
  raise
1448
1473
  finally:
1449
1474
  if settings.track_agent_run:
1450
- await server.job_manager.safe_update_job_status_async(
1451
- job_id=run.id, new_status=job_status, actor=actor, metadata=job_update_metadata
1475
+ await server.run_manager.update_run_by_id_async(
1476
+ run_id=run.id, update=RunUpdate(status=run_status, metadata=run_update_metadata), actor=actor
1452
1477
  )
1453
1478
 
1454
1479
 
@@ -1477,21 +1502,25 @@ async def cancel_agent_run(
1477
1502
  run_id = await redis_client.get(f"{REDIS_RUN_ID_PREFIX}:{agent_id}")
1478
1503
  if run_id is None:
1479
1504
  logger.warning("Cannot find run associated with agent to cancel in redis, fetching from db.")
1480
- job_ids = await server.job_manager.list_jobs_async(
1505
+ run_ids = await server.run_manager.list_runs(
1481
1506
  actor=actor,
1482
- statuses=[JobStatus.created, JobStatus.running],
1483
- job_type=JobType.RUN,
1507
+ statuses=[RunStatus.created, RunStatus.running],
1484
1508
  ascending=False,
1509
+ agent_id=agent_id, # NOTE: this will override agent_ids if provided
1485
1510
  )
1486
- run_ids = [Run.from_job(job).id for job in job_ids]
1511
+ run_ids = [run.id for run in run_ids]
1487
1512
  else:
1488
1513
  run_ids = [run_id]
1489
1514
 
1490
1515
  results = {}
1491
1516
  for run_id in run_ids:
1492
- success = await server.job_manager.safe_update_job_status_async(
1493
- job_id=run_id,
1494
- new_status=JobStatus.cancelled,
1517
+ run = await server.run_manager.get_run_by_id(run_id=run_id, actor=actor)
1518
+ if run.metadata.get("lettuce"):
1519
+ lettuce_client = await LettuceClient.create()
1520
+ await lettuce_client.cancel(run_id)
1521
+ success = await server.run_manager.update_run_by_id_async(
1522
+ run_id=run_id,
1523
+ update=RunUpdate(status=RunStatus.cancelled),
1495
1524
  actor=actor,
1496
1525
  )
1497
1526
  results[run_id] = "cancelled" if success else "failed"
@@ -1517,21 +1546,18 @@ async def search_messages(
1517
1546
  if agent_count == 0:
1518
1547
  raise HTTPException(status_code=400, detail="No agents found in organization to derive embedding configuration from")
1519
1548
 
1520
- try:
1521
- results = await server.message_manager.search_messages_org_async(
1522
- actor=actor,
1523
- query_text=request.query,
1524
- search_mode=request.search_mode,
1525
- roles=request.roles,
1526
- project_id=request.project_id,
1527
- template_id=request.template_id,
1528
- limit=request.limit,
1529
- start_date=request.start_date,
1530
- end_date=request.end_date,
1531
- )
1532
- return results
1533
- except ValueError as e:
1534
- raise HTTPException(status_code=400, detail=str(e))
1549
+ results = await server.message_manager.search_messages_org_async(
1550
+ actor=actor,
1551
+ query_text=request.query,
1552
+ search_mode=request.search_mode,
1553
+ roles=request.roles,
1554
+ project_id=request.project_id,
1555
+ template_id=request.template_id,
1556
+ limit=request.limit,
1557
+ start_date=request.start_date,
1558
+ end_date=request.end_date,
1559
+ )
1560
+ return results
1535
1561
 
1536
1562
 
1537
1563
  async def _process_message_background(
@@ -1546,7 +1572,7 @@ async def _process_message_background(
1546
1572
  max_steps: int = DEFAULT_MAX_STEPS,
1547
1573
  include_return_message_types: list[MessageType] | None = None,
1548
1574
  ) -> None:
1549
- """Background task to process the message and update job status."""
1575
+ """Background task to process the message and update run status."""
1550
1576
  request_start_timestamp_ns = get_utc_timestamp_ns()
1551
1577
  try:
1552
1578
  agent = await server.agent_manager.get_agent_by_id_async(
@@ -1583,7 +1609,7 @@ async def _process_message_background(
1583
1609
  input_messages=messages,
1584
1610
  stream_steps=False,
1585
1611
  stream_tokens=False,
1586
- metadata={"job_id": run_id},
1612
+ metadata={"run_id": run_id},
1587
1613
  # Support for AssistantMessage
1588
1614
  use_assistant_message=use_assistant_message,
1589
1615
  assistant_message_tool_name=assistant_message_tool_name,
@@ -1591,34 +1617,40 @@ async def _process_message_background(
1591
1617
  include_return_message_types=include_return_message_types,
1592
1618
  )
1593
1619
 
1594
- job_update = JobUpdate(
1595
- status=JobStatus.completed,
1596
- completed_at=datetime.now(timezone.utc),
1597
- metadata={"result": result.model_dump(mode="json")},
1620
+ runs_manager = RunManager()
1621
+ from letta.schemas.enums import RunStatus
1622
+
1623
+ await runs_manager.update_run_by_id_async(
1624
+ run_id=run_id,
1625
+ update=RunUpdate(status=RunStatus.completed, stop_reason=result.stop_reason.stop_reason),
1626
+ actor=actor,
1598
1627
  )
1599
- await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
1600
1628
 
1601
1629
  except PendingApprovalError as e:
1602
- # Update job status to failed with specific error info
1603
- job_update = JobUpdate(
1604
- status=JobStatus.failed,
1605
- completed_at=datetime.now(timezone.utc),
1606
- metadata={"error": str(e), "error_code": "PENDING_APPROVAL", "pending_request_id": e.pending_request_id},
1630
+ # Update run status to failed with specific error info
1631
+ runs_manager = RunManager()
1632
+ from letta.schemas.enums import RunStatus
1633
+
1634
+ await runs_manager.update_run_by_id_async(
1635
+ run_id=run_id,
1636
+ update=RunUpdate(status=RunStatus.failed),
1637
+ actor=actor,
1607
1638
  )
1608
- await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
1609
1639
  except Exception as e:
1610
- # Update job status to failed
1611
- job_update = JobUpdate(
1612
- status=JobStatus.failed,
1613
- completed_at=datetime.now(timezone.utc),
1614
- metadata={"error": str(e)},
1640
+ # Update run status to failed
1641
+ runs_manager = RunManager()
1642
+ from letta.schemas.enums import RunStatus
1643
+
1644
+ await runs_manager.update_run_by_id_async(
1645
+ run_id=run_id,
1646
+ update=RunUpdate(status=RunStatus.failed),
1647
+ actor=actor,
1615
1648
  )
1616
- await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
1617
1649
 
1618
1650
 
1619
1651
  @router.post(
1620
1652
  "/{agent_id}/messages/async",
1621
- response_model=Run,
1653
+ response_model=PydanticRun,
1622
1654
  operation_id="create_agent_message_async",
1623
1655
  )
1624
1656
  async def send_message_async(
@@ -1631,29 +1663,44 @@ async def send_message_async(
1631
1663
  Asynchronously process a user message and return a run object.
1632
1664
  The actual processing happens in the background, and the status can be checked using the run ID.
1633
1665
 
1634
- This is "asynchronous" in the sense that it's a background job and explicitly must be fetched by the run ID.
1635
- This is more like `send_message_job`
1666
+ This is "asynchronous" in the sense that it's a background run and explicitly must be fetched by the run ID.
1636
1667
  """
1637
1668
  MetricRegistry().user_message_counter.add(1, get_ctx_attributes())
1638
1669
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1639
-
1640
- # Create a new job
1641
- run = Run(
1642
- user_id=actor.id,
1643
- status=JobStatus.created,
1670
+ # Create a new run
1671
+ use_lettuce = headers.experimental_params.message_async
1672
+ run = PydanticRun(
1644
1673
  callback_url=request.callback_url,
1674
+ agent_id=agent_id,
1675
+ background=True, # Async endpoints are always background
1645
1676
  metadata={
1646
- "job_type": "send_message_async",
1647
- "agent_id": agent_id,
1677
+ "run_type": "send_message_async",
1678
+ "lettuce": use_lettuce,
1648
1679
  },
1649
- request_config=LettaRequestConfig(
1650
- use_assistant_message=request.use_assistant_message,
1651
- assistant_message_tool_name=request.assistant_message_tool_name,
1652
- assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
1653
- include_return_message_types=request.include_return_message_types,
1654
- ),
1680
+ request_config=LettaRequestConfig.from_letta_request(request),
1655
1681
  )
1656
- run = await server.job_manager.create_job_async(pydantic_job=run, actor=actor)
1682
+ run = await server.run_manager.create_run(
1683
+ pydantic_run=run,
1684
+ actor=actor,
1685
+ )
1686
+
1687
+ if use_lettuce:
1688
+ agent_state = await server.agent_manager.get_agent_by_id_async(
1689
+ agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
1690
+ )
1691
+ if agent_state.multi_agent_group is None and agent_state.agent_type != AgentType.letta_v1_agent:
1692
+ lettuce_client = await LettuceClient.create()
1693
+ run_id_from_lettuce = await lettuce_client.step(
1694
+ agent_state=agent_state,
1695
+ actor=actor,
1696
+ input_messages=request.messages,
1697
+ max_steps=request.max_steps,
1698
+ run_id=run.id,
1699
+ use_assistant_message=request.use_assistant_message,
1700
+ include_return_message_types=request.include_return_message_types,
1701
+ )
1702
+ if run_id_from_lettuce:
1703
+ return run
1657
1704
 
1658
1705
  # Create asyncio task for background processing (shielded to prevent cancellation)
1659
1706
  task = safe_create_shielded_task(
@@ -1681,17 +1728,21 @@ async def send_message_async(
1681
1728
  # Don't mark as failed since the shielded task is still running
1682
1729
  except Exception as e:
1683
1730
  logger.error(f"Unhandled exception in background task for run {run.id}: {e}")
1684
- safe_create_task(
1685
- server.job_manager.update_job_by_id_async(
1686
- job_id=run.id,
1687
- job_update=JobUpdate(
1688
- status=JobStatus.failed,
1689
- completed_at=datetime.now(timezone.utc),
1690
- metadata={"error": str(e)},
1691
- ),
1731
+ from letta.services.run_manager import RunManager
1732
+
1733
+ async def update_failed_run():
1734
+ runs_manager = RunManager()
1735
+ from letta.schemas.enums import RunStatus
1736
+
1737
+ await runs_manager.update_run_by_id_async(
1738
+ run_id=run.id,
1739
+ update=RunUpdate(status=RunStatus.failed),
1692
1740
  actor=actor,
1693
- ),
1694
- label=f"update_failed_job_{run.id}",
1741
+ )
1742
+
1743
+ safe_create_task(
1744
+ update_failed_run(),
1745
+ label=f"update_failed_run_{run.id}",
1695
1746
  )
1696
1747
 
1697
1748
  task.add_done_callback(handle_task_completion)
@@ -1719,11 +1770,30 @@ async def list_agent_groups(
1719
1770
  manager_type: str | None = Query(None, description="Manager type to filter groups by"),
1720
1771
  server: "SyncServer" = Depends(get_letta_server),
1721
1772
  headers: HeaderParams = Depends(get_headers),
1773
+ before: Optional[str] = Query(
1774
+ None, description="Group ID cursor for pagination. Returns groups that come before this group ID in the specified sort order"
1775
+ ),
1776
+ after: Optional[str] = Query(
1777
+ None, description="Group ID cursor for pagination. Returns groups that come after this group ID in the specified sort order"
1778
+ ),
1779
+ limit: Optional[int] = Query(100, description="Maximum number of groups to return"),
1780
+ order: Literal["asc", "desc"] = Query(
1781
+ "desc", description="Sort order for groups by creation time. 'asc' for oldest first, 'desc' for newest first"
1782
+ ),
1783
+ order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
1722
1784
  ):
1723
1785
  """Lists the groups for an agent"""
1724
1786
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1725
1787
  logger.info("in list agents with manager_type", manager_type)
1726
- return server.agent_manager.list_groups(agent_id=agent_id, manager_type=manager_type, actor=actor)
1788
+ return await server.agent_manager.list_groups_async(
1789
+ agent_id=agent_id,
1790
+ manager_type=manager_type,
1791
+ actor=actor,
1792
+ before=before,
1793
+ after=after,
1794
+ limit=limit,
1795
+ ascending=(order == "asc"),
1796
+ )
1727
1797
 
1728
1798
 
1729
1799
  @router.post(
@@ -1745,7 +1815,9 @@ async def preview_raw_payload(
1745
1815
  be sent to the LLM provider. Useful for debugging and inspection.
1746
1816
  """
1747
1817
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1748
- agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor, include_relationships=["multi_agent_group"])
1818
+ agent = await server.agent_manager.get_agent_by_id_async(
1819
+ agent_id, actor, include_relationships=["multi_agent_group", "memory", "sources"]
1820
+ )
1749
1821
  agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
1750
1822
  model_compatible = agent.llm_config.model_endpoint_type in [
1751
1823
  "anthropic",