letta-nightly 0.8.13.dev20250713104250__py3-none-any.whl → 0.8.14.dev20250714180504__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (31) hide show
  1. letta/__init__.py +1 -1
  2. letta/constants.py +6 -0
  3. letta/functions/function_sets/base.py +2 -2
  4. letta/helpers/pinecone_utils.py +164 -11
  5. letta/orm/file.py +2 -17
  6. letta/orm/files_agents.py +9 -10
  7. letta/orm/organization.py +0 -4
  8. letta/orm/passage.py +0 -10
  9. letta/orm/source.py +3 -20
  10. letta/schemas/file.py +1 -0
  11. letta/schemas/memory.py +2 -2
  12. letta/server/rest_api/routers/v1/agents.py +4 -4
  13. letta/server/rest_api/routers/v1/messages.py +2 -6
  14. letta/server/rest_api/routers/v1/sources.py +3 -3
  15. letta/server/server.py +0 -3
  16. letta/services/agent_manager.py +194 -147
  17. letta/services/block_manager.py +18 -18
  18. letta/services/context_window_calculator/context_window_calculator.py +15 -10
  19. letta/services/context_window_calculator/token_counter.py +40 -0
  20. letta/services/file_processor/chunker/line_chunker.py +17 -0
  21. letta/services/file_processor/embedder/openai_embedder.py +50 -5
  22. letta/services/files_agents_manager.py +12 -2
  23. letta/services/group_manager.py +11 -11
  24. letta/services/source_manager.py +19 -3
  25. letta/services/tool_executor/core_tool_executor.py +2 -2
  26. letta/services/tool_executor/files_tool_executor.py +6 -1
  27. {letta_nightly-0.8.13.dev20250713104250.dist-info → letta_nightly-0.8.14.dev20250714180504.dist-info}/METADATA +1 -1
  28. {letta_nightly-0.8.13.dev20250713104250.dist-info → letta_nightly-0.8.14.dev20250714180504.dist-info}/RECORD +31 -31
  29. {letta_nightly-0.8.13.dev20250713104250.dist-info → letta_nightly-0.8.14.dev20250714180504.dist-info}/LICENSE +0 -0
  30. {letta_nightly-0.8.13.dev20250713104250.dist-info → letta_nightly-0.8.14.dev20250714180504.dist-info}/WHEEL +0 -0
  31. {letta_nightly-0.8.13.dev20250713104250.dist-info → letta_nightly-0.8.14.dev20250714180504.dist-info}/entry_points.txt +0 -0
@@ -107,6 +107,32 @@ class AgentManager:
107
107
  self.identity_manager = IdentityManager()
108
108
  self.file_agent_manager = FileAgentManager()
109
109
 
110
+ @trace_method
111
+ async def _validate_agent_exists_async(self, session, agent_id: str, actor: PydanticUser) -> None:
112
+ """
113
+ Validate that an agent exists and user has access to it using raw SQL for efficiency.
114
+
115
+ Args:
116
+ session: Database session
117
+ agent_id: ID of the agent to validate
118
+ actor: User performing the action
119
+
120
+ Raises:
121
+ NoResultFound: If agent doesn't exist or user doesn't have access
122
+ """
123
+ agent_check_query = sa.text(
124
+ """
125
+ SELECT 1 FROM agents
126
+ WHERE id = :agent_id
127
+ AND organization_id = :org_id
128
+ AND is_deleted = false
129
+ """
130
+ )
131
+ agent_exists = await session.execute(agent_check_query, {"agent_id": agent_id, "org_id": actor.organization_id})
132
+
133
+ if not agent_exists.fetchone():
134
+ raise NoResultFound(f"Agent with ID {agent_id} not found")
135
+
110
136
  @staticmethod
