letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251024223017__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 (159) hide show
  1. letta/__init__.py +2 -3
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/simple_llm_request_adapter.py +8 -5
  4. letta/adapters/simple_llm_stream_adapter.py +22 -6
  5. letta/agents/agent_loop.py +10 -3
  6. letta/agents/base_agent.py +4 -1
  7. letta/agents/helpers.py +41 -9
  8. letta/agents/letta_agent.py +11 -10
  9. letta/agents/letta_agent_v2.py +47 -37
  10. letta/agents/letta_agent_v3.py +395 -300
  11. letta/agents/voice_agent.py +8 -6
  12. letta/agents/voice_sleeptime_agent.py +3 -3
  13. letta/constants.py +30 -7
  14. letta/errors.py +20 -0
  15. letta/functions/function_sets/base.py +55 -3
  16. letta/functions/mcp_client/types.py +33 -57
  17. letta/functions/schema_generator.py +135 -23
  18. letta/groups/sleeptime_multi_agent_v3.py +6 -11
  19. letta/groups/sleeptime_multi_agent_v4.py +227 -0
  20. letta/helpers/converters.py +78 -4
  21. letta/helpers/crypto_utils.py +6 -2
  22. letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
  23. letta/interfaces/anthropic_streaming_interface.py +3 -4
  24. letta/interfaces/gemini_streaming_interface.py +4 -6
  25. letta/interfaces/openai_streaming_interface.py +63 -28
  26. letta/llm_api/anthropic_client.py +7 -4
  27. letta/llm_api/deepseek_client.py +6 -4
  28. letta/llm_api/google_ai_client.py +3 -12
  29. letta/llm_api/google_vertex_client.py +1 -1
  30. letta/llm_api/helpers.py +90 -61
  31. letta/llm_api/llm_api_tools.py +4 -1
  32. letta/llm_api/openai.py +12 -12
  33. letta/llm_api/openai_client.py +53 -16
  34. letta/local_llm/constants.py +4 -3
  35. letta/local_llm/json_parser.py +5 -2
  36. letta/local_llm/utils.py +2 -3
  37. letta/log.py +171 -7
  38. letta/orm/agent.py +43 -9
  39. letta/orm/archive.py +4 -0
  40. letta/orm/custom_columns.py +15 -0
  41. letta/orm/identity.py +11 -11
  42. letta/orm/mcp_server.py +9 -0
  43. letta/orm/message.py +6 -1
  44. letta/orm/run_metrics.py +7 -2
  45. letta/orm/sqlalchemy_base.py +2 -2
  46. letta/orm/tool.py +3 -0
  47. letta/otel/tracing.py +2 -0
  48. letta/prompts/prompt_generator.py +7 -2
  49. letta/schemas/agent.py +41 -10
  50. letta/schemas/agent_file.py +3 -0
  51. letta/schemas/archive.py +4 -2
  52. letta/schemas/block.py +2 -1
  53. letta/schemas/enums.py +36 -3
  54. letta/schemas/file.py +3 -3
  55. letta/schemas/folder.py +2 -1
  56. letta/schemas/group.py +2 -1
  57. letta/schemas/identity.py +18 -9
  58. letta/schemas/job.py +3 -1
  59. letta/schemas/letta_message.py +71 -12
  60. letta/schemas/letta_request.py +7 -3
  61. letta/schemas/letta_stop_reason.py +0 -25
  62. letta/schemas/llm_config.py +8 -2
  63. letta/schemas/mcp.py +80 -83
  64. letta/schemas/mcp_server.py +349 -0
  65. letta/schemas/memory.py +20 -8
  66. letta/schemas/message.py +212 -67
  67. letta/schemas/providers/anthropic.py +13 -6
  68. letta/schemas/providers/azure.py +6 -4
  69. letta/schemas/providers/base.py +8 -4
  70. letta/schemas/providers/bedrock.py +6 -2
  71. letta/schemas/providers/cerebras.py +7 -3
  72. letta/schemas/providers/deepseek.py +2 -1
  73. letta/schemas/providers/google_gemini.py +15 -6
  74. letta/schemas/providers/groq.py +2 -1
  75. letta/schemas/providers/lmstudio.py +9 -6
  76. letta/schemas/providers/mistral.py +2 -1
  77. letta/schemas/providers/openai.py +7 -2
  78. letta/schemas/providers/together.py +9 -3
  79. letta/schemas/providers/xai.py +7 -3
  80. letta/schemas/run.py +7 -2
  81. letta/schemas/run_metrics.py +2 -1
  82. letta/schemas/sandbox_config.py +2 -2
  83. letta/schemas/secret.py +3 -158
  84. letta/schemas/source.py +2 -2
  85. letta/schemas/step.py +2 -2
  86. letta/schemas/tool.py +24 -1
  87. letta/schemas/usage.py +0 -1
  88. letta/server/rest_api/app.py +123 -7
  89. letta/server/rest_api/dependencies.py +3 -0
  90. letta/server/rest_api/interface.py +7 -4
  91. letta/server/rest_api/redis_stream_manager.py +16 -1
  92. letta/server/rest_api/routers/v1/__init__.py +7 -0
  93. letta/server/rest_api/routers/v1/agents.py +332 -322
  94. letta/server/rest_api/routers/v1/archives.py +127 -40
  95. letta/server/rest_api/routers/v1/blocks.py +54 -6
  96. letta/server/rest_api/routers/v1/chat_completions.py +146 -0
  97. letta/server/rest_api/routers/v1/folders.py +27 -35
  98. letta/server/rest_api/routers/v1/groups.py +23 -35
  99. letta/server/rest_api/routers/v1/identities.py +24 -10
  100. letta/server/rest_api/routers/v1/internal_runs.py +107 -0
  101. letta/server/rest_api/routers/v1/internal_templates.py +162 -179
  102. letta/server/rest_api/routers/v1/jobs.py +15 -27
  103. letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
  104. letta/server/rest_api/routers/v1/messages.py +23 -34
  105. letta/server/rest_api/routers/v1/organizations.py +6 -27
  106. letta/server/rest_api/routers/v1/providers.py +35 -62
  107. letta/server/rest_api/routers/v1/runs.py +30 -43
  108. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
  109. letta/server/rest_api/routers/v1/sources.py +26 -42
  110. letta/server/rest_api/routers/v1/steps.py +16 -29
  111. letta/server/rest_api/routers/v1/tools.py +17 -13
  112. letta/server/rest_api/routers/v1/users.py +5 -17
  113. letta/server/rest_api/routers/v1/voice.py +18 -27
  114. letta/server/rest_api/streaming_response.py +5 -2
  115. letta/server/rest_api/utils.py +187 -25
  116. letta/server/server.py +27 -22
  117. letta/server/ws_api/server.py +5 -4
  118. letta/services/agent_manager.py +148 -26
  119. letta/services/agent_serialization_manager.py +6 -1
  120. letta/services/archive_manager.py +168 -15
  121. letta/services/block_manager.py +14 -4
  122. letta/services/file_manager.py +33 -29
  123. letta/services/group_manager.py +10 -0
  124. letta/services/helpers/agent_manager_helper.py +65 -11
  125. letta/services/identity_manager.py +105 -4
  126. letta/services/job_manager.py +11 -1
  127. letta/services/mcp/base_client.py +2 -2
  128. letta/services/mcp/oauth_utils.py +33 -8
  129. letta/services/mcp_manager.py +174 -78
  130. letta/services/mcp_server_manager.py +1331 -0
  131. letta/services/message_manager.py +109 -4
  132. letta/services/organization_manager.py +4 -4
  133. letta/services/passage_manager.py +9 -25
  134. letta/services/provider_manager.py +91 -15
  135. letta/services/run_manager.py +72 -15
  136. letta/services/sandbox_config_manager.py +45 -3
  137. letta/services/source_manager.py +15 -8
  138. letta/services/step_manager.py +24 -1
  139. letta/services/streaming_service.py +581 -0
  140. letta/services/summarizer/summarizer.py +1 -1
  141. letta/services/tool_executor/core_tool_executor.py +111 -0
  142. letta/services/tool_executor/files_tool_executor.py +5 -3
  143. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  144. letta/services/tool_executor/tool_execution_manager.py +1 -1
  145. letta/services/tool_manager.py +10 -3
  146. letta/services/tool_sandbox/base.py +61 -1
  147. letta/services/tool_sandbox/local_sandbox.py +1 -3
  148. letta/services/user_manager.py +2 -2
  149. letta/settings.py +49 -5
  150. letta/system.py +14 -5
  151. letta/utils.py +73 -1
  152. letta/validators.py +105 -0
  153. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
  155. letta/schemas/letta_ping.py +0 -28
  156. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  157. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
