letta-nightly 0.11.7.dev20250910104051__py3-none-any.whl → 0.11.7.dev20250911104039__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 (37) hide show
  1. letta/adapters/letta_llm_request_adapter.py +4 -2
  2. letta/adapters/letta_llm_stream_adapter.py +4 -2
  3. letta/agents/agent_loop.py +23 -0
  4. letta/agents/letta_agent_v2.py +5 -4
  5. letta/functions/helpers.py +3 -2
  6. letta/groups/sleeptime_multi_agent_v2.py +4 -2
  7. letta/groups/sleeptime_multi_agent_v3.py +4 -2
  8. letta/interfaces/anthropic_streaming_interface.py +10 -6
  9. letta/llm_api/google_vertex_client.py +1 -1
  10. letta/orm/agent.py +4 -1
  11. letta/orm/block.py +1 -0
  12. letta/orm/blocks_agents.py +1 -0
  13. letta/orm/sources_agents.py +2 -1
  14. letta/orm/tools_agents.py +5 -2
  15. letta/schemas/message.py +19 -2
  16. letta/server/rest_api/interface.py +34 -2
  17. letta/server/rest_api/json_parser.py +2 -0
  18. letta/server/rest_api/redis_stream_manager.py +2 -1
  19. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -2
  20. letta/server/rest_api/routers/v1/agents.py +47 -180
  21. letta/server/rest_api/routers/v1/folders.py +2 -2
  22. letta/server/rest_api/routers/v1/sources.py +2 -2
  23. letta/server/rest_api/streaming_response.py +2 -1
  24. letta/server/server.py +7 -5
  25. letta/services/agent_serialization_manager.py +4 -3
  26. letta/services/mcp_manager.py +2 -2
  27. letta/services/summarizer/summarizer.py +2 -1
  28. letta/services/tool_executor/multi_agent_tool_executor.py +17 -14
  29. letta/services/tool_sandbox/local_sandbox.py +2 -2
  30. letta/services/tool_sandbox/modal_version_manager.py +2 -1
  31. letta/streaming_utils.py +29 -4
  32. letta/utils.py +72 -3
  33. {letta_nightly-0.11.7.dev20250910104051.dist-info → letta_nightly-0.11.7.dev20250911104039.dist-info}/METADATA +1 -1
  34. {letta_nightly-0.11.7.dev20250910104051.dist-info → letta_nightly-0.11.7.dev20250911104039.dist-info}/RECORD +37 -36
  35. {letta_nightly-0.11.7.dev20250910104051.dist-info → letta_nightly-0.11.7.dev20250911104039.dist-info}/WHEEL +0 -0
  36. {letta_nightly-0.11.7.dev20250910104051.dist-info → letta_nightly-0.11.7.dev20250911104039.dist-info}/entry_points.txt +0 -0
  37. {letta_nightly-0.11.7.dev20250910104051.dist-info → letta_nightly-0.11.7.dev20250911104039.dist-info}/licenses/LICENSE +0 -0
@@ -12,7 +12,9 @@ from pydantic import BaseModel, Field
12
12
  from sqlalchemy.exc import IntegrityError, OperationalError
13
13
  from starlette.responses import Response, StreamingResponse
14
14
 
15
+ from letta.agents.agent_loop import AgentLoop
15
16
  from letta.agents.letta_agent import LettaAgent
17
+ from letta.agents.letta_agent_v2 import LettaAgentV2
16
18
  from letta.constants import AGENT_ID_PATTERN, DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
17
19
  from letta.data_sources.redis_client import NoopAsyncRedisClient, get_redis_client
