letta-nightly 0.11.3.dev20250820104219__py3-none-any.whl → 0.11.4.dev20250821104215__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 (90) hide show
  1. letta/__init__.py +1 -1
  2. letta/agents/helpers.py +4 -0
  3. letta/agents/letta_agent.py +142 -5
  4. letta/constants.py +10 -7
  5. letta/data_sources/connectors.py +70 -53
  6. letta/embeddings.py +3 -240
  7. letta/errors.py +28 -0
  8. letta/functions/function_sets/base.py +4 -4
  9. letta/functions/functions.py +287 -32
  10. letta/functions/mcp_client/types.py +11 -0
  11. letta/functions/schema_validator.py +187 -0
  12. letta/functions/typescript_parser.py +196 -0
  13. letta/helpers/datetime_helpers.py +8 -4
  14. letta/helpers/tool_execution_helper.py +25 -2
  15. letta/llm_api/anthropic_client.py +23 -18
  16. letta/llm_api/azure_client.py +73 -0
  17. letta/llm_api/bedrock_client.py +8 -4
  18. letta/llm_api/google_vertex_client.py +14 -5
  19. letta/llm_api/llm_api_tools.py +2 -217
  20. letta/llm_api/llm_client.py +15 -1
  21. letta/llm_api/llm_client_base.py +32 -1
  22. letta/llm_api/openai.py +1 -0
  23. letta/llm_api/openai_client.py +18 -28
  24. letta/llm_api/together_client.py +55 -0
  25. letta/orm/provider.py +1 -0
  26. letta/orm/step_metrics.py +40 -1
  27. letta/otel/db_pool_monitoring.py +1 -1
  28. letta/schemas/agent.py +3 -4
  29. letta/schemas/agent_file.py +2 -0
  30. letta/schemas/block.py +11 -5
  31. letta/schemas/embedding_config.py +4 -5
  32. letta/schemas/enums.py +1 -1
  33. letta/schemas/job.py +2 -3
  34. letta/schemas/llm_config.py +79 -7
  35. letta/schemas/mcp.py +0 -24
  36. letta/schemas/message.py +0 -108
  37. letta/schemas/openai/chat_completion_request.py +1 -0
  38. letta/schemas/providers/__init__.py +0 -2
  39. letta/schemas/providers/anthropic.py +106 -8
  40. letta/schemas/providers/azure.py +102 -8
  41. letta/schemas/providers/base.py +10 -3
  42. letta/schemas/providers/bedrock.py +28 -16
  43. letta/schemas/providers/letta.py +3 -3
  44. letta/schemas/providers/ollama.py +2 -12
  45. letta/schemas/providers/openai.py +4 -4
  46. letta/schemas/providers/together.py +14 -2
  47. letta/schemas/sandbox_config.py +2 -1
  48. letta/schemas/tool.py +46 -22
  49. letta/server/rest_api/routers/v1/agents.py +179 -38
  50. letta/server/rest_api/routers/v1/folders.py +13 -8
  51. letta/server/rest_api/routers/v1/providers.py +10 -3
  52. letta/server/rest_api/routers/v1/sources.py +14 -8
  53. letta/server/rest_api/routers/v1/steps.py +17 -1
  54. letta/server/rest_api/routers/v1/tools.py +96 -5
  55. letta/server/rest_api/streaming_response.py +91 -45
  56. letta/server/server.py +27 -38
  57. letta/services/agent_manager.py +92 -20
  58. letta/services/agent_serialization_manager.py +11 -7
  59. letta/services/context_window_calculator/context_window_calculator.py +40 -2
  60. letta/services/helpers/agent_manager_helper.py +73 -12
  61. letta/services/mcp_manager.py +109 -15
  62. letta/services/passage_manager.py +28 -109
  63. letta/services/provider_manager.py +24 -0
  64. letta/services/step_manager.py +68 -0
  65. letta/services/summarizer/summarizer.py +1 -4
  66. letta/services/tool_executor/core_tool_executor.py +1 -1
  67. letta/services/tool_executor/sandbox_tool_executor.py +26 -9
  68. letta/services/tool_manager.py +82 -5
  69. letta/services/tool_sandbox/base.py +3 -11
  70. letta/services/tool_sandbox/modal_constants.py +17 -0
  71. letta/services/tool_sandbox/modal_deployment_manager.py +242 -0
  72. letta/services/tool_sandbox/modal_sandbox.py +218 -3
  73. letta/services/tool_sandbox/modal_sandbox_v2.py +429 -0
  74. letta/services/tool_sandbox/modal_version_manager.py +273 -0
  75. letta/services/tool_sandbox/safe_pickle.py +193 -0
  76. letta/settings.py +5 -3
  77. letta/templates/sandbox_code_file.py.j2 +2 -4
  78. letta/templates/sandbox_code_file_async.py.j2 +2 -4
  79. letta/utils.py +1 -1
  80. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250821104215.dist-info}/METADATA +2 -2
  81. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250821104215.dist-info}/RECORD +84 -81
  82. letta/llm_api/anthropic.py +0 -1206
  83. letta/llm_api/aws_bedrock.py +0 -104
  84. letta/llm_api/azure_openai.py +0 -118
  85. letta/llm_api/azure_openai_constants.py +0 -11
  86. letta/llm_api/cohere.py +0 -391
  87. letta/schemas/providers/cohere.py +0 -18
  88. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250821104215.dist-info}/LICENSE +0 -0
  89. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250821104215.dist-info}/WHEEL +0 -0
  90. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250821104215.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ import asyncio