@@ -25,6 +25,7 @@ from letta.constants import (
25
25
  INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES,
26
26
  RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
27
27
  )
28
+ from letta.errors import LettaAgentNotFoundError
28
29
  from letta.helpers import ToolRulesSolver
29
30
  from letta.helpers.datetime_helpers import get_utc_time
30
31
  from letta.llm_api.llm_client import LLMClient
@@ -50,6 +51,7 @@ from letta.orm.sqlalchemy_base import AccessType
50
51
  from letta.otel.tracing import trace_method
51
52
  from letta.prompts.prompt_generator import PromptGenerator
52
53
  from letta.schemas.agent import (
54
+ AgentRelationships,
53
55
  AgentState as PydanticAgentState,
54
56
  CreateAgent,
55
57
  InternalTemplateAgentCreate,
@@ -57,13 +59,14 @@ from letta.schemas.agent import (
57
59
  )
58
60
  from letta.schemas.block import DEFAULT_BLOCKS, Block as PydanticBlock, BlockUpdate
59
61
  from letta.schemas.embedding_config import EmbeddingConfig
60
- from letta.schemas.enums import AgentType, ProviderType, TagMatchMode, ToolType, VectorDBProvider
62
+ from letta.schemas.enums import AgentType, PrimitiveType, ProviderType, TagMatchMode, ToolType, VectorDBProvider
61
63
  from letta.schemas.file import FileMetadata as PydanticFileMetadata
62
64
  from letta.schemas.group import Group as PydanticGroup, ManagerType
63
65
  from letta.schemas.llm_config import LLMConfig
64
66
  from letta.schemas.memory import ContextWindowOverview, Memory
65
67
  from letta.schemas.message import Message, Message as PydanticMessage, MessageCreate, MessageUpdate
66
68
  from letta.schemas.passage import Passage as PydanticPassage
69
+ from letta.schemas.secret import Secret
67
70
  from letta.schemas.source import Source as PydanticSource
68
71
  from letta.schemas.tool import Tool as PydanticTool
69
72
  from letta.schemas.tool_rule import ContinueToolRule, RequiresApprovalToolRule, TerminalToolRule
@@ -106,8 +109,9 @@ from letta.services.message_manager import MessageManager
106
109
  from letta.services.passage_manager import PassageManager
107
110
  from letta.services.source_manager import SourceManager
108
111
  from letta.services.tool_manager import ToolManager
109
- from letta.settings import DatabaseChoice, settings
112
+ from letta.settings import DatabaseChoice, model_settings, settings
110
113
  from letta.utils import calculate_file_defaults_based_on_context_window, enforce_types, united_diff
114
+ from letta.validators import raise_on_invalid_id
111
115
 
112
116
  logger = get_logger(__name__)
113
117
 
@@ -170,13 +174,20 @@ class AgentManager:
170
174
 
171
175
  @staticmethod
172
176
  async def _resolve_tools_async(
173
- session, names: Set[str], ids: Set[str], org_id: str
177
+ session, names: Set[str], ids: Set[str], org_id: str, ignore_invalid_tools: bool = False
174
178
  ) -> Tuple[Dict[str, str], Dict[str, str], List[str]]:
175
179
  """
176
180
  Bulk‑fetch all ToolModel rows matching either name ∈ names or id ∈ ids
177
181
  (and scoped to this organization), and return two maps:
178
182
  name_to_id, id_to_name.
179
- Raises if any requested name or id was not found.
183
+ Raises if any requested name or id was not found (unless ignore_invalid_tools is True).
184
+
185
+ Args:
186
+ session: Database session
187
+ names: Set of tool names to resolve
188
+ ids: Set of tool IDs to resolve
189
+ org_id: Organization ID for scoping
190
+ ignore_invalid_tools: If True, silently filters out missing tools instead of raising an error
180
191
  """
181
192
  stmt = select(ToolModel.id, ToolModel.name, ToolModel.default_requires_approval).where(
182
193
  ToolModel.organization_id == org_id,
@@ -193,10 +204,21 @@ class AgentManager:
193
204
 
194
205
  missing_names = names - set(name_to_id.keys())
195
206
  missing_ids = ids - set(id_to_name.keys())
196
- if missing_names:
197
- raise ValueError(f"Tools not found by name: {missing_names}")
198
- if missing_ids:
199
- raise ValueError(f"Tools not found by id: {missing_ids}")
207
+
208
+ if not ignore_invalid_tools:
209
+ # Original behavior: raise errors for missing tools
210
+ if missing_names:
211
+ raise ValueError(f"Tools not found by name: {missing_names}")
212
+ if missing_ids:
213
+ raise ValueError(f"Tools not found by id: {missing_ids}")
214
+ else:
215
+ # New behavior: log missing tools but don't raise errors
216
+ if missing_names or missing_ids:
217
+ logger = get_logger(__name__)
218
+ if missing_names:
219
+ logger.warning(f"Ignoring tools not found by name: {missing_names}")
220
+ if missing_ids:
221
+ logger.warning(f"Ignoring tools not found by id: {missing_ids}")
200
222
 
201
223
  return name_to_id, id_to_name, requires_approval
202
224
 
@@ -310,6 +332,7 @@ class AgentManager:
310
332
  actor: PydanticUser,
311
333
  _test_only_force_id: Optional[str] = None,
312
334
  _init_with_no_messages: bool = False,
335
+ ignore_invalid_tools: bool = False,
313
336
  ) -> PydanticAgentState:
314
337
  # validate required configs
315
338
  if not agent_create.llm_config or not agent_create.embedding_config:
@@ -378,6 +401,9 @@ class AgentManager:
378
401
  # NOTE: also overwrite initial message sequence to empty by default
379
402
  if agent_create.initial_message_sequence is None:
380
403
  agent_create.initial_message_sequence = []
404
+ # NOTE: default to no base tool rules unless explicitly provided
405
+ if not agent_create.tool_rules and agent_create.include_base_tool_rules is None:
406
+ agent_create.include_base_tool_rules = False
381
407
  elif agent_create.agent_type == AgentType.workflow_agent:
382
408
  pass # no default tools
383
409
  else:
@@ -416,6 +442,7 @@ class AgentManager:
416
442
  tool_names,
417
443
  supplied_ids,
418
444
  actor.organization_id,
445
+ ignore_invalid_tools=ignore_invalid_tools,
419
446
  )