18
20
  from letta.errors import (
@@ -58,7 +60,7 @@ from letta.server.server import SyncServer
58
60
  from letta.services.summarizer.enums import SummarizationMode
59
61
  from letta.services.telemetry_manager import NoopTelemetryManager
60
62
  from letta.settings import settings
61
- from letta.utils import safe_create_task, truncate_file_visible_content
63
+ from letta.utils import safe_create_shielded_task, safe_create_task, truncate_file_visible_content
62
64
 
63
65
  # These can be forward refs, but because Fastapi needs them at runtime the must be imported normally
64
66
 
@@ -1144,7 +1146,9 @@ async def send_message(
1144
1146
 
1145
1147
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
1146
1148
  # TODO: This is redundant, remove soon
1147
- agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor, include_relationships=["multi_agent_group"])
1149
+ agent = await server.agent_manager.get_agent_by_id_async(
1150
+ agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
1151
+ )
1148
1152
  agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
1149
1153
  model_compatible = agent.llm_config.model_endpoint_type in [
1150
1154
  "anthropic",
@@ -1190,42 +1194,11 @@ async def send_message(
1190
1194
 
1191
1195
  try:
1192
1196
  if agent_eligible and model_compatible:
1193
- if agent.enable_sleeptime and agent.agent_type != AgentType.voice_convo_agent:
1194
- agent_loop = SleeptimeMultiAgentV2(
1195
- agent_id=agent_id,
1196
- message_manager=server.message_manager,
1197
- agent_manager=server.agent_manager,
1198
- block_manager=server.block_manager,
1199
- passage_manager=server.passage_manager,
1200
- group_manager=server.group_manager,
1201
- job_manager=server.job_manager,
1202
- actor=actor,
1203
- group=agent.multi_agent_group,
1204
- current_run_id=run.id if run else None,
1205
- )
1206
- else:
1207
- agent_loop = LettaAgent(
1208
- agent_id=agent_id,
1209
- message_manager=server.message_manager,
1210
- agent_manager=server.agent_manager,
1211
- block_manager=server.block_manager,
1212
- job_manager=server.job_manager,
1213
- passage_manager=server.passage_manager,
1214
- actor=actor,
1215
- step_manager=server.step_manager,
1216
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
1217
- current_run_id=run.id if run else None,
1218
- # summarizer settings to be added here
1219
- summarizer_mode=(
1220
- SummarizationMode.STATIC_MESSAGE_BUFFER
1221
- if agent.agent_type == AgentType.voice_convo_agent
1222
- else SummarizationMode.PARTIAL_EVICT_MESSAGE_BUFFER
1223
- ),
1224
- )
1225
-
1197
+ agent_loop = AgentLoop.load(agent_state=agent, actor=actor)
1226
1198
  result = await agent_loop.step(
1227
1199
  request.messages,
1228
1200
  max_steps=request.max_steps,
1201
+ run_id=run.id if run else None,
1229
1202
  use_assistant_message=request.use_assistant_message,
1230
1203
  request_start_timestamp_ns=request_start_timestamp_ns,
1231
1204
  include_return_message_types=request.include_return_message_types,
@@ -1299,7 +1272,9 @@ async def send_message_streaming(
1299
1272
 
1300
1273
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
1301
1274
  # TODO: This is redundant, remove soon
1302
- agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor, include_relationships=["multi_agent_group"])
1275
+ agent = await server.agent_manager.get_agent_by_id_async(
1276
+ agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
1277
+ )
1303
1278
  agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
1304
1279
  model_compatible = agent.llm_config.model_endpoint_type in [
1305
1280
  "anthropic",
@@ -1344,57 +1319,16 @@ async def send_message_streaming(
1344
1319
 
1345
1320
  try:
1346
1321
  if agent_eligible and model_compatible:
1347
- if agent.enable_sleeptime and agent.agent_type != AgentType.voice_convo_agent:
1348
- agent_loop = SleeptimeMultiAgentV2(
1349
- agent_id=agent_id,
1350
- message_manager=server.message_manager,
1351
- agent_manager=server.agent_manager,
1352
- block_manager=server.block_manager,
1353
- passage_manager=server.passage_manager,
1354
- group_manager=server.group_manager,
1355
- job_manager=server.job_manager,
1356
- actor=actor,
1357
- step_manager=server.step_manager,
1358
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
1359
- group=agent.multi_agent_group,
1360
- current_run_id=run.id if run else None,
1361
- )
1362
- else:
1363
- agent_loop = LettaAgent(
1364
- agent_id=agent_id,
1365
- message_manager=server.message_manager,
1366
- agent_manager=server.agent_manager,
1367
- block_manager=server.block_manager,
1368
- job_manager=server.job_manager,
1369
- passage_manager=server.passage_manager,
1370
- actor=actor,
1371
- step_manager=server.step_manager,
1372
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
1373
- current_run_id=run.id if run else None,
1374
- # summarizer settings to be added here
1375
- summarizer_mode=(
1376
- SummarizationMode.STATIC_MESSAGE_BUFFER
1377
- if agent.agent_type == AgentType.voice_convo_agent
1378
- else SummarizationMode.PARTIAL_EVICT_MESSAGE_BUFFER
1379
- ),
1380
- )
1381
-
1382
- if request.stream_tokens and model_compatible_token_streaming:
1383
- raw_stream = agent_loop.step_stream(
1384
- input_messages=request.messages,
1385
- max_steps=request.max_steps,
1386
- use_assistant_message=request.use_assistant_message,
1387
- request_start_timestamp_ns=request_start_timestamp_ns,
1388
- include_return_message_types=request.include_return_message_types,
1389
- )
1390
- else:
1391
- raw_stream = agent_loop.step_stream_no_tokens(
1392
- request.messages,
1393
- max_steps=request.max_steps,
1394
- use_assistant_message=request.use_assistant_message,
1395
- request_start_timestamp_ns=request_start_timestamp_ns,
1396
- include_return_message_types=request.include_return_message_types,
1397
- )
1322
+ agent_loop = AgentLoop.load(agent_state=agent, actor=actor)
1323
+ raw_stream = agent_loop.stream(
1324
+ input_messages=request.messages,
1325
+ max_steps=request.max_steps,
1326
+ stream_tokens=request.stream_tokens and model_compatible_token_streaming,
1327
+ run_id=run.id if run else None,
1328
+ use_assistant_message=request.use_assistant_message,
1329
+ request_start_timestamp_ns=request_start_timestamp_ns,
1330
+ include_return_message_types=request.include_return_message_types,
1331
+ )
1398
1332
 
1399
1333
  from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode, add_keepalive_to_stream
1400
1334
 
@@ -1409,12 +1343,13 @@ async def send_message_streaming(
1409
1343
  ),
1410
1344
  )
1411
1345
 
1412
- asyncio.create_task(
1346
+ safe_create_task(
1413
1347
  create_background_stream_processor(
1414
1348
  stream_generator=raw_stream,
1415
1349
  redis_client=redis_client,
1416
1350
  run_id=run.id,
1417
- )
1351
+ ),
1352
+ label=f"background_stream_processor_{run.id}",
1418
1353
  )
1419
1354
 
1420
1355
  raw_stream = redis_sse_stream_generator(
@@ -1568,7 +1503,9 @@ async def _process_message_background(
1568
1503
  """Background task to process the message and update job status."""
1569
1504
  request_start_timestamp_ns = get_utc_timestamp_ns()
1570
1505
  try:
1571
- agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor, include_relationships=["multi_agent_group"])
1506
+ agent = await server.agent_manager.get_agent_by_id_async(
1507
+ agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
1508
+ )
1572
1509
  agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
1573
1510
  model_compatible = agent.llm_config.model_endpoint_type in [
1574
1511
  "anthropic",
@@ -1584,37 +1521,7 @@ async def _process_message_background(
1584
1521
  "deepseek",
1585
1522
  ]
1586
1523
  if agent_eligible and model_compatible:
1587
- if agent.enable_sleeptime and agent.agent_type != AgentType.voice_convo_agent:
1588
- agent_loop = SleeptimeMultiAgentV2(
1589
- agent_id=agent_id,
1590
- message_manager=server.message_manager,
1591
- agent_manager=server.agent_manager,
1592
- block_manager=server.block_manager,
1593
- passage_manager=server.passage_manager,
1594
- group_manager=server.group_manager,
1595
- job_manager=server.job_manager,
1596
- actor=actor,
1597
- group=agent.multi_agent_group,
1598
- )
1599
- else:
1600
- agent_loop = LettaAgent(
1601
- agent_id=agent_id,
1602
- message_manager=server.message_manager,
1603
- agent_manager=server.agent_manager,
1604
- block_manager=server.block_manager,
1605
- job_manager=server.job_manager,
1606
- passage_manager=server.passage_manager,
1607
- actor=actor,
1608
- step_manager=server.step_manager,
1609
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
1610
- # summarizer settings to be added here
1611
- summarizer_mode=(
1612
- SummarizationMode.STATIC_MESSAGE_BUFFER
1613
- if agent.agent_type == AgentType.voice_convo_agent
1614
- else SummarizationMode.PARTIAL_EVICT_MESSAGE_BUFFER
1615
- ),
1616
- )
1617
-
1524
+ agent_loop = AgentLoop.load(agent_state=agent, actor=actor)
1618
1525
  result = await agent_loop.step(
1619
1526
  messages,
1620
1527
  max_steps=max_steps,
@@ -1702,8 +1609,8 @@ async def send_message_async(
1702
1609
  )
1703
1610
  run = await server.job_manager.create_job_async(pydantic_job=run, actor=actor)
1704
1611
 
1705
- # Create asyncio task for background processing
1706
- task = asyncio.create_task(
1612
+ # Create asyncio task for background processing (shielded to prevent cancellation)
1613
+ task = safe_create_shielded_task(
1707
1614
  _process_message_background(
1708
1615
  run_id=run.id,
1709
1616
  server=server,
@@ -1715,28 +1622,20 @@ async def send_message_async(
1715
1622
  assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
1716
1623
  max_steps=request.max_steps,
1717
1624
  include_return_message_types=request.include_return_message_types,
1718
- )
1625
+ ),
1626
+ label=f"process_message_background_{run.id}",
1719
1627
  )
1720
1628
 
1721
1629
  def handle_task_completion(t):
1722
1630
  try:
1723
1631
  t.result()
1724
1632
  except asyncio.CancelledError:
1725
- logger.error(f"Background task for run {run.id} was cancelled")
1726
- asyncio.create_task(
1727
- server.job_manager.update_job_by_id_async(
1728
- job_id=run.id,
1729
- job_update=JobUpdate(
1730
- status=JobStatus.failed,
1731
- completed_at=datetime.now(timezone.utc),
1732
- metadata={"error": "Task was cancelled"},
1733
- ),
1734
- actor=actor,
1735
- )
1736
- )
1633
+ # Note: With shielded tasks, cancellation attempts don't actually stop the task
1634
+ logger.info(f"Cancellation attempted on shielded background task for run {run.id}, but task continues running")
1635
+ # Don't mark as failed since the shielded task is still running
1737
1636
  except Exception as e:
1738
1637
  logger.error(f"Unhandled exception in background task for run {run.id}: {e}")
1739
- asyncio.create_task(
1638
+ safe_create_task(
1740
1639
  server.job_manager.update_job_by_id_async(
1741
1640
  job_id=run.id,
1742
1641
  job_update=JobUpdate(
@@ -1745,7 +1644,8 @@ async def send_message_async(
1745
1644
  metadata={"error": str(e)},
1746
1645
  ),
1747
1646
  actor=actor,
1748
- )
1647
+ ),
1648
+ label=f"update_failed_job_{run.id}",
1749
1649
  )
1750
1650
 
1751
1651
  task.add_done_callback(handle_task_completion)
@@ -1816,38 +1716,10 @@ async def preview_raw_payload(
1816
1716
  ]
1817
1717
 
1818
1718
  if agent_eligible and model_compatible:
1819
- if agent.enable_sleeptime:
1820
- # TODO: @caren need to support this for sleeptime
1821
- raise HTTPException(
1822
- status_code=status.HTTP_400_BAD_REQUEST,
1823
- detail="Payload inspection is not supported for agents with sleeptime enabled.",
1824
- )
1825
- else:
1826
- agent_loop = LettaAgent(
1827
- agent_id=agent_id,
1828
- message_manager=server.message_manager,
1829
- agent_manager=server.agent_manager,
1830
- block_manager=server.block_manager,
1831
- job_manager=server.job_manager,
1832
- passage_manager=server.passage_manager,
1833
- actor=actor,
1834
- step_manager=server.step_manager,
1835
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
1836
- summarizer_mode=(
1837
- SummarizationMode.STATIC_MESSAGE_BUFFER
1838
- if agent.agent_type == AgentType.voice_convo_agent
1839
- else SummarizationMode.PARTIAL_EVICT_MESSAGE_BUFFER
1840
- ),
1841
- )
1842
-
1843
- # TODO: Support step_streaming
1844
- return await agent_loop.step(
1719
+ agent_loop = AgentLoop.load(agent_state=agent, actor=actor)
1720
+ return await agent_loop.build_request(
1845
1721
  input_messages=request.messages,
1846
- use_assistant_message=request.use_assistant_message,
1847
- include_return_message_types=request.include_return_message_types,
1848
- dry_run=True,
1849
1722
  )
1850
-
1851
1723
  else:
1852
1724
  raise HTTPException(
1853
1725
  status_code=status.HTTP_403_FORBIDDEN,
@@ -1888,19 +1760,14 @@ async def summarize_agent_conversation(
1888
1760
  ]
1889
1761
 
1890
1762
  if agent_eligible and model_compatible:
1891
- agent = LettaAgent(
1892
- agent_id=agent_id,
1893
- message_manager=server.message_manager,
1894
- agent_manager=server.agent_manager,
1895
- block_manager=server.block_manager,
1896
- job_manager=server.job_manager,
1897
- passage_manager=server.passage_manager,
1898
- actor=actor,
1899
- step_manager=server.step_manager,
1900
- telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
1901
- message_buffer_min=max_message_length,
1763
+ agent_loop = LettaAgentV2(agent_state=agent, actor=actor)
1764
+ in_context_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor)
1765
+ await agent_loop.summarize_conversation_history(
1766
+ in_context_messages=in_context_messages,
1767
+ new_letta_messages=[],
1768
+ total_tokens=None,
1769
+ force=True,
1902
1770
  )
1903
- await agent.summarize_conversation_history()
1904
1771
  # Summarization completed, return 204 No Content
1905
1772
  else:
1906
1773
  raise HTTPException(
@@ -327,7 +327,7 @@ async def upload_file_to_folder(
327
327
  logger=logger,
328
328
  label="file_processor.process",
329
329
  )
330
- safe_create_task(sleeptime_document_ingest_async(server, folder_id, actor), logger=logger, label="sleeptime_document_ingest_async")
330
+ safe_create_task(sleeptime_document_ingest_async(server, folder_id, actor), label="sleeptime_document_ingest_async")
331
331
 
332
332
  return file_metadata
333
333
 
@@ -467,7 +467,7 @@ async def delete_file_from_folder(
467
467
  logger.info(f"Deleting file {file_id} from pinecone index")
468
468
  await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
469
469
 
470
- asyncio.create_task(sleeptime_document_ingest_async(server, folder_id, actor, clear_history=True))
470
+ safe_create_task(sleeptime_document_ingest_async(server, folder_id, actor, clear_history=True), label="document_ingest_after_delete")
471
471
  if deleted_file is None:
472
472
  raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
473
473
 
@@ -325,7 +325,7 @@ async def upload_file_to_source(
325
325
  logger=logger,
326
326
  label="file_processor.process",
327
327
  )
328
- safe_create_task(sleeptime_document_ingest_async(server, source_id, actor), logger=logger, label="sleeptime_document_ingest_async")
328
+ safe_create_task(sleeptime_document_ingest_async(server, source_id, actor), label="sleeptime_document_ingest_async")
329
329
 
330
330
  return file_metadata
331
331
 
@@ -452,7 +452,7 @@ async def delete_file_from_source(
452
452
  logger.info(f"Deleting file {file_id} from pinecone index")
453
453
  await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
454
454
 
455
- asyncio.create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True))
455
+ safe_create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True), label="document_ingest_after_delete")
456
456
  if deleted_file is None:
457
457
  raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
458
458
 
@@ -19,6 +19,7 @@ from letta.schemas.user import User
19
19
  from letta.server.rest_api.utils import capture_sentry_exception
20
20
  from letta.services.job_manager import JobManager
21
21
  from letta.settings import settings
22
+ from letta.utils import safe_create_task
22
23
 
23
24
  logger = get_logger(__name__)
24
25
 
@@ -64,7 +65,7 @@ async def add_keepalive_to_stream(
64
65
  await queue.put(("end", None))
65
66
 
66
67
  # Start the stream reader task
67
- reader_task = asyncio.create_task(stream_reader())
68
+ reader_task = safe_create_task(stream_reader(), label="stream_reader")
68
69
 
69
70
  try:
70
71
  while True:
letta/server/server.py CHANGED
@@ -109,7 +109,7 @@ from letta.services.tool_manager import ToolManager
109
109
  from letta.services.user_manager import UserManager
110
110
  from letta.settings import DatabaseChoice, model_settings, settings, tool_settings
111
111
  from letta.streaming_interface import AgentChunkStreamingInterface
112
- from letta.utils import get_friendly_error_msg, get_persona_text, make_key
112
+ from letta.utils import get_friendly_error_msg, get_persona_text, make_key, safe_create_task
113
113
 
114
114
  config = LettaConfig.load()
115
115
  logger = get_logger(__name__)
@@ -2248,7 +2248,7 @@ class SyncServer(Server):
2248
2248
 
2249
2249
  # Offload the synchronous message_func to a separate thread
2250
2250
  streaming_interface.stream_start()
2251
- task = asyncio.create_task(
2251
+ task = safe_create_task(
2252
2252
  asyncio.to_thread(
2253
2253
  self.send_messages,
2254
2254
  actor=actor,
@@ -2256,7 +2256,8 @@ class SyncServer(Server):
2256
2256
  input_messages=input_messages,
2257
2257
  interface=streaming_interface,
2258
2258
  metadata=metadata,
2259
- )
2259
+ ),
2260
+ label="send_messages_thread",
2260
2261
  )
2261
2262
 
2262
2263
  if stream_steps:
@@ -2363,13 +2364,14 @@ class SyncServer(Server):
2363
2364
  streaming_interface.metadata = metadata
2364
2365
 
2365
2366
  streaming_interface.stream_start()
2366
- task = asyncio.create_task(
2367
+ task = safe_create_task(
2367
2368
  asyncio.to_thread(
2368
2369
  letta_multi_agent.step,
2369
2370
  input_messages=input_messages,
2370
2371
  chaining=self.chaining,
2371
2372
  max_chaining_steps=self.max_chaining_steps,
2372
- )
2373
+ ),
2374
+ label="multi_agent_step_thread",
2373
2375
  )
2374
2376
 
2375
2377
  if stream_steps:
@@ -53,7 +53,7 @@ from letta.services.message_manager import MessageManager
53
53
  from letta.services.source_manager import SourceManager
54
54
  from letta.services.tool_manager import ToolManager
55
55
  from letta.settings import settings
56
- from letta.utils import get_latest_alembic_revision
56
+ from letta.utils import get_latest_alembic_revision, safe_create_task
57
57
 
58
58
  logger = get_logger(__name__)
59
59
 
@@ -622,10 +622,11 @@ class AgentSerializationManager:
622
622
 
623
623
  # Create background task for file processing
624
624
  # TODO: This can be moved to celery or RQ or something
625
- task = asyncio.create_task(
625
+ task = safe_create_task(
626
626
  self._process_file_async(
627
627
  file_metadata=file_metadata, source_id=source_db_id, file_processor=file_processor, actor=actor
628
- )
628
+ ),
629
+ label=f"process_file_{file_metadata.file_name}",
629
630
  )
630
631
  background_tasks.append(task)
631
632
  logger.info(f"Started background processing for file {file_metadata.file_name} (ID: {file_db_id})")
@@ -43,7 +43,7 @@ from letta.services.mcp.stdio_client import AsyncStdioMCPClient
43
43
  from letta.services.mcp.streamable_http_client import AsyncStreamableHTTPMCPClient
44
44
  from letta.services.tool_manager import ToolManager
45
45
  from letta.settings import tool_settings
46
- from letta.utils import enforce_types, printd
46
+ from letta.utils import enforce_types, printd, safe_create_task
47
47
 
48
48
  logger = get_logger(__name__)
49
49
 
@@ -869,7 +869,7 @@ class MCPManager:
869
869
 
870
870
  # Run connect_to_server in background to avoid blocking
871
871
  # This will trigger the OAuth flow and the redirect_handler will save the authorization URL to database
872
- connect_task = asyncio.create_task(temp_client.connect_to_server())
872
+ connect_task = safe_create_task(temp_client.connect_to_server(), label="mcp_oauth_connect")
873
873
 
874
874
  # Give the OAuth flow time to trigger and save the URL
875
875
  await asyncio.sleep(1.0)
@@ -20,6 +20,7 @@ from letta.services.message_manager import MessageManager
20
20
  from letta.services.summarizer.enums import SummarizationMode
21
21
  from letta.system import package_summarize_message_no_counts
22
22
  from letta.templates.template_helper import render_template
23
+ from letta.utils import safe_create_task
23
24
 
24
25
  logger = get_logger(__name__)
25
26
 
@@ -100,7 +101,7 @@ class Summarizer:
100
101
  return in_context_messages, False
101
102
 
102
103
  def fire_and_forget(self, coro):
103
- task = asyncio.create_task(coro)
104
+ task = safe_create_task(coro, label="summarizer_background_task")
104
105
 
105
106
  def callback(t):
106
107
  try:
@@ -13,6 +13,7 @@ from letta.schemas.tool_execution_result import ToolExecutionResult
13
13
  from letta.schemas.user import User
14
14
  from letta.services.tool_executor.tool_executor_base import ToolExecutor
15
15
  from letta.settings import settings
16
+ from letta.utils import safe_create_task
16
17
 
17
18
  logger = get_logger(__name__)
18
19
 
@@ -55,7 +56,8 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
55
56
  f"{message}"
56
57
  )
57
58
 
58
- return str(await self._process_agent(agent_id=other_agent_id, message=augmented_message))
59
+ other_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=other_agent_id, actor=self.actor)
60
+ return str(await self._process_agent(agent_state=other_agent_state, message=augmented_message))
59
61
 
60
62
  async def send_message_to_agents_matching_tags_async(
61
63
  self, agent_state: AgentState, message: str, match_all: List[str], match_some: List[str]
@@ -75,22 +77,20 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
75
77
  )
76
78
 
77
79
  tasks = [
78
- asyncio.create_task(self._process_agent(agent_id=agent_state.id, message=augmented_message)) for agent_state in matching_agents
80
+ safe_create_task(
81
+ self._process_agent(agent_state=agent_state, message=augmented_message), label=f"process_agent_{agent_state.id}"
82
+ )
83
+ for agent_state in matching_agents
79
84
  ]
80
85
  results = await asyncio.gather(*tasks)
81
86
  return str(results)
82
87
 
83
- async def _process_agent(self, agent_id: str, message: str) -> Dict[str, Any]:
84
- from letta.agents.letta_agent import LettaAgent
88
+ async def _process_agent(self, agent_state: AgentState, message: str) -> Dict[str, Any]:
89
+ from letta.agents.letta_agent_v2 import LettaAgentV2
85
90
 
86
91
  try:
87
- letta_agent = LettaAgent(
88
- agent_id=agent_id,
89
- message_manager=self.message_manager,
90
- agent_manager=self.agent_manager,
91
- block_manager=self.block_manager,
92
- job_manager=self.job_manager,
93
- passage_manager=self.passage_manager,
92
+ letta_agent = LettaAgentV2(
93
+ agent_state=agent_state,
94
94
  actor=self.actor,
95
95
  )
96
96
 
@@ -100,13 +100,13 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
100
100
  send_message_content = [message.content for message in messages if isinstance(message, AssistantMessage)]
101
101
 
102
102
  return {
103
- "agent_id": agent_id,
103
+ "agent_id": agent_state.id,
104
104
  "response": send_message_content if send_message_content else ["<no response>"],
105
105
  }
106
106
 
107
107
  except Exception as e:
108
108
  return {
109
- "agent_id": agent_id,
109
+ "agent_id": agent_state.id,
110
110
  "error": str(e),
111
111
  "type": type(e).__name__,
112
112
  }
@@ -123,7 +123,10 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
123
123
  f"{message}"
124
124
  )
125
125
 
126
- task = asyncio.create_task(self._process_agent(agent_id=other_agent_id, message=prefixed))
126
+ other_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=other_agent_id, actor=self.actor)
127
+ task = safe_create_task(
128
+ self._process_agent(agent_state=other_agent_state, message=prefixed), label=f"send_message_to_{other_agent_id}"
129
+ )
127
130
 
128
131
  task.add_done_callback(lambda t: (logger.error(f"Async send_message task failed: {t.exception()}") if t.exception() else None))
129
132
 
@@ -23,7 +23,7 @@ from letta.services.helpers.tool_execution_helper import (
23
23
  from letta.services.helpers.tool_parser_helper import parse_stdout_best_effort
24
24
  from letta.services.tool_sandbox.base import AsyncToolSandboxBase
25
25
  from letta.settings import tool_settings
26
- from letta.utils import get_friendly_error_msg, parse_stderr_error_msg
26
+ from letta.utils import get_friendly_error_msg, parse_stderr_error_msg, safe_create_task
27
27
 
28
28
  logger = get_logger(__name__)
29
29
 
@@ -89,7 +89,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
89
89
  venv_preparation_task = None
90
90
  if use_venv:
91
91
  venv_path = str(os.path.join(sandbox_dir, local_configs.venv_name))
92
- venv_preparation_task = asyncio.create_task(self._prepare_venv(local_configs, venv_path, env))
92
+ venv_preparation_task = safe_create_task(self._prepare_venv(local_configs, venv_path, env), label="prepare_venv")
93
93
 
94
94
  # Generate and write execution script (always with markers, since we rely on stdout)
95
95
  code = await self.generate_execution_script(agent_state=agent_state, wrap_print_with_markers=True)
@@ -16,6 +16,7 @@ from letta.log import get_logger
16
16
  from letta.schemas.tool import ToolUpdate
17
17
  from letta.services.tool_manager import ToolManager
18
18
  from letta.services.tool_sandbox.modal_constants import CACHE_TTL_SECONDS, DEFAULT_CONFIG_KEY, MODAL_DEPLOYMENTS_KEY
19
+ from letta.utils import safe_create_task
19
20
 
20
21
  logger = get_logger(__name__)
21
22
 
@@ -197,7 +198,7 @@ class ModalVersionManager:
197
198
  if deployment_key in self._deployments_in_progress:
198
199
  self._deployments_in_progress[deployment_key].set()
199
200
  # Clean up after a short delay to allow waiters to wake up
200
- asyncio.create_task(self._cleanup_deployment_marker(deployment_key))
201
+ safe_create_task(self._cleanup_deployment_marker(deployment_key), label=f"cleanup_deployment_{deployment_key}")
201
202
 
202
203
  async def _cleanup_deployment_marker(self, deployment_key: str):
203
204
  """Clean up deployment marker after a delay."""