111
137
  def _resolve_tools(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
112
138
  """
@@ -635,24 +661,24 @@ class AgentManager:
635
661
 
636
662
  return init_messages
637
663
 
638
- @trace_method
639
664
  @enforce_types
665
+ @trace_method
640
666
  def append_initial_message_sequence_to_in_context_messages(
641
667
  self, actor: PydanticUser, agent_state: PydanticAgentState, initial_message_sequence: Optional[List[MessageCreate]] = None
642
668
  ) -> PydanticAgentState:
643
669
  init_messages = self._generate_initial_message_sequence(actor, agent_state, initial_message_sequence)
644
670
  return self.append_to_in_context_messages(init_messages, agent_id=agent_state.id, actor=actor)
645
671
 
646
- @trace_method
647
672
  @enforce_types
673
+ @trace_method
648
674
  async def append_initial_message_sequence_to_in_context_messages_async(
649
675
  self, actor: PydanticUser, agent_state: PydanticAgentState, initial_message_sequence: Optional[List[MessageCreate]] = None
650
676
  ) -> PydanticAgentState:
651
677
  init_messages = self._generate_initial_message_sequence(actor, agent_state, initial_message_sequence)
652
678
  return await self.append_to_in_context_messages_async(init_messages, agent_id=agent_state.id, actor=actor)
653
679
 
654
- @trace_method
655
680
  @enforce_types
681
+ @trace_method
656
682
  def update_agent(
657
683
  self,
658
684
  agent_id: str,
@@ -773,8 +799,8 @@ class AgentManager:
773
799
 
774
800
  return agent.to_pydantic()
775
801
 
776
- @trace_method
777
802
  @enforce_types
803
+ @trace_method
778
804
  async def update_agent_async(
779
805
  self,
780
806
  agent_id: str,
@@ -1125,16 +1151,16 @@ class AgentManager:
1125
1151
  async with db_registry.async_session() as session:
1126
1152
  return await AgentModel.size_async(db_session=session, actor=actor)
1127
1153
 
1128
- @trace_method
1129
1154
  @enforce_types
1155
+ @trace_method
1130
1156
  def get_agent_by_id(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1131
1157
  """Fetch an agent by its ID."""
1132
1158
  with db_registry.session() as session:
1133
1159
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1134
1160
  return agent.to_pydantic()
1135
1161
 
1136
- @trace_method
1137
1162
  @enforce_types
1163
+ @trace_method
1138
1164
  async def get_agent_by_id_async(
1139
1165
  self,
1140
1166
  agent_id: str,
@@ -1147,8 +1173,8 @@ class AgentManager:
1147
1173
  agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1148
1174
  return await agent.to_pydantic_async(include_relationships=include_relationships)
1149
1175
 
1150
- @trace_method
1151
1176
  @enforce_types
1177
+ @trace_method
1152
1178
  async def get_agents_by_ids_async(
1153
1179
  self,
1154
1180
  agent_ids: list[str],
@@ -1164,16 +1190,16 @@ class AgentManager:
1164
1190
  )
1165
1191
  return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
1166
1192
 
1167
- @trace_method
1168
1193
  @enforce_types
1194
+ @trace_method
1169
1195
  def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
1170
1196
  """Fetch an agent by its ID."""
1171
1197
  with db_registry.session() as session:
1172
1198
  agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
1173
1199
  return agent.to_pydantic()
1174
1200
 
1175
- @trace_method
1176
1201
  @enforce_types
1202
+ @trace_method
1177
1203
  def delete_agent(self, agent_id: str, actor: PydanticUser) -> None:
1178
1204
  """
1179
1205
  Deletes an agent and its associated relationships.
@@ -1220,8 +1246,8 @@ class AgentManager:
1220
1246
  else:
1221
1247
  logger.debug(f"Agent with ID {agent_id} successfully hard deleted")
1222
1248
 
1223
- @trace_method
1224
1249
  @enforce_types
1250
+ @trace_method
1225
1251
  async def delete_agent_async(self, agent_id: str, actor: PydanticUser) -> None:
1226
1252
  """
1227
1253
  Deletes an agent and its associated relationships.
@@ -1270,8 +1296,8 @@ class AgentManager:
1270
1296
  else:
1271
1297
  logger.debug(f"Agent with ID {agent_id} successfully hard deleted")
1272
1298
 
1273
- @trace_method
1274
1299
  @enforce_types
1300
+ @trace_method
1275
1301
  def serialize(self, agent_id: str, actor: PydanticUser) -> AgentSchema:
1276
1302
  with db_registry.session() as session:
1277
1303
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
@@ -1279,8 +1305,8 @@ class AgentManager:
1279
1305
  data = schema.dump(agent)
1280
1306
  return AgentSchema(**data)
1281
1307
 
1282
- @trace_method
1283
1308
  @enforce_types
1309
+ @trace_method
1284
1310
  def deserialize(
1285
1311
  self,
1286
1312
  serialized_agent: AgentSchema,
@@ -1349,8 +1375,8 @@ class AgentManager:
1349
1375
  # ======================================================================================================================
1350
1376
  # Per Agent Environment Variable Management
1351
1377
  # ======================================================================================================================
1352
- @trace_method
1353
1378
  @enforce_types
1379
+ @trace_method
1354
1380
  def _set_environment_variables(
1355
1381
  self,
1356
1382
  agent_id: str,
@@ -1405,8 +1431,8 @@ class AgentManager:
1405
1431
  # Return the updated agent state
1406
1432
  return agent.to_pydantic()
1407
1433
 
1408
- @trace_method
1409
1434
  @enforce_types
1435
+ @trace_method
1410
1436
  def list_groups(self, agent_id: str, actor: PydanticUser, manager_type: Optional[str] = None) -> List[PydanticGroup]:
1411
1437
  with db_registry.session() as session:
1412
1438
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
@@ -1422,20 +1448,20 @@ class AgentManager:
1422
1448
  # TODO: 2) These messages are ordered from oldest to newest
1423
1449
  # TODO: This can be fixed by having an actual relationship in the ORM for message_ids
1424
1450
  # TODO: This can also be made more efficient, instead of getting, setting, we can do it all in one db session for one query.
1425
- @trace_method
1426
1451
  @enforce_types
1452
+ @trace_method
1427
1453
  def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
1428
1454
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1429
1455
  return self.message_manager.get_messages_by_ids(message_ids=message_ids, actor=actor)
1430
1456
 
1431
- @trace_method
1432
1457
  @enforce_types
1458
+ @trace_method
1433
1459
  def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
1434
1460
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1435
1461
  return self.message_manager.get_message_by_id(message_id=message_ids[0], actor=actor)
1436
1462
 
1437
- @trace_method
1438
1463
  @enforce_types
1464
+ @trace_method
1439
1465
  async def get_system_message_async(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
1440
1466
  agent = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=[], actor=actor)
1441
1467
  return await self.message_manager.get_message_by_id_async(message_id=agent.message_ids[0], actor=actor)
@@ -1443,8 +1469,8 @@ class AgentManager:
1443
1469
  # TODO: This is duplicated below
1444
1470
  # TODO: This is legacy code and should be cleaned up
1445
1471
  # TODO: A lot of the memory "compilation" should be offset to a separate class
1446
- @trace_method
1447
1472
  @enforce_types
1473
+ @trace_method
1448
1474
  def rebuild_system_prompt(self, agent_id: str, actor: PydanticUser, force=False, update_timestamp=True) -> PydanticAgentState:
1449
1475
  """Rebuilds the system message with the latest memory object and any shared memory block updates
1450
1476
 
@@ -1515,29 +1541,42 @@ class AgentManager:
1515
1541
  else:
1516
1542
  return agent_state
1517
1543
 
1518
- @trace_method
1544
+ # TODO: This is probably one of the worst pieces of code I've ever written please rip up as you see wish
1519
1545
  @enforce_types
1546
+ @trace_method
1520
1547
  async def rebuild_system_prompt_async(
1521
- self, agent_id: str, actor: PydanticUser, force=False, update_timestamp=True, tool_rules_solver: Optional[ToolRulesSolver] = None
1522
- ) -> PydanticAgentState:
1548
+ self,
1549
+ agent_id: str,
1550
+ actor: PydanticUser,
1551
+ force=False,
1552
+ update_timestamp=True,
1553
+ tool_rules_solver: Optional[ToolRulesSolver] = None,
1554
+ dry_run: bool = False,
1555
+ ) -> Tuple[PydanticAgentState, Optional[PydanticMessage], int, int]:
1523
1556
  """Rebuilds the system message with the latest memory object and any shared memory block updates
1524
1557
 
1525
1558
  Updates to core memory blocks should trigger a "rebuild", which itself will create a new message object
1526
1559
 
1527
1560
  Updates to the memory header should *not* trigger a rebuild, since that will simply flood recall storage with excess messages
1528
1561
  """