420
447
 
421
448
  tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
@@ -521,16 +548,22 @@ class AgentManager:
521
548
 
522
549
  env_rows = []
523
550
  agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
551
+
524
552
  if agent_secrets:
525
- env_rows = [
526
- {
553
+ # Encrypt environment variable values
554
+ env_rows = []
555
+ for key, val in agent_secrets.items():
556
+ row = {
527
557
  "agent_id": aid,
528
558
  "key": key,
529
559
  "value": val,
530
560
  "organization_id": actor.organization_id,
531
561
  }
532
- for key, val in agent_secrets.items()
533
- ]
562
+ # Encrypt value (Secret.from_plaintext handles missing encryption key internally)
563
+ value_secret = Secret.from_plaintext(val)
564
+ row["value_enc"] = value_secret.get_encrypted()
565
+ env_rows.append(row)
566
+
534
567
  result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
535
568
  env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
536
569
 
@@ -640,6 +673,7 @@ class AgentManager:
640
673
 
641
674
  @enforce_types
642
675
  @trace_method
676
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
643
677
  async def update_agent_async(
644
678
  self,
645
679
  agent_id: str,
@@ -742,16 +776,44 @@ class AgentManager:
742
776
 
743
777
  agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
744
778
  if agent_secrets is not None:
779
+ # Fetch existing environment variables to check if values changed
780
+ result = await session.execute(select(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
781
+ existing_env_vars = {env.key: env for env in result.scalars().all()}
782
+
783
+ # TODO: do we need to delete each time or can we just upsert?
745
784
  await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
746
- env_rows = [
747
- {
785
+ # Encrypt environment variable values
786
+ # Only re-encrypt if the value has actually changed
787
+ env_rows = []
788
+ for k, v in agent_secrets.items():
789
+ row = {
748
790
  "agent_id": aid,
749
791
  "key": k,
750
792
  "value": v,
751
793
  "organization_id": agent.organization_id,
752
794
  }
753
- for k, v in agent_secrets.items()
754
- ]
795
+
796
+ # Check if value changed to avoid unnecessary re-encryption
797
+ existing_env = existing_env_vars.get(k)
798
+ existing_value = None
799
+ if existing_env:
800
+ if existing_env.value_enc:
801
+ existing_secret = Secret.from_encrypted(existing_env.value_enc)
802
+ existing_value = existing_secret.get_plaintext()
803
+ elif existing_env.value:
804
+ existing_value = existing_env.value
805
+
806
+ # Encrypt value (reuse existing encrypted value if unchanged)
807
+ if existing_value == v and existing_env and existing_env.value_enc:
808
+ # Value unchanged, reuse existing encrypted value
809
+ row["value_enc"] = existing_env.value_enc
810
+ else:
811
+ # Value changed or new, encrypt
812
+ value_secret = Secret.from_plaintext(v)
813
+ row["value_enc"] = value_secret.get_encrypted()
814
+
815
+ env_rows.append(row)
816
+
755
817
  if env_rows:
756
818
  await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
757
819
  session.expire(agent, ["tool_exec_environment_variables"])
@@ -809,6 +871,7 @@ class AgentManager:
809
871
  identity_id: Optional[str] = None,
810
872
  identifier_keys: Optional[List[str]] = None,
811
873
  include_relationships: Optional[List[str]] = None,
874
+ include: List[str] = [],
812
875
  ascending: bool = True,
813
876
  sort_by: Optional[str] = "created_at",
814
877
  show_hidden_agents: Optional[bool] = None,
@@ -846,7 +909,7 @@ class AgentManager:
846
909
  query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
847
910
  query = _apply_identity_filters(query, identity_id, identifier_keys)
848
911
  query = _apply_tag_filter(query, tags, match_all_tags)
849
- query = _apply_relationship_filters(query, include_relationships)
912
+ query = _apply_relationship_filters(query, include_relationships, include)
850
913
 
851
914
  # Apply hidden filter
852
915
  if not show_hidden_agents:
@@ -857,7 +920,9 @@ class AgentManager:
857
920
  query = query.limit(limit)
858
921
  result = await session.execute(query)
859
922
  agents = result.scalars().all()
860
- return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
923
+ return await asyncio.gather(
924
+ *[agent.to_pydantic_async(include_relationships=include_relationships, include=include) for agent in agents]
925
+ )
861
926
 
862
927
  @enforce_types
863
928
  @trace_method
@@ -916,19 +981,22 @@ class AgentManager:
916
981
 
917
982
  @enforce_types
918
983
  @trace_method
984
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
919
985
  async def get_agent_by_id_async(
920
986
  self,
921
987
  agent_id: str,
922
988
  actor: PydanticUser,
923
989
  include_relationships: Optional[List[str]] = None,
990
+ include: List[str] = [],
924
991
  ) -> PydanticAgentState:
925
992
  """Fetch an agent by its ID."""
993
+
926
994
  async with db_registry.async_session() as session:
927
995
  try:
928
996
  query = select(AgentModel)
929
997
  query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
930
998
  query = query.where(AgentModel.id == agent_id)
931
- query = _apply_relationship_filters(query, include_relationships)
999
+ query = _apply_relationship_filters(query, include_relationships, include)
932
1000
 
933
1001
  result = await session.execute(query)
934
1002
  agent = result.scalar_one_or_none()
@@ -936,7 +1004,7 @@ class AgentManager:
936
1004
  if agent is None:
937
1005
  raise NoResultFound(f"Agent with ID {agent_id} not found")
938
1006
 
939
- return await agent.to_pydantic_async(include_relationships=include_relationships)
1007
+ return await agent.to_pydantic_async(include_relationships=include_relationships, include=include)
940
1008
  except NoResultFound:
941
1009
  # Re-raise NoResultFound without logging to preserve 404 handling
942
1010
  raise
@@ -974,6 +1042,7 @@ class AgentManager:
974
1042
 
975
1043
  @enforce_types
976
1044
  @trace_method
1045
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
977
1046
  async def get_agent_archive_ids_async(self, agent_id: str, actor: PydanticUser) -> List[str]:
978
1047
  """Get all archive IDs associated with an agent."""
979
1048
  from letta.orm import ArchivesAgents
@@ -987,6 +1056,25 @@ class AgentManager:
987
1056
 
988
1057
  @enforce_types
989
1058
  @trace_method
1059
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1060
+ async def validate_agent_exists_async(self, agent_id: str, actor: PydanticUser) -> None:
1061
+ """
1062
+ Validate that an agent exists and user has access to it.
1063
+ Lightweight method that doesn't load the full agent object.
1064
+
1065
+ Args:
1066
+ agent_id: ID of the agent to validate
1067
+ actor: User performing the action
1068
+
1069
+ Raises:
1070
+ LettaAgentNotFoundError: If agent doesn't exist or user doesn't have access
1071
+ """
1072
+ async with db_registry.async_session() as session:
1073
+ await validate_agent_exists_async(session, agent_id, actor)
1074
+
1075
+ @enforce_types
1076
+ @trace_method
1077
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
990
1078
  async def delete_agent_async(self, agent_id: str, actor: PydanticUser) -> None:
991
1079
  """
992
1080
  Deletes an agent and its associated relationships.
@@ -1049,6 +1137,7 @@ class AgentManager:
1049
1137
  # TODO: This can also be made more efficient, instead of getting, setting, we can do it all in one db session for one query.
1050
1138
  @enforce_types
1051
1139
  @trace_method
1140
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1052
1141
  async def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
1053
1142
  agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
1054
1143
  return await self.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor)
@@ -1061,6 +1150,7 @@ class AgentManager:
1061
1150
 
1062
1151
  @enforce_types
1063
1152
  @trace_method
1153
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1064
1154
  async def get_system_message_async(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
1065
1155
  agent = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=[], actor=actor)
1066
1156
  return await self.message_manager.get_message_by_id_async(message_id=agent.message_ids[0], actor=actor)
@@ -1091,7 +1181,7 @@ class AgentManager:
1091
1181
 
1092
1182
  # note: we only update the system prompt if the core memory is changed
1093
1183
  # this means that the archival/recall memory statistics may be someout out of date
1094
- curr_memory_str = agent_state.memory.compile(sources=agent_state.sources)
1184
+ curr_memory_str = agent_state.memory.compile(sources=agent_state.sources, llm_config=agent_state.llm_config)
1095
1185
  if curr_memory_str in curr_system_message_openai["content"] and not force:
1096
1186
  # NOTE: could this cause issues if a block is removed? (substring match would still work)
1097
1187
  logger.debug(
@@ -1120,6 +1210,7 @@ class AgentManager:
1120
1210
  archival_memory_size=num_archival_memories,
1121
1211
  sources=agent_state.sources,
1122
1212
  max_files_open=agent_state.max_files_open,
1213
+ llm_config=agent_state.llm_config,
1123
1214
  )
1124
1215
 
1125
1216
  diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
@@ -1165,7 +1256,10 @@ class AgentManager:
1165
1256
 
1166
1257
  tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
1167
1258
 
1168
- curr_system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
1259
+ if agent_state.message_ids == []:
1260
+ curr_system_message = None
1261
+ else:
1262
+ curr_system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
1169
1263
 
1170
1264
  if curr_system_message is None:
1171
1265
  logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
@@ -1179,6 +1273,7 @@ class AgentManager:
1179
1273
  sources=agent_state.sources,
1180
1274
  tool_usage_rules=tool_rules_solver.compile_tool_rule_prompts(),
1181
1275
  max_files_open=agent_state.max_files_open,
1276
+ llm_config=agent_state.llm_config,
1182
1277
  )
1183
1278
  if curr_memory_str in curr_system_message_openai["content"] and not force:
1184
1279
  # NOTE: could this cause issues if a block is removed? (substring match would still work)
@@ -1237,6 +1332,7 @@ class AgentManager:
1237
1332
 
1238
1333
  @enforce_types
1239
1334
  @trace_method
1335
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1240
1336
  async def set_in_context_messages_async(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
1241
1337
  return await self.update_agent_async(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
1242
1338
 
@@ -1347,6 +1443,7 @@ class AgentManager:
1347
1443
 
1348
1444
  @enforce_types
1349
1445
  @trace_method
1446
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1350
1447
  async def update_memory_if_changed_async(self, agent_id: str, new_memory: Memory, actor: PydanticUser) -> PydanticAgentState:
1351
1448
  """
1352
1449
  Update internal memory object and system prompt if there have been modifications.
@@ -1366,6 +1463,7 @@ class AgentManager:
1366
1463
  sources=agent_state.sources,
1367
1464
  tool_usage_rules=temp_tool_rules_solver.compile_tool_rule_prompts(),
1368
1465
  max_files_open=agent_state.max_files_open,
1466
+ llm_config=agent_state.llm_config,
1369
1467
  )
1370
1468
  if new_memory_str not in system_message.content[0].text:
1371
1469
  # update the blocks (LRW) in the DB
@@ -1458,6 +1556,8 @@ class AgentManager:
1458
1556
  # ======================================================================================================================
1459
1557
  @enforce_types
1460
1558
  @trace_method
1559
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1560
+ @raise_on_invalid_id(param_name="source_id", expected_prefix=PrimitiveType.SOURCE)
1461
1561
  async def attach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
1462
1562
  """
1463
1563
  Attaches a source to an agent.
@@ -1527,6 +1627,7 @@ class AgentManager:
1527
1627
 
1528
1628
  @enforce_types
1529
1629
  @trace_method
1630
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1530
1631
  async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser):
1531
1632
  """
1532
1633
  Async version of append_system_message.
@@ -1614,6 +1715,8 @@ class AgentManager:
1614
1715
 
1615
1716
  @enforce_types
1616
1717
  @trace_method
1718
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1719
+ @raise_on_invalid_id(param_name="source_id", expected_prefix=PrimitiveType.SOURCE)
1617
1720
  async def detach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
1618
1721
  """
1619
1722
  Detaches a source from an agent.
@@ -1699,6 +1802,8 @@ class AgentManager:
1699
1802
 
1700
1803
  @enforce_types
1701
1804
  @trace_method
1805
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
1806
+ @raise_on_invalid_id(param_name="block_id", expected_prefix=PrimitiveType.BLOCK)
1702
1807
  async def attach_block_async(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
1703
1808
  """Attaches a block to an agent. For sleeptime agents, also attaches to paired agents in the same group."""
1704
1809
  async with db_registry.async_session() as session:
@@ -1849,9 +1954,8 @@ class AgentManager:
1849
1954
  """
1850
1955
  import warnings
1851
1956
 
1852
- warnings.warn(
1957
+ logger.warning(
1853
1958
  "list_passages_async is deprecated. Use query_source_passages_async or query_agent_passages_async instead.",
1854
- DeprecationWarning,
1855
1959
  stacklevel=2,
1856
1960
  )
1857
1961
 
@@ -2285,8 +2389,9 @@ class AgentManager:
2285
2389
  # Tool Management
2286
2390
  # ======================================================================================================================
2287
2391
  @enforce_types
2288
- @enforce_types
2289
2392
  @trace_method
2393
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2394
+ @raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
2290
2395
  async def attach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
2291
2396
  """
2292
2397
  Attaches a tool to an agent.
@@ -2355,6 +2460,7 @@ class AgentManager:
2355
2460
 
2356
2461
  @enforce_types
2357
2462
  @trace_method
2463
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2358
2464
  async def bulk_attach_tools_async(self, agent_id: str, tool_ids: List[str], actor: PydanticUser) -> None:
2359
2465
  """
2360
2466
  Efficiently attaches multiple tools to an agent in a single operation.
@@ -2520,6 +2626,8 @@ class AgentManager:
2520
2626
 
2521
2627
  @enforce_types
2522
2628
  @trace_method
2629
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2630
+ @raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
2523
2631
  async def detach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
2524
2632
  """
2525
2633
  Detaches a tool from an agent.
@@ -2549,6 +2657,7 @@ class AgentManager:
2549
2657
 
2550
2658
  @enforce_types
2551
2659
  @trace_method
2660
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2552
2661
  async def bulk_detach_tools_async(self, agent_id: str, tool_ids: List[str], actor: PydanticUser) -> None:
2553
2662
  """
2554
2663
  Efficiently detaches multiple tools from an agent in a single operation.
@@ -2585,6 +2694,7 @@ class AgentManager:
2585
2694
 
2586
2695
  @enforce_types
2587
2696
  @trace_method
2697
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2588
2698
  async def modify_approvals_async(self, agent_id: str, tool_name: str, requires_approval: bool, actor: PydanticUser) -> None:
2589
2699
  def is_target_rule(rule):
2590
2700
  return rule.tool_name == tool_name and rule.type == "requires_approval"
@@ -2933,6 +3043,7 @@ class AgentManager:
2933
3043
 
2934
3044
  @enforce_types
2935
3045
  @trace_method
3046
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2936
3047
  async def get_agent_files_config_async(self, agent_id: str, actor: PydanticUser) -> Tuple[int, int]:
2937
3048
  """Get per_file_view_window_char_limit and max_files_open for an agent.
2938
3049
 
@@ -2989,6 +3100,7 @@ class AgentManager:
2989
3100
 
2990
3101
  @enforce_types
2991
3102
  @trace_method
3103
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
2992
3104
  async def get_agent_max_files_open_async(self, agent_id: str, actor: PydanticUser) -> int:
2993
3105
  """Get max_files_open for an agent.
2994
3106
 
@@ -3017,6 +3129,7 @@ class AgentManager:
3017
3129
 
3018
3130
  @enforce_types
3019
3131
  @trace_method
3132
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
3020
3133
  async def get_agent_per_file_view_window_char_limit_async(self, agent_id: str, actor: PydanticUser) -> int:
3021
3134
  """Get per_file_view_window_char_limit for an agent.
3022
3135
 
@@ -3043,14 +3156,23 @@ class AgentManager:
3043
3156
 
3044
3157
  return row
3045
3158
 
3159
+ @enforce_types
3046
3160
  @trace_method
3161
+ @raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
3047
3162
  async def get_context_window(self, agent_id: str, actor: PydanticUser) -> ContextWindowOverview:
3048
3163
  agent_state, system_message, num_messages, num_archival_memories = await self.rebuild_system_prompt_async(
3049
3164
  agent_id=agent_id, actor=actor, force=True, dry_run=True
3050
3165
  )
3051
3166
  calculator = ContextWindowCalculator()
3052
3167
 
3053
- if settings.environment == "PRODUCTION" or agent_state.llm_config.model_endpoint_type == "anthropic":
3168
+ # Use Anthropic token counter if:
3169
+ # 1. The model endpoint type is anthropic, OR
3170
+ # 2. We're in PRODUCTION and anthropic_api_key is available
3171
+ use_anthropic = agent_state.llm_config.model_endpoint_type == "anthropic" or (
3172
+ settings.environment == "PRODUCTION" and model_settings.anthropic_api_key is not None
3173
+ )
3174
+
3175
+ if use_anthropic:
3054
3176
  anthropic_client = LLMClient.create(provider_type=ProviderType.anthropic, actor=actor)
3055
3177
  model = agent_state.llm_config.model if agent_state.llm_config.model_endpoint_type == "anthropic" else None
3056
3178
 
@@ -452,6 +452,7 @@ class AgentSerializationManager:
452
452
  schema: AgentFileSchema,
453
453
  actor: User,
454
454
  append_copy_suffix: bool = False,
455
+ override_name: Optional[str] = None,
455
456
  override_existing_tools: bool = True,
456
457
  dry_run: bool = False,
457
458
  env_vars: Optional[Dict[str, Any]] = None,
@@ -658,7 +659,11 @@ class AgentSerializationManager:
658
659
 
659
660
  # Convert AgentSchema back to CreateAgent, remapping tool/block IDs
660
661
  agent_data = agent_schema.model_dump(exclude={"id", "in_context_message_ids", "messages"})
661
- if append_copy_suffix:
662
+
663
+ # Handle agent name override: override_name takes precedence over append_copy_suffix
664
+ if override_name:
665
+ agent_data["name"] = override_name
666
+ elif append_copy_suffix:
662
667
  agent_data["name"] = agent_data.get("name") + "_copy"
663
668
 
664
669
  # Remap tool_ids from file IDs to database IDs