6
6
  import json
7
7
  from collections.abc import AsyncIterator
8
8
 
9
+ import anyio
9
10
  from fastapi.responses import StreamingResponse
10
11
  from starlette.types import Send
11
12
 
@@ -15,6 +16,7 @@ from letta.schemas.letta_ping import LettaPing
15
16
  from letta.schemas.user import User
16
17
  from letta.server.rest_api.utils import capture_sentry_exception
17
18
  from letta.services.job_manager import JobManager
19
+ from letta.settings import settings
18
20
 
19
21
  logger = get_logger(__name__)
20
22
 
@@ -175,8 +177,24 @@ class StreamingResponseWithStatusCode(StreamingResponse):
175
177
 
176
178
  body_iterator: AsyncIterator[str | bytes]
177
179
  response_started: bool = False
180
+ _client_connected: bool = True
178
181
 
179
182
  async def stream_response(self, send: Send) -> None:
183
+ if settings.use_asyncio_shield:
184
+ try:
185
+ await asyncio.shield(self._protected_stream_response(send))
186
+ except asyncio.CancelledError:
187
+ logger.info(f"Stream response was cancelled, but shielded task should continue")
188
+ except anyio.ClosedResourceError:
189
+ logger.info("Client disconnected, but shielded task should continue")
190
+ self._client_connected = False
191
+ except Exception as e:
192
+ logger.error(f"Error in protected stream response: {e}")
193
+ raise
194
+ else:
195
+ await self._protected_stream_response(send)
196
+
197
+ async def _protected_stream_response(self, send: Send) -> None:
180
198
  more_body = True
181
199
  try:
182
200
  first_chunk = await self.body_iterator.__anext__()
@@ -188,21 +206,25 @@ class StreamingResponseWithStatusCode(StreamingResponse):
188
206
  if isinstance(first_chunk_content, str):
189
207
  first_chunk_content = first_chunk_content.encode(self.charset)
190
208
 
191
- await send(
192
- {
193
- "type": "http.response.start",
194
- "status": self.status_code,
195
- "headers": self.raw_headers,
196
- }
197
- )
198
- self.response_started = True
199
- await send(
200
- {
201
- "type": "http.response.body",
202
- "body": first_chunk_content,
203
- "more_body": more_body,
204
- }
205
- )
209
+ try:
210
+ await send(
211
+ {
212
+ "type": "http.response.start",
213
+ "status": self.status_code,
214
+ "headers": self.raw_headers,
215
+ }
216
+ )
217
+ self.response_started = True
218
+ await send(
219
+ {
220
+ "type": "http.response.body",
221
+ "body": first_chunk_content,
222
+ "more_body": more_body,
223
+ }
224
+ )
225
+ except anyio.ClosedResourceError:
226
+ logger.info("Client disconnected during initial response, continuing processing without sending more chunks")
227
+ self._client_connected = False
206
228
 
207
229
  async for chunk in self.body_iterator:
208
230
  if isinstance(chunk, tuple):
@@ -219,13 +241,21 @@ class StreamingResponseWithStatusCode(StreamingResponse):
219
241
  if isinstance(content, str):