1529
- # Get the current agent state
1530
- agent_state = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory", "sources"], actor=actor)
1562
+ num_messages_task = self.message_manager.size_async(actor=actor, agent_id=agent_id)
1563
+ num_archival_memories_task = self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
1564
+ agent_state_task = self.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory", "sources", "tools"], actor=actor)
1565
+
1566
+ num_messages, num_archival_memories, agent_state = await asyncio.gather(
1567
+ num_messages_task,
1568
+ num_archival_memories_task,
1569
+ agent_state_task,
1570
+ )
1571
+
1531
1572
  if not tool_rules_solver:
1532
1573
  tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
1533
1574
 
1534
- curr_system_message = await self.get_system_message_async(
1535
- agent_id=agent_id, actor=actor
1536
- ) # this is the system + memory bank, not just the system prompt
1575
+ curr_system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
1537
1576
 
1538
1577
  if curr_system_message is None:
1539
1578
  logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
1540
- return agent_state
1579
+ return agent_state, curr_system_message, num_messages, num_archival_memories
1541
1580
 
1542
1581
  curr_system_message_openai = curr_system_message.to_openai_dict()
1543
1582
 
@@ -1551,7 +1590,7 @@ class AgentManager:
1551
1590
  logger.debug(
1552
1591
  f"Memory hasn't changed for agent id={agent_id} and actor=({actor.id}, {actor.name}), skipping system prompt rebuild"
1553
1592
  )
1554
- return agent_state
1593
+ return agent_state, curr_system_message, num_messages, num_archival_memories
1555
1594
 
1556
1595
  # If the memory didn't update, we probably don't want to update the timestamp inside
1557
1596
  # For example, if we're doing a system prompt swap, this should probably be False
@@ -1561,9 +1600,6 @@ class AgentManager:
1561
1600
  # NOTE: a bit of a hack - we pull the timestamp from the message created_by
1562
1601
  memory_edit_timestamp = curr_system_message.created_at
1563
1602
 