220
242
  content = content.encode(self.charset)
221
243
  more_body = True
222
- await send(
223
- {
224
- "type": "http.response.body",
225
- "body": content,
226
- "more_body": more_body,
227
- }
228
- )
244
+
245
+ # Only attempt to send if client is still connected
246
+ if self._client_connected:
247
+ try:
248
+ await send(
249
+ {
250
+ "type": "http.response.body",
251
+ "body": content,
252
+ "more_body": more_body,
253
+ }
254
+ )
255
+ except anyio.ClosedResourceError:
256
+ logger.info("Client disconnected, continuing processing without sending more data")
257
+ self._client_connected = False
258
+ # Continue processing but don't try to send more data
229
259
 
230
260
  # Handle explicit job cancellations (should not throw error)
231
261
  except JobCancelledException as exc:
@@ -243,13 +273,17 @@ class StreamingResponseWithStatusCode(StreamingResponse):
243
273
  }
244
274
  )
245
275
  raise
246
- await send(
247
- {
248
- "type": "http.response.body",
249
- "body": cancellation_event,
250
- "more_body": more_body,
251
- }
252
- )
276
+ if self._client_connected:
277
+ try:
278
+ await send(
279
+ {
280
+ "type": "http.response.body",
281
+ "body": cancellation_event,
282
+ "more_body": more_body,
283
+ }
284
+ )
285
+ except anyio.ClosedResourceError:
286
+ self._client_connected = False
253
287
  return
254
288
 
255
289
  # Handle client timeouts (should throw error to inform user)
@@ -268,13 +302,17 @@ class StreamingResponseWithStatusCode(StreamingResponse):
268
302
  }
269
303
  )
270
304
  raise
271
- await send(
272
- {
273
- "type": "http.response.body",
274
- "body": error_event,
275
- "more_body": more_body,
276
- }
277
- )
305
+ if self._client_connected:
306
+ try:
307
+ await send(
308
+ {
309
+ "type": "http.response.body",
310
+ "body": error_event,
311
+ "more_body": more_body,
312
+ }
313
+ )
314
+ except anyio.ClosedResourceError:
315
+ self._client_connected = False
278
316
  capture_sentry_exception(exc)
279
317
  return
280
318
 
@@ -293,14 +331,22 @@ class StreamingResponseWithStatusCode(StreamingResponse):
293
331
  }
294
332
  )
295
333
  raise
296
- await send(
297
- {
298
- "type": "http.response.body",
299
- "body": error_event,
300
- "more_body": more_body,
301
- }
302
- )
334
+ if self._client_connected:
335
+ try:
336
+ await send(
337
+ {
338
+ "type": "http.response.body",
339
+ "body": error_event,
340
+ "more_body": more_body,
341
+ }
342
+ )
343
+ except anyio.ClosedResourceError:
344
+ self._client_connected = False
345
+
303
346
  capture_sentry_exception(exc)
304
347
  return
305
- if more_body:
306
- await send({"type": "http.response.body", "body": b"", "more_body": False})
348
+ if more_body and self._client_connected:
349
+ try:
350
+ await send({"type": "http.response.body", "body": b"", "more_body": False})
351
+ except anyio.ClosedResourceError:
352
+ self._client_connected = False
letta/server/server.py CHANGED
@@ -23,7 +23,8 @@ from letta.config import LettaConfig
23
23
  from letta.constants import LETTA_TOOL_EXECUTION_DIR
24
24
  from letta.data_sources.connectors import DataConnector, load_data
25
25
  from letta.errors import HandleNotFoundError
26
- from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig
26
+ from letta.functions.mcp_client.types import MCPServerType, MCPTool, MCPToolHealth, SSEServerConfig, StdioServerConfig
27
+ from letta.functions.schema_validator import validate_complete_json_schema
27
28
  from letta.groups.helpers import load_multi_agent
28
29
  from letta.helpers.datetime_helpers import get_utc_time
29
30
  from letta.helpers.json_helpers import json_dumps, json_loads
@@ -40,7 +41,7 @@ from letta.schemas.block import Block, BlockUpdate, CreateBlock
40
41
  from letta.schemas.embedding_config import EmbeddingConfig
41
42
 
42
43
  # openai schemas