1564
- num_messages = await self.message_manager.size_async(actor=actor, agent_id=agent_id)
1565
- num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
1566
-
1567
1603
  # update memory (TODO: potentially update recall/archival stats separately)
1568
1604
 
1569
1605
  new_system_message_str = compile_system_message(
@@ -1582,63 +1618,67 @@ class AgentManager:
1582
1618
  logger.debug(f"Rebuilding system with new memory...\nDiff:\n{diff}")
1583
1619
 
1584
1620
  # Swap the system message out (only if there is a diff)
1585
- message = PydanticMessage.dict_to_message(
1621
+ temp_message = PydanticMessage.dict_to_message(
1586
1622
  agent_id=agent_id,
1587
1623
  model=agent_state.llm_config.model,
1588
1624
  openai_message_dict={"role": "system", "content": new_system_message_str},
1589
1625
  )
1590
- message = await self.message_manager.update_message_by_id_async(
1591
- message_id=curr_system_message.id,
1592
- message_update=MessageUpdate(**message.model_dump()),
1593
- actor=actor,
1594
- )
1595
- return await self.set_in_context_messages_async(agent_id=agent_id, message_ids=agent_state.message_ids, actor=actor)
1596
- else:
1597
- return agent_state
1626
+ temp_message.id = curr_system_message.id
1627
+
1628
+ if not dry_run:
1629
+ await self.message_manager.update_message_by_id_async(
1630
+ message_id=curr_system_message.id,
1631
+ message_update=MessageUpdate(**temp_message.model_dump()),
1632
+ actor=actor,
1633
+ )
1634
+ else:
1635
+ curr_system_message = temp_message
1636
+
1637
+ return agent_state, curr_system_message, num_messages, num_archival_memories
1598
1638
 
1599
- @trace_method
1600
1639
  @enforce_types
1640
+ @trace_method
1601
1641
  def set_in_context_messages(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
1602
1642
  return self.update_agent(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
1603
1643
 
1604
- @trace_method
1605
1644
  @enforce_types
1645
+ @trace_method
1606
1646
  async def set_in_context_messages_async(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
1607
1647
  return await self.update_agent_async(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
1608
1648
 
1609
- @trace_method
1610
1649
  @enforce_types
1650
+ @trace_method
1611
1651
  def trim_older_in_context_messages(self, num: int, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1612
1652
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1613
1653
  new_messages = [message_ids[0]] + message_ids[num:] # 0 is system message
1614
1654
  return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
1615
1655
 
1616
- @trace_method
1617
1656
  @enforce_types
1657
+ @trace_method
1618
1658
  def trim_all_in_context_messages_except_system(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1619
1659
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1620
1660
  # TODO: How do we know this?
1621
1661
  new_messages = [message_ids[0]] # 0 is system message
1622
1662
  return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
1623
1663
 
1624
- @trace_method
1625
1664
  @enforce_types
1665
+ @trace_method
1626
1666
  def prepend_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1627
1667
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1628
1668
  new_messages = self.message_manager.create_many_messages(messages, actor=actor)
1629
1669
  message_ids = [message_ids[0]] + [m.id for m in new_messages] + message_ids[1:]
1630
1670
  return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
1631
1671
 
1632
- @trace_method
1633
1672
  @enforce_types
1673
+ @trace_method
1634
1674
  def append_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1635
1675
  messages = self.message_manager.create_many_messages(messages, actor=actor)
1636
1676
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids or []
1637
1677
  message_ids += [m.id for m in messages]
1638
1678
  return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
1639
1679
 
1640
- @trace_method
1641
1680
  @enforce_types
1681
+ @trace_method
1642
1682
  async def append_to_in_context_messages_async(
1643
1683
  self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser
1644
1684
  ) -> PydanticAgentState:
@@ -1648,8 +1688,8 @@ class AgentManager:
1648
1688
  message_ids += [m.id for m in messages]
1649
1689
  return await self.set_in_context_messages_async(agent_id=agent_id, message_ids=message_ids, actor=actor)
1650
1690
 
1651
- @trace_method
1652
1691
  @enforce_types
1692
+ @trace_method
1653
1693
  async def reset_messages_async(
1654
1694
  self, agent_id: str, actor: PydanticUser, add_default_initial_messages: bool = False
1655
1695
  ) -> PydanticAgentState:
@@ -1712,8 +1752,8 @@ class AgentManager:
1712
1752
  else:
1713
1753
  return agent_state
1714
1754
 
1715
- @trace_method
1716
1755
  @enforce_types
1756
+ @trace_method
1717
1757
  async def update_memory_if_changed_async(self, agent_id: str, new_memory: Memory, actor: PydanticUser) -> PydanticAgentState:
1718
1758
  """
1719
1759
  Update internal memory object and system prompt if there have been modifications.
@@ -1756,12 +1796,12 @@ class AgentManager:
1756
1796
  # NOTE: don't do this since re-buildin the memory is handled at the start of the step
1757
1797
  # rebuild memory - this records the last edited timestamp of the memory
1758
1798
  # TODO: pass in update timestamp from block edit time
1759
- agent_state = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor)
1799
+ await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor)
1760
1800
 
1761
1801
  return agent_state
1762
1802
 
1763
- @trace_method
1764
1803
  @enforce_types
1804
+ @trace_method
1765
1805
  async def refresh_memory_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
1766
1806
  # TODO: This will NOT work for new blocks/file blocks added intra-step
1767
1807
  block_ids = [b.id for b in agent_state.memory.blocks]
@@ -1779,8 +1819,8 @@ class AgentManager:
1779
1819
 
1780
1820
  return agent_state
1781
1821
 
1782
- @trace_method
1783
1822
  @enforce_types
1823
+ @trace_method
1784
1824
  async def refresh_file_blocks(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
1785
1825
  file_blocks = await self.file_agent_manager.list_files_for_agent(agent_id=agent_state.id, actor=actor, return_as_blocks=True)
1786
1826
  agent_state.memory.file_blocks = [b for b in file_blocks if b is not None]
@@ -1789,8 +1829,8 @@ class AgentManager:
1789
1829
  # ======================================================================================================================
1790
1830
  # Source Management
1791
1831
  # ======================================================================================================================
1792
- @trace_method
1793
1832
  @enforce_types
1833
+ @trace_method
1794
1834
  async def attach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
1795
1835
  """
1796
1836
  Attaches a source to an agent.
@@ -1820,15 +1860,11 @@ class AgentManager:
1820
1860
  )
1821
1861
 
1822
1862
  # Commit the changes
1823
- await agent.update_async(session, actor=actor)
1824
-
1825
- # Force rebuild of system prompt so that the agent is updated with passage count
1826
- pydantic_agent = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True)
1827
-
1828
- return pydantic_agent
1863
+ agent = await agent.update_async(session, actor=actor)
1864
+ return await agent.to_pydantic_async()
1829
1865
 
1830
- @trace_method
1831
1866
  @enforce_types
1867
+ @trace_method
1832
1868
  def append_system_message(self, agent_id: str, content: str, actor: PydanticUser):
1833
1869
 
1834
1870
  # get the agent
@@ -1840,8 +1876,8 @@ class AgentManager:
1840
1876
  # update agent in-context message IDs
1841
1877
  self.append_to_in_context_messages(messages=[message], agent_id=agent_id, actor=actor)
1842
1878
 
1843
- @trace_method
1844
1879
  @enforce_types
1880
+ @trace_method
1845
1881
  async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser):
1846
1882
 
1847
1883
  # get the agent
@@ -1853,28 +1889,8 @@ class AgentManager:
1853
1889
  # update agent in-context message IDs
1854
1890
  await self.append_to_in_context_messages_async(messages=[message], agent_id=agent_id, actor=actor)
1855
1891
 
1856
- @trace_method
1857
1892
  @enforce_types
1858
- def list_attached_sources(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
1859
- """
1860
- Lists all sources attached to an agent.
1861
-
1862
- Args:
1863
- agent_id: ID of the agent to list sources for
1864
- actor: User performing the action
1865
-
1866
- Returns:
1867
- List[str]: List of source IDs attached to the agent
1868
- """
1869
- with db_registry.session() as session:
1870
- # Verify agent exists and user has permission to access it
1871
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1872
-
1873
- # Use the lazy-loaded relationship to get sources
1874
- return [source.to_pydantic() for source in agent.sources]
1875
-
1876
1893
  @trace_method
1877
- @enforce_types
1878
1894
  async def list_attached_sources_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
1879
1895
  """
1880
1896
  Lists all sources attached to an agent.
@@ -1885,44 +1901,34 @@ class AgentManager:
1885
1901
 
1886
1902
  Returns:
1887
1903
  List[str]: List of source IDs attached to the agent
1888
- """
1889
- async with db_registry.async_session() as session:
1890
- # Verify agent exists and user has permission to access it
1891
- agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1892
-
1893
- # Use the lazy-loaded relationship to get sources
1894
- return [source.to_pydantic() for source in agent.sources]
1895
-
1896
- @trace_method
1897
- @enforce_types
1898
- def detach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
1899
- """
1900
- Detaches a source from an agent.
1901
1904
 
1902
- Args:
1903
- agent_id: ID of the agent to detach the source from
1904
- source_id: ID of the source to detach
1905
- actor: User performing the action
1905
+ Raises:
1906
+ NoResultFound: If agent doesn't exist or user doesn't have access
1906
1907
  """
1907
- with db_registry.session() as session:
1908
- # Verify agent exists and user has permission to access it
1909
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1910
-
1911
- # Remove the source from the relationship
1912
- remaining_sources = [s for s in agent.sources if s.id != source_id]
1908
+ async with db_registry.async_session() as session:
1909
+ # Validate agent exists and user has access
1910
+ await self._validate_agent_exists_async(session, agent_id, actor)
1913
1911
 
1914
- if len(remaining_sources) == len(agent.sources): # Source ID was not in the relationship
1915
- logger.warning(f"Attempted to remove unattached source id={source_id} from agent id={agent_id} by actor={actor}")
1912
+ # Use raw SQL to efficiently fetch sources - much faster than lazy loading
1913
+ # Fast query without relationship loading
1914
+ query = (
1915
+ select(SourceModel)
1916
+ .join(SourcesAgents, SourceModel.id == SourcesAgents.source_id)
1917
+ .where(
1918
+ SourcesAgents.agent_id == agent_id,
1919
+ SourceModel.organization_id == actor.organization_id,
1920
+ SourceModel.is_deleted == False,
1921
+ )
1922
+ .order_by(SourceModel.created_at.desc(), SourceModel.id)
1923
+ )
1916
1924
 
1917
- # Update the sources relationship
1918
- agent.sources = remaining_sources
1925
+ result = await session.execute(query)
1926
+ sources = result.scalars().all()
1919
1927
 
1920
- # Commit the changes
1921
- agent.update(session, actor=actor)
1922
- return agent.to_pydantic()
1928
+ return [source.to_pydantic() for source in sources]
1923
1929
 
1924
- @trace_method
1925
1930
  @enforce_types
1931
+ @trace_method
1926
1932
  async def detach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
1927
1933
  """
1928
1934
  Detaches a source from an agent.
@@ -1931,29 +1937,36 @@ class AgentManager:
1931
1937
  agent_id: ID of the agent to detach the source from
1932
1938
  source_id: ID of the source to detach
1933
1939
  actor: User performing the action
1940
+
1941
+ Raises:
1942
+ NoResultFound: If agent doesn't exist or user doesn't have access
1934
1943
  """
1935
1944
  async with db_registry.async_session() as session:
1936
- # Verify agent exists and user has permission to access it
1937
- agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1945
+ # Validate agent exists and user has access
1946
+ await self._validate_agent_exists_async(session, agent_id, actor)
1938
1947
 
1939
- # Remove the source from the relationship
1940
- remaining_sources = [s for s in agent.sources if s.id != source_id]
1948
+ # Check if the source is actually attached to this agent using junction table
1949
+ attachment_check_query = select(SourcesAgents).where(SourcesAgents.agent_id == agent_id, SourcesAgents.source_id == source_id)
1950
+ attachment_result = await session.execute(attachment_check_query)
1951
+ attachment = attachment_result.scalar_one_or_none()
1941
1952
 
1942
- if len(remaining_sources) == len(agent.sources): # Source ID was not in the relationship
1953
+ if not attachment:
1943
1954
  logger.warning(f"Attempted to remove unattached source id={source_id} from agent id={agent_id} by actor={actor}")
1955
+ else:
1956
+ # Delete the association directly from the junction table
1957
+ delete_query = delete(SourcesAgents).where(SourcesAgents.agent_id == agent_id, SourcesAgents.source_id == source_id)
1958
+ await session.execute(delete_query)
1959
+ await session.commit()
1944
1960
 
1945
- # Update the sources relationship
1946
- agent.sources = remaining_sources
1947
-
1948
- # Commit the changes
1949
- await agent.update_async(session, actor=actor)
1961
+ # Get agent without loading relationships for return value
1962
+ agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1950
1963
  return await agent.to_pydantic_async()
1951
1964
 
1952
1965
  # ======================================================================================================================
1953
1966
  # Block management
1954
1967
  # ======================================================================================================================
1955
- @trace_method
1956
1968
  @enforce_types
1969
+ @trace_method
1957
1970
  def get_block_with_label(
1958
1971
  self,
1959
1972
  agent_id: str,
@@ -1968,8 +1981,8 @@ class AgentManager:
1968
1981
  return block.to_pydantic()
1969
1982
  raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}'")
1970
1983
 
1971
- @trace_method
1972
1984
  @enforce_types
1985
+ @trace_method
1973
1986
  async def get_block_with_label_async(
1974
1987
  self,
1975
1988
  agent_id: str,
@@ -1984,8 +1997,8 @@ class AgentManager:
1984
1997
  return block.to_pydantic()
1985
1998
  raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}'")
1986
1999
 
1987
- @trace_method
1988
2000
  @enforce_types
2001
+ @trace_method
1989
2002
  async def modify_block_by_label_async(
1990
2003
  self,
1991
2004
  agent_id: str,
@@ -2012,8 +2025,8 @@ class AgentManager:
2012
2025
  await block.update_async(session, actor=actor)
2013
2026
  return block.to_pydantic()
2014
2027
 
2015
- @trace_method
2016
2028
  @enforce_types
2029
+ @trace_method
2017
2030
  def update_block_with_label(
2018
2031
  self,
2019
2032
  agent_id: str,
@@ -2037,8 +2050,8 @@ class AgentManager:
2037
2050
  agent.update(session, actor=actor)
2038
2051
  return agent.to_pydantic()
2039
2052
 
2040
- @trace_method
2041
2053
  @enforce_types
2054
+ @trace_method
2042
2055
  def attach_block(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
2043
2056
  """Attaches a block to an agent. For sleeptime agents, also attaches to paired agents in the same group."""
2044
2057
  with db_registry.session() as session:
@@ -2067,8 +2080,8 @@ class AgentManager:
2067
2080
 
2068
2081
  return agent.to_pydantic()
2069
2082
 
2070
- @trace_method
2071
2083
  @enforce_types
2084
+ @trace_method
2072
2085
  async def attach_block_async(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
2073
2086
  """Attaches a block to an agent. For sleeptime agents, also attaches to paired agents in the same group."""
2074
2087
  async with db_registry.async_session() as session:
@@ -2103,8 +2116,8 @@ class AgentManager:
2103
2116
 
2104
2117
  return await agent.to_pydantic_async()
2105
2118
 
2106
- @trace_method
2107
2119
  @enforce_types
2120
+ @trace_method
2108
2121
  def detach_block(
2109
2122
  self,
2110
2123
  agent_id: str,
@@ -2124,8 +2137,8 @@ class AgentManager:
2124
2137
  agent.update(session, actor=actor)
2125
2138
  return agent.to_pydantic()
2126
2139
 
2127
- @trace_method
2128
2140
  @enforce_types
2141
+ @trace_method
2129
2142
  async def detach_block_async(
2130
2143
  self,
2131
2144
  agent_id: str,
@@ -2145,8 +2158,8 @@ class AgentManager:
2145
2158
  await agent.update_async(session, actor=actor)
2146
2159
  return await agent.to_pydantic_async()
2147
2160
 
2148
- @trace_method
2149
2161
  @enforce_types
2162
+ @trace_method
2150
2163
  def detach_block_with_label(
2151
2164
  self,
2152
2165
  agent_id: str,
@@ -2170,8 +2183,8 @@ class AgentManager:
2170
2183
  # Passage Management
2171
2184
  # ======================================================================================================================
2172
2185
 
2173
- @trace_method
2174
2186
  @enforce_types
2187
+ @trace_method
2175
2188
  def list_passages(
2176
2189
  self,
2177
2190
  actor: PydanticUser,
@@ -2231,8 +2244,8 @@ class AgentManager:
2231
2244
 
2232
2245
  return [p.to_pydantic() for p in passages]
2233
2246
 
2234
- @trace_method
2235
2247
  @enforce_types
2248
+ @trace_method
2236
2249
  async def list_passages_async(
2237
2250
  self,
2238
2251
  actor: PydanticUser,
@@ -2292,8 +2305,8 @@ class AgentManager:
2292
2305
 
2293
2306
  return [p.to_pydantic() for p in passages]
2294
2307
 
2295
- @trace_method
2296
2308
  @enforce_types
2309
+ @trace_method
2297
2310
  async def list_source_passages_async(
2298
2311
  self,
2299
2312
  actor: PydanticUser,
@@ -2340,8 +2353,8 @@ class AgentManager:
2340
2353
  # Convert to Pydantic models
2341
2354
  return [p.to_pydantic() for p in passages]
2342
2355
 
2343
- @trace_method
2344
2356
  @enforce_types
2357
+ @trace_method
2345
2358
  async def list_agent_passages_async(
2346
2359
  self,
2347
2360
  actor: PydanticUser,
@@ -2384,8 +2397,8 @@ class AgentManager:
2384
2397
  # Convert to Pydantic models
2385
2398
  return [p.to_pydantic() for p in passages]
2386
2399
 
2387
- @trace_method
2388
2400
  @enforce_types
2401
+ @trace_method
2389
2402
  def passage_size(
2390
2403
  self,
2391
2404
  actor: PydanticUser,
@@ -2465,8 +2478,8 @@ class AgentManager:
2465
2478
  # ======================================================================================================================
2466
2479
  # Tool Management
2467
2480
  # ======================================================================================================================
2468
- @trace_method
2469
2481
  @enforce_types
2482
+ @trace_method
2470
2483
  def attach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
2471
2484
  """
2472
2485
  Attaches a tool to an agent.
@@ -2501,8 +2514,8 @@ class AgentManager:
2501
2514
  agent.update(session, actor=actor)
2502
2515
  return agent.to_pydantic()
2503
2516
 
2504
- @trace_method
2505
2517
  @enforce_types
2518
+ @trace_method
2506
2519
  async def attach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
2507
2520
  """
2508
2521
  Attaches a tool to an agent.
@@ -2537,8 +2550,8 @@ class AgentManager:
2537
2550
  await agent.update_async(session, actor=actor)
2538
2551
  return await agent.to_pydantic_async()
2539
2552
 
2540
- @trace_method
2541
2553
  @enforce_types
2554
+ @trace_method
2542
2555
  async def attach_missing_files_tools_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
2543
2556
  """
2544
2557
  Attaches missing core file tools to an agent.
@@ -2569,8 +2582,8 @@ class AgentManager:
2569
2582
 
2570
2583
  return agent_state
2571
2584
 
2572
- @trace_method
2573
2585
  @enforce_types
2586
+ @trace_method
2574
2587
  async def detach_all_files_tools_async(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
2575
2588
  """
2576
2589
  Detach all core file tools from an agent.
@@ -2596,8 +2609,8 @@ class AgentManager:
2596
2609
 
2597
2610
  return agent_state
2598
2611
 
2599
- @trace_method
2600
2612
  @enforce_types
2613
+ @trace_method
2601
2614
  def detach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
2602
2615
  """
2603
2616
  Detaches a tool from an agent.
@@ -2630,8 +2643,8 @@ class AgentManager:
2630
2643
  agent.update(session, actor=actor)
2631
2644
  return agent.to_pydantic()
2632
2645
 
2633
- @trace_method
2634
2646
  @enforce_types
2647
+ @trace_method
2635
2648
  async def detach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
2636
2649
  """
2637
2650
  Detaches a tool from an agent.
@@ -2664,8 +2677,8 @@ class AgentManager:
2664
2677
  await agent.update_async(session, actor=actor)
2665
2678
  return await agent.to_pydantic_async()
2666
2679
 
2667
- @trace_method
2668
2680
  @enforce_types
2681
+ @trace_method
2669
2682
  def list_attached_tools(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
2670
2683
  """
2671
2684
  List all tools attached to an agent.
@@ -2681,11 +2694,40 @@ class AgentManager:
2681
2694
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
2682
2695
  return [tool.to_pydantic() for tool in agent.tools]
2683
2696
 
2697
+ @enforce_types
2698
+ @trace_method
2699
+ async def list_attached_tools_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
2700
+ """
2701
+ List all tools attached to an agent (async version with optimized performance).
2702
+ Uses direct SQL queries to avoid SqlAlchemyBase overhead.
2703
+
2704
+ Args:
2705
+ agent_id: ID of the agent to list tools for.
2706
+ actor: User performing the action.
2707
+
2708
+ Returns:
2709
+ List[PydanticTool]: List of tools attached to the agent.
2710
+ """
2711
+ async with db_registry.async_session() as session:
2712
+ # lightweight check for agent access
2713
+ await self._validate_agent_exists_async(session, agent_id, actor)
2714
+
2715
+ # direct query for tools via join - much more performant
2716
+ query = (
2717
+ select(ToolModel)
2718
+ .join(ToolsAgents, ToolModel.id == ToolsAgents.tool_id)
2719
+ .where(ToolsAgents.agent_id == agent_id, ToolModel.organization_id == actor.organization_id)
2720
+ )
2721
+
2722
+ result = await session.execute(query)
2723
+ tools = result.scalars().all()
2724
+ return [tool.to_pydantic() for tool in tools]
2725
+
2684
2726
  # ======================================================================================================================
2685
2727
  # Tag Management
2686
2728
  # ======================================================================================================================
2687
- @trace_method
2688
2729
  @enforce_types
2730
+ @trace_method
2689
2731
  def list_tags(
2690
2732
  self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, query_text: Optional[str] = None
2691
2733
  ) -> List[str]:
@@ -2719,8 +2761,8 @@ class AgentManager:
2719
2761
  results = [tag[0] for tag in query.all()]
2720
2762
  return results
2721
2763
 
2722
- @trace_method
2723
2764
  @enforce_types
2765
+ @trace_method
2724
2766
  async def list_tags_async(
2725
2767
  self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, query_text: Optional[str] = None
2726
2768
  ) -> List[str]:
@@ -2759,8 +2801,11 @@ class AgentManager:
2759
2801
  results = [row[0] for row in result.all()]
2760
2802
  return results
2761
2803
 
2804
+ @trace_method
2762
2805
  async def get_context_window(self, agent_id: str, actor: PydanticUser) -> ContextWindowOverview:
2763
- agent_state = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True)
2806
+ agent_state, system_message, num_messages, num_archival_memories = await self.rebuild_system_prompt_async(
2807
+ agent_id=agent_id, actor=actor, force=True, dry_run=True
2808
+ )
2764
2809
  calculator = ContextWindowCalculator()
2765
2810
 
2766
2811
  if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION" and agent_state.llm_config.model_endpoint_type == "anthropic":
@@ -2776,5 +2821,7 @@ class AgentManager:
2776
2821
  actor=actor,
2777
2822
  token_counter=token_counter,
2778
2823
  message_manager=self.message_manager,
2779
- passage_manager=self.passage_manager,
2824
+ system_message_compiled=system_message,
2825
+ num_archival_memories=num_archival_memories,
2826
+ num_messages=num_messages,
2780
2827
  )