43
- from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType
44
+ from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType, ToolSourceType
44
45
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
45
46
  from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
46
47
  from letta.schemas.job import Job, JobUpdate
@@ -856,6 +857,9 @@ class SyncServer(Server):
856
857
  request.llm_config = await self.get_cached_llm_config_async(actor=actor, **config_params)
857
858
  log_event(name="end get_cached_llm_config", attributes=config_params)
858
859
 
860
+ if request.reasoning is None:
861
+ request.reasoning = request.llm_config.enable_reasoner or request.llm_config.put_inner_thoughts_in_kwargs
862
+
859
863
  if request.embedding_config is None:
860
864
  if request.embedding is None:
861
865
  if settings.default_embedding_handle is None:
@@ -1099,33 +1103,6 @@ class SyncServer(Server):
1099
1103
  def get_recall_memory_summary(self, agent_id: str, actor: User) -> RecallMemorySummary:
1100
1104
  return RecallMemorySummary(size=self.message_manager.size(actor=actor, agent_id=agent_id))
1101
1105
 
1102
- def get_agent_archival(
1103
- self,
1104
- user_id: str,
1105
- agent_id: str,
1106
- after: Optional[str] = None,
1107
- before: Optional[str] = None,
1108
- limit: Optional[int] = 100,
1109
- order_by: Optional[str] = "created_at",
1110
- reverse: Optional[bool] = False,
1111
- query_text: Optional[str] = None,
1112
- ascending: Optional[bool] = True,
1113
- ) -> List[Passage]:
1114
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
1115
- actor = self.user_manager.get_user_or_default(user_id=user_id)
1116
-
1117
- # iterate over records
1118
- records = self.agent_manager.list_passages(
1119
- actor=actor,
1120
- agent_id=agent_id,
1121
- after=after,
1122
- query_text=query_text,
1123
- before=before,
1124
- ascending=ascending,
1125
- limit=limit,
1126
- )
1127
- return records
1128
-
1129
1106
  async def get_agent_archival_async(
1130
1107
  self,
1131
1108
  agent_id: str,
@@ -1153,7 +1130,7 @@ class SyncServer(Server):
1153
1130
  agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
1154
1131
 
1155
1132
  # Insert passages into the archive
1156
- passages = await self.passage_manager.insert_passage_async(agent_state=agent_state, text=memory_contents, actor=actor)
1133
+ passages = await self.passage_manager.insert_passage(agent_state=agent_state, text=memory_contents, actor=actor)
1157
1134
 
1158
1135
  return passages
1159
1136
 
@@ -1471,10 +1448,6 @@ class SyncServer(Server):
1471
1448
  passage_count, document_count = await load_data(connector, source, self.passage_manager, self.file_manager, actor=actor)
1472
1449
  return passage_count, document_count
1473
1450
 
1474
- def list_data_source_passages(self, user_id: str, source_id: str) -> List[Passage]:
1475
- # TODO: move this query into PassageManager
1476
- return self.agent_manager.list_passages(actor=self.user_manager.get_user_or_default(user_id=user_id), source_id=source_id)
1477
-
1478
1451
  def list_all_sources(self, actor: User) -> List[Source]:
1479
1452
  # TODO: legacy: remove
1480
1453
  """List all sources (w/ extra metadata) belonging to a user"""
@@ -1934,12 +1907,19 @@ class SyncServer(Server):
1934
1907
  pip_requirements: Optional[List[PipRequirement]] = None,
1935
1908
  ) -> ToolReturnMessage:
1936
1909
  """Run a tool from source code"""
1937
- if tool_source_type is not None and tool_source_type != "python":
1938
- raise ValueError("Only Python source code is supported at this time")
1910
+
1911
+ if tool_source_type not in (None, ToolSourceType.python, ToolSourceType.typescript):
1912
+ raise ValueError("Tool source type is not supported at this time. Found {tool_source_type}")
1939
1913
 
1940
1914
  # If tools_json_schema is explicitly passed in, override it on the created Tool object
1941
1915
  if tool_json_schema:
1942
- tool = Tool(name=tool_name, source_code=tool_source, json_schema=tool_json_schema, pip_requirements=pip_requirements)
1916
+ tool = Tool(
1917
+ name=tool_name,
1918
+ source_code=tool_source,
1919
+ json_schema=tool_json_schema,
1920
+ pip_requirements=pip_requirements,
1921
+ source_type=tool_source_type,
1922
+ )
1943
1923
  else:
1944
1924
  # NOTE: we're creating a floating Tool object and NOT persisting to DB
1945
1925
  tool = Tool(
@@ -1947,6 +1927,7 @@ class SyncServer(Server):
1947
1927
  source_code=tool_source,
1948
1928
  args_json_schema=tool_args_json_schema,
1949
1929
  pip_requirements=pip_requirements,
1930
+ source_type=tool_source_type,
1950
1931
  )
1951
1932
 
1952
1933
  assert tool.name is not None, "Failed to create tool object"
@@ -2086,7 +2067,15 @@ class SyncServer(Server):
2086
2067
  if mcp_server_name not in self.mcp_clients:
2087
2068
  raise ValueError(f"No client was created for MCP server: {mcp_server_name}")
2088
2069
 
2089
- return await self.mcp_clients[mcp_server_name].list_tools()
2070
+ tools = await self.mcp_clients[mcp_server_name].list_tools()
2071
+
2072
+ # Add health information to each tool
2073
+ for tool in tools:
2074
+ if tool.inputSchema:
2075
+ health_status, reasons = validate_complete_json_schema(tool.inputSchema)
2076
+ tool.health = MCPToolHealth(status=health_status.value, reasons=reasons)
2077
+
2078
+ return tools
2090
2079
 
2091
2080
  async def add_mcp_server_to_config(
2092
2081
  self, server_config: Union[SSEServerConfig, StdioServerConfig], allow_upsert: bool = True
@@ -19,8 +19,9 @@ from letta.constants import (
19
19
  DEFAULT_MAX_FILES_OPEN,
20
20
  DEFAULT_TIMEZONE,
21
21
  DEPRECATED_LETTA_TOOLS,
22
- EXCLUDED_PROVIDERS_FROM_BASE_TOOL_RULES,
22
+ EXCLUDE_MODEL_KEYWORDS_FROM_BASE_TOOL_RULES,
23
23
  FILES_TOOLS,
24
+ INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES,
24
25
  )
25
26
  from letta.helpers import ToolRulesSolver
26
27
  from letta.helpers.datetime_helpers import get_utc_time
@@ -117,6 +118,21 @@ class AgentManager:
117
118
  self.identity_manager = IdentityManager()
118
119
  self.file_agent_manager = FileAgentManager()
119
120
 
121
+ @staticmethod
122
+ def _should_exclude_model_from_base_tool_rules(model: str) -> bool:
123
+ """Check if a model should be excluded from base tool rules based on model keywords."""
124
+ # First check if model contains any include keywords (overrides exclusion)
125
+ for include_keyword in INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES:
126
+ if include_keyword in model:
127
+ return False
128
+
129
+ # Then check if model contains any exclude keywords
130
+ for exclude_keyword in EXCLUDE_MODEL_KEYWORDS_FROM_BASE_TOOL_RULES:
131
+ if exclude_keyword in model:
132
+ return True
133
+
134
+ return False
135
+
120
136
  @staticmethod
121
137
  def _resolve_tools(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
122
138
  """
@@ -334,16 +350,16 @@ class AgentManager:
334
350
 
335
351
  tool_rules = list(agent_create.tool_rules or [])
336
352
 
337
- # Override include_base_tool_rules to False if provider is not in excluded set and include_base_tool_rules is not explicitly set to True
353
+ # Override include_base_tool_rules to False if model matches exclusion keywords and include_base_tool_rules is not explicitly set to True
338
354
  if (
339
355
  (
340
- agent_create.llm_config.model_endpoint_type in EXCLUDED_PROVIDERS_FROM_BASE_TOOL_RULES
356
+ self._should_exclude_model_from_base_tool_rules(agent_create.llm_config.model)
341
357
  and agent_create.include_base_tool_rules is None
342
358
  )
343
359
  and agent_create.agent_type != AgentType.sleeptime_agent
344
360
  ) or agent_create.include_base_tool_rules is False:
345
361
  agent_create.include_base_tool_rules = False
346
- logger.info(f"Overriding include_base_tool_rules to False for provider: {agent_create.llm_config.model_endpoint_type}")
362
+ logger.info(f"Overriding include_base_tool_rules to False for model: {agent_create.llm_config.model}")
347
363
  else:
348
364
  agent_create.include_base_tool_rules = True
349
365
 
@@ -543,16 +559,16 @@ class AgentManager:
543
559
  tool_names = set(name_to_id.keys()) # now canonical
544
560
  tool_rules = list(agent_create.tool_rules or [])
545
561
 
546
- # Override include_base_tool_rules to False if provider is not in excluded set and include_base_tool_rules is not explicitly set to True
562
+ # Override include_base_tool_rules to False if model matches exclusion keywords and include_base_tool_rules is not explicitly set to True
547
563
  if (
548
564
  (
549
- agent_create.llm_config.model_endpoint_type in EXCLUDED_PROVIDERS_FROM_BASE_TOOL_RULES
565
+ self._should_exclude_model_from_base_tool_rules(agent_create.llm_config.model)
550
566
  and agent_create.include_base_tool_rules is None
551
567
  )
552
568
  and agent_create.agent_type != AgentType.sleeptime_agent
553
569
  ) or agent_create.include_base_tool_rules is False:
554
570
  agent_create.include_base_tool_rules = False
555
- logger.info(f"Overriding include_base_tool_rules to False for provider: {agent_create.llm_config.model_endpoint_type}")
571
+ logger.info(f"Overriding include_base_tool_rules to False for model: {agent_create.llm_config.model}")
556
572
  else:
557
573
  agent_create.include_base_tool_rules = True
558
574
 
@@ -630,6 +646,7 @@ class AgentManager:
630
646
  [{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
631
647
  )
632
648
 
649
+ env_rows = []
633
650
  if agent_create.tool_exec_environment_variables:
634
651
  env_rows = [
635
652
  {
@@ -640,7 +657,8 @@ class AgentManager:
640
657
  }
641
658
  for key, val in agent_create.tool_exec_environment_variables.items()
642
659
  ]
643
- await session.execute(insert(AgentEnvironmentVariable).values(env_rows))
660
+ result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
661
+ env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
644
662
 
645
663
  include_relationships = []
646
664
  if tool_ids:
@@ -656,6 +674,9 @@ class AgentManager:
656
674
 
657
675
  result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
658
676
 
677
+ if agent_create.tool_exec_environment_variables and env_rows:
678
+ result.tool_exec_environment_variables = [AgentEnvironmentVariable(**row) for row in env_rows]
679
+
659
680
  # initial message sequence (skip if _init_with_no_messages is True)
660
681
  if not _init_with_no_messages:
661
682
  init_messages = await self._generate_initial_message_sequence_async(
@@ -1986,6 +2007,26 @@ class AgentManager:
1986
2007
  @enforce_types
1987
2008
  @trace_method
1988
2009
  async def refresh_file_blocks(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
2010
+ """
2011
+ Refresh the file blocks in an agent's memory with current file content.
2012
+
2013
+ This method synchronizes the agent's in-memory file blocks with the actual
2014
+ file content from attached sources. It respects the per-file view window
2015
+ limit to prevent excessive memory usage.
2016
+
2017
+ Args:
2018
+ agent_state: The current agent state containing memory configuration
2019
+ actor: The user performing this action (for permission checking)
2020
+
2021
+ Returns:
2022
+ Updated agent state with refreshed file blocks
2023
+
2024
+ Important:
2025
+ - File blocks are truncated based on per_file_view_window_char_limit
2026
+ - None values are filtered out (files that couldn't be loaded)
2027
+ - This does NOT persist changes to the database, only updates the state object
2028
+ - Call this before agent interactions if files may have changed externally
2029
+ """
1989
2030
  file_blocks = await self.file_agent_manager.list_files_for_agent(
1990
2031
  agent_id=agent_state.id,
1991
2032
  per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit,
@@ -2035,6 +2076,28 @@ class AgentManager:
2035
2076
  @enforce_types
2036
2077
  @trace_method
2037
2078
  def append_system_message(self, agent_id: str, content: str, actor: PydanticUser):
2079
+ """
2080
+ Append a system message to an agent's in-context message history.
2081
+
2082
+ This method is typically used during agent initialization to add system prompts,
2083
+ instructions, or context that should be treated as system-level guidance.
2084
+ Unlike user messages, system messages directly influence the agent's behavior
2085
+ and understanding of its role.
2086
+
2087
+ Args:
2088
+ agent_id: The ID of the agent to append the message to
2089
+ content: The system message content (e.g., instructions, context, role definition)
2090
+ actor: The user performing this action (for permission checking)
2091
+
2092
+ Side Effects:
2093
+ - Creates a new Message object in the database
2094
+ - Updates the agent's in_context_message_ids list
2095
+ - The message becomes part of the agent's permanent context window
2096
+
2097
+ Note:
2098
+ System messages consume tokens in the context window and cannot be
2099
+ removed without rebuilding the agent's message history.
2100
+ """
2038
2101
 
2039
2102
  # get the agent
2040
2103
  agent = self.get_agent_by_id(agent_id=agent_id, actor=actor)
@@ -2048,6 +2111,15 @@ class AgentManager:
2048
2111
  @enforce_types
2049
2112
  @trace_method
2050
2113
  async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser):
2114
+ """
2115
+ Async version of append_system_message.
2116
+
2117
+ Append a system message to an agent's in-context message history.
2118
+ See append_system_message for detailed documentation.
2119
+
2120
+ This async version is preferred for high-throughput scenarios or when
2121
+ called within other async operations to avoid blocking the event loop.
2122
+ """
2051
2123
 
2052
2124
  # get the agent
2053
2125
  agent = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
@@ -2354,7 +2426,7 @@ class AgentManager:
2354
2426
 
2355
2427
  @enforce_types
2356
2428
  @trace_method
2357
- def list_passages(
2429
+ async def list_passages(
2358
2430
  self,
2359
2431
  actor: PydanticUser,
2360
2432
  agent_id: Optional[str] = None,
@@ -2372,8 +2444,8 @@ class AgentManager:
2372
2444
  agent_only: bool = False,
2373
2445
  ) -> List[PydanticPassage]:
2374
2446
  """Lists all passages attached to an agent."""
2375
- with db_registry.session() as session:
2376
- main_query = build_passage_query(
2447
+ async with db_registry.async_session() as session:
2448
+ main_query = await build_passage_query(
2377
2449
  actor=actor,
2378
2450
  agent_id=agent_id,
2379
2451
  file_id=file_id,
@@ -2394,7 +2466,7 @@ class AgentManager:
2394
2466
  main_query = main_query.limit(limit)
2395
2467
 
2396
2468
  # Execute query
2397
- result = session.execute(main_query)
2469
+ result = await session.execute(main_query)
2398
2470
 
2399
2471
  passages = []
2400
2472
  for row in result:
@@ -2437,7 +2509,7 @@ class AgentManager:
2437
2509
  ) -> List[PydanticPassage]:
2438
2510
  """Lists all passages attached to an agent."""
2439
2511
  async with db_registry.async_session() as session:
2440
- main_query = build_passage_query(
2512
+ main_query = await build_passage_query(
2441
2513
  actor=actor,
2442
2514
  agent_id=agent_id,
2443
2515
  file_id=file_id,
@@ -2500,7 +2572,7 @@ class AgentManager:
2500
2572
  ) -> List[PydanticPassage]:
2501
2573
  """Lists all passages attached to an agent."""
2502
2574
  async with db_registry.async_session() as session:
2503
- main_query = build_source_passage_query(
2575
+ main_query = await build_source_passage_query(
2504
2576
  actor=actor,
2505
2577
  agent_id=agent_id,
2506
2578
  file_id=file_id,
@@ -2546,7 +2618,7 @@ class AgentManager:
2546
2618
  ) -> List[PydanticPassage]:
2547
2619
  """Lists all passages attached to an agent."""
2548
2620
  async with db_registry.async_session() as session:
2549
- main_query = build_agent_passage_query(
2621
+ main_query = await build_agent_passage_query(
2550
2622
  actor=actor,
2551
2623
  agent_id=agent_id,
2552
2624
  query_text=query_text,
@@ -2574,7 +2646,7 @@ class AgentManager:
2574
2646
 
2575
2647
  @enforce_types
2576
2648
  @trace_method
2577
- def passage_size(
2649
+ async def passage_size(
2578
2650
  self,
2579
2651
  actor: PydanticUser,
2580
2652
  agent_id: Optional[str] = None,
@@ -2591,8 +2663,8 @@ class AgentManager:
2591
2663
  agent_only: bool = False,
2592
2664
  ) -> int:
2593
2665
  """Returns the count of passages matching the given criteria."""
2594
- with db_registry.session() as session:
2595
- main_query = build_passage_query(
2666
+ async with db_registry.async_session() as session:
2667
+ main_query = await build_passage_query(
2596
2668
  actor=actor,
2597
2669
  agent_id=agent_id,
2598
2670
  file_id=file_id,
@@ -2610,7 +2682,7 @@ class AgentManager:
2610
2682
 
2611
2683
  # Convert to count query
2612
2684
  count_query = select(func.count()).select_from(main_query.subquery())
2613
- return session.scalar(count_query) or 0
2685
+ return (await session.scalar(count_query)) or 0
2614
2686
 
2615
2687
  @enforce_types
2616
2688
  async def passage_size_async(
@@ -2630,7 +2702,7 @@ class AgentManager:
2630
2702
  agent_only: bool = False,
2631
2703
  ) -> int:
2632
2704
  async with db_registry.async_session() as session:
2633
- main_query = build_passage_query(
2705
+ main_query = await build_passage_query(
2634
2706
  actor=actor,
2635
2707
  agent_id=agent_id,
2636
2708
  file_id=file_id,
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
2
2
  from typing import Any, Dict, List, Optional
3
3
 
4
4
  from letta.constants import MCP_TOOL_TAG_NAME_PREFIX
5
- from letta.errors import AgentFileExportError, AgentFileImportError
5
+ from letta.errors import AgentExportIdMappingError, AgentExportProcessingError, AgentFileImportError, AgentNotFoundForExportError
6
6
  from letta.helpers.pinecone_utils import should_use_pinecone
7
7
  from letta.log import get_logger
8
8
  from letta.schemas.agent import AgentState, CreateAgent
@@ -118,10 +118,7 @@ class AgentSerializationManager:
118
118
  return self._db_to_file_ids[db_id]
119
119
 
120
120
  if not allow_new:
121
- raise AgentFileExportError(
122
- f"Unexpected new {entity_type} ID '{db_id}' encountered during conversion. "
123
- f"All IDs should have been mapped during agent processing."
124
- )
121
+ raise AgentExportIdMappingError(db_id, entity_type)
125
122
 
126
123
  file_id = self._generate_file_id(entity_type)
127
124
  self._db_to_file_ids[db_id] = file_id
@@ -352,7 +349,7 @@ class AgentSerializationManager:
352
349
  if len(agent_states) != len(agent_ids):
353
350
  found_ids = {agent.id for agent in agent_states}
354
351
  missing_ids = [agent_id for agent_id in agent_ids if agent_id not in found_ids]
355
- raise AgentFileExportError(f"The following agent IDs were not found: {missing_ids}")
352
+ raise AgentNotFoundForExportError(missing_ids)
356
353
 
357
354
  groups = []
358
355
  group_agent_ids = []
@@ -417,7 +414,7 @@ class AgentSerializationManager:
417
414
 
418
415
  except Exception as e:
419
416
  logger.error(f"Failed to export agent file: {e}")
420
- raise AgentFileExportError(f"Export failed: {e}") from e
417
+ raise AgentExportProcessingError(str(e), e) from e
421
418
 
422
419
  async def import_file(
423
420
  self,
@@ -657,6 +654,12 @@ class AgentSerializationManager:
657
654
  )
658
655
  imported_count += len(files_for_agent)
659
656
 
657
+ # Extract the imported agent IDs (database IDs)
658
+ imported_agent_ids = []
659
+ for agent_schema in schema.agents:
660
+ if agent_schema.id in file_to_db_ids:
661
+ imported_agent_ids.append(file_to_db_ids[agent_schema.id])
662
+
660
663
  for group in schema.groups:
661
664
  group_data = group.model_dump(exclude={"id"})
662
665
  group_data["agent_ids"] = [file_to_db_ids[agent_id] for agent_id in group_data["agent_ids"]]
@@ -670,6 +673,7 @@ class AgentSerializationManager:
670
673
  success=True,
671
674
  message=f"Import completed successfully. Imported {imported_count} entities.",
672
675
  imported_count=imported_count,
676
+ imported_agent_ids=imported_agent_ids,
673
677
  id_mappings=file_to_db_ids,
674
678
  )
675
679