letta-nightly 0.12.1.dev20251023104211__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.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251023104211.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.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
@@ -8,14 +8,16 @@ from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException,
8
8
  from fastapi.responses import JSONResponse
9
9
  from marshmallow import ValidationError
10
10
  from orjson import orjson
11
- from pydantic import BaseModel, Field
11
+ from pydantic import BaseModel, ConfigDict, Field
12
12
  from sqlalchemy.exc import IntegrityError, OperationalError
13
13
  from starlette.responses import Response, StreamingResponse
14
14
 
15
15
  from letta.agents.agent_loop import AgentLoop
16
+ from letta.agents.base_agent_v2 import BaseAgentV2
17
+ from letta.agents.letta_agent import LettaAgent
16
18
  from letta.agents.letta_agent_v2 import LettaAgentV2
17
- from letta.constants import AGENT_ID_PATTERN, DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
18
- from letta.data_sources.redis_client import NoopAsyncRedisClient, get_redis_client
19
+ from letta.constants import DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
20
+ from letta.data_sources.redis_client import get_redis_client
19
21
  from letta.errors import (
20
22
  AgentExportIdMappingError,
21
23
  AgentExportProcessingError,
@@ -28,11 +30,11 @@ from letta.log import get_logger
28
30
  from letta.orm.errors import NoResultFound
29
31
  from letta.otel.context import get_ctx_attributes
30
32
  from letta.otel.metric_registry import MetricRegistry
31
- from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
33
+ from letta.schemas.agent import AgentRelationships, AgentState, CreateAgent, UpdateAgent
32
34
  from letta.schemas.agent_file import AgentFileSchema
33
- from letta.schemas.block import Block, BlockUpdate
35
+ from letta.schemas.block import BaseBlock, Block, BlockUpdate
34
36
  from letta.schemas.enums import AgentType, RunStatus
35
- from letta.schemas.file import AgentFileAttachment, PaginatedAgentFiles
37
+ from letta.schemas.file import AgentFileAttachment, FileMetadataBase, PaginatedAgentFiles
36
38
  from letta.schemas.group import Group
37
39
  from letta.schemas.job import LettaRequestConfig
38
40
  from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion, MessageType
@@ -46,20 +48,20 @@ from letta.schemas.memory import (
46
48
  CreateArchivalMemory,
47
49
  Memory,
48
50
  )
49
- from letta.schemas.message import MessageCreate, MessageSearchRequest, MessageSearchResult
51
+ from letta.schemas.message import BaseMessage, MessageCreate, MessageCreateType, MessageSearchRequest, MessageSearchResult
50
52
  from letta.schemas.passage import Passage
51
53
  from letta.schemas.run import Run as PydanticRun, RunUpdate
52
- from letta.schemas.source import Source
53
- from letta.schemas.tool import Tool
54
+ from letta.schemas.source import BaseSource, Source
55
+ from letta.schemas.tool import BaseTool, Tool
54
56
  from letta.schemas.user import User
55
57
  from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
56
58
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
57
- from letta.server.rest_api.redis_stream_manager import create_background_stream_processor, redis_sse_stream_generator
58
59
  from letta.server.server import SyncServer
59
60
  from letta.services.lettuce import LettuceClient
60
61
  from letta.services.run_manager import RunManager
61
62
  from letta.settings import settings
62
- from letta.utils import safe_create_shielded_task, safe_create_task, truncate_file_visible_content
63
+ from letta.utils import is_1_0_sdk_version, safe_create_shielded_task, safe_create_task, truncate_file_visible_content
64
+ from letta.validators import AgentId, BlockId, FileId, MessageId, SourceId, ToolId
63
65
 
64
66
  # These can be forward refs, but because Fastapi needs them at runtime the must be imported normally
65
67
 
@@ -94,8 +96,13 @@ async def list_agents(
94
96
  "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. "
95
97
  "If not provided, all relationships are loaded by default. "
96
98
  "Using this can optimize performance by reducing unnecessary joins."
99
+ "This is a legacy parameter, and no longer supported after 1.0.0 SDK versions."
97
100
  ),
98
101
  ),
102
+ include: List[AgentRelationships] = Query(
103
+ [],
104
+ description=("Specify which relational fields to include in the response. No relationships are included by default."),
105
+ ),
99
106
  order: Literal["asc", "desc"] = Query(
100
107
  "desc", description="Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first"
101
108
  ),
@@ -126,6 +133,8 @@ async def list_agents(
126
133
  # Handle backwards compatibility - prefer new parameters over legacy ones
127
134
  final_ascending = (order == "asc") if order else ascending
128
135
  final_sort_by = order_by if order_by else sort_by
136
+ if include_relationships is None and is_1_0_sdk_version(headers):
137
+ include_relationships = [] # don't default include all if using new SDK version
129
138
 
130
139
  # Call list_agents directly without unnecessary dict handling
131
140
  return await server.agent_manager.list_agents_async(
@@ -143,6 +152,7 @@ async def list_agents(
143
152
  identity_id=identity_id,
144
153
  identifier_keys=identifier_keys,
145
154
  include_relationships=include_relationships,
155
+ include=include,
146
156
  ascending=final_ascending,
147
157
  sort_by=final_sort_by,
148
158
  show_hidden_agents=show_hidden_agents,
@@ -170,13 +180,14 @@ class IndentedORJSONResponse(Response):
170
180
 
171
181
  @router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent")
172
182
  async def export_agent(
173
- agent_id: str,
174
- max_steps: int = 100,
183
+ agent_id: str = AgentId,
184
+ max_steps: int = Query(100, deprecated=True),
175
185
  server: "SyncServer" = Depends(get_letta_server),
176
186
  headers: HeaderParams = Depends(get_headers),
177
187
  use_legacy_format: bool = Query(
178
188
  False,
179
- description="If true, exports using the legacy single-agent format (v1). If false, exports using the new multi-entity format (v2).",
189
+ description="If True, exports using the legacy single-agent 'v1' format with inline tools/blocks. If False, exports using the new multi-entity 'v2' format, with separate agents, tools, blocks, files, etc.",
190
+ deprecated=True,
180
191
  ),
181
192
  # do not remove, used to autogeneration of spec
182
193
  # TODO: Think of a better way to export AgentFileSchema
@@ -185,21 +196,12 @@ async def export_agent(
185
196
  ) -> JSONResponse:
186
197
  """
187
198
  Export the serialized JSON representation of an agent, formatted with indentation.
188
-
189
- Supports two export formats:
190
- - Legacy format (use_legacy_format=true): Single agent with inline tools/blocks
191
- - New format (default): Multi-entity format with separate agents, tools, blocks, files, etc.
192
199
  """
193
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
194
-
195
200
  if use_legacy_format:
196
- # Use the legacy serialization method
197
- agent = await server.agent_manager.serialize(agent_id=agent_id, actor=actor, max_steps=max_steps)
198
- return agent.model_dump()
199
- else:
200
- # Use the new multi-entity export format
201
- agent_file_schema = await server.agent_serialization_manager.export(agent_ids=[agent_id], actor=actor)
202
- return agent_file_schema.model_dump()
201
+ raise HTTPException(status_code=400, detail="Legacy format is not supported")
202
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
203
+ agent_file_schema = await server.agent_serialization_manager.export(agent_ids=[agent_id], actor=actor)
204
+ return agent_file_schema.model_dump()
203
205
 
204
206
 
205
207
  class ImportedAgentsResponse(BaseModel):
@@ -242,6 +244,7 @@ async def _import_agent(
242
244
  actor: User,
243
245
  # TODO: Support these fields for new agent file
244
246
  append_copy_suffix: bool = True,
247
+ override_name: Optional[str] = None,
245
248
  override_existing_tools: bool = True,
246
249
  project_id: str | None = None,
247
250
  strip_messages: bool = False,
@@ -262,6 +265,7 @@ async def _import_agent(
262
265
  schema=agent_schema,
263
266
  actor=actor,
264
267
  append_copy_suffix=append_copy_suffix,
268
+ override_name=override_name,
265
269
  override_existing_tools=override_existing_tools,
266
270
  env_vars=env_vars,
267
271
  override_embedding_config=embedding_config_override,
@@ -282,7 +286,15 @@ async def import_agent(
282
286
  server: "SyncServer" = Depends(get_letta_server),
283
287
  headers: HeaderParams = Depends(get_headers),
284
288
  x_override_embedding_model: str | None = Header(None, alias="x-override-embedding-model"),
285
- append_copy_suffix: bool = Form(True, description='If set to True, appends "_copy" to the end of the agent name.'),
289
+ append_copy_suffix: bool = Form(
290
+ True,
291
+ description='If set to True, appends "_copy" to the end of the agent name.',
292
+ deprecated=True,
293
+ ),
294
+ override_name: Optional[str] = Form(
295
+ None,
296
+ description="If provided, overrides the agent name with this value.",
297
+ ),
286
298
  override_existing_tools: bool = Form(
287
299
  True,
288
300
  description="If set to True, existing tools can get their source code overwritten by the uploaded tool definitions. Note that Letta core tools can never be updated externally.",
@@ -335,6 +347,7 @@ async def import_agent(
335
347
  server=server,
336
348
  actor=actor,
337
349
  append_copy_suffix=append_copy_suffix,
350
+ override_name=override_name,
338
351
  override_existing_tools=override_existing_tools,
339
352
  project_id=project_id,
340
353
  strip_messages=strip_messages,
@@ -351,9 +364,9 @@ async def import_agent(
351
364
  return ImportedAgentsResponse(agent_ids=agent_ids)
352
365
 
353
366
 
354
- @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="retrieve_agent_context_window")
367
+ @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="retrieve_agent_context_window", deprecated=True)
355
368
  async def retrieve_agent_context_window(
356
- agent_id: str,
369
+ agent_id: AgentId,
357
370
  server: "SyncServer" = Depends(get_letta_server),
358
371
  headers: HeaderParams = Depends(get_headers),
359
372
  ):
@@ -386,14 +399,14 @@ async def create_agent(
386
399
  Create an agent.
387
400
  """
388
401
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
389
- if headers.experimental_params.letta_v1_agent and agent.agent_type == AgentType.memgpt_v2_agent and not agent.enable_sleeptime:
402
+ if headers.experimental_params.letta_v1_agent and agent.agent_type == AgentType.memgpt_v2_agent:
390
403
  agent.agent_type = AgentType.letta_v1_agent
391
404
  return await server.create_agent_async(agent, actor=actor)
392
405
 
393
406
 
394
407
  @router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
395
408
  async def modify_agent(
396
- agent_id: str,
409
+ agent_id: AgentId,
397
410
  update_agent: UpdateAgent = Body(...),
398
411
  server: "SyncServer" = Depends(get_letta_server),
399
412
  headers: HeaderParams = Depends(get_headers),
@@ -403,9 +416,9 @@ async def modify_agent(
403
416
  return await server.update_agent_async(agent_id=agent_id, request=update_agent, actor=actor)
404
417
 
405
418
 
406
- @router.get("/{agent_id}/tools", response_model=list[Tool], operation_id="list_agent_tools")
407
- async def list_agent_tools(
408
- agent_id: str,
419
+ @router.get("/{agent_id}/tools", response_model=list[Tool], operation_id="list_tools_for_agent")
420
+ async def list_tools_for_agent(
421
+ agent_id: AgentId,
409
422
  server: "SyncServer" = Depends(get_letta_server),
410
423
  headers: HeaderParams = Depends(get_headers),
411
424
  before: Optional[str] = Query(
@@ -420,7 +433,7 @@ async def list_agent_tools(
420
433
  ),
421
434
  order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
422
435
  ):
423
- """Get tools from an existing agent"""
436
+ """Get tools from an existing agent."""
424
437
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
425
438
  return await server.agent_manager.list_attached_tools_async(
426
439
  agent_id=agent_id,
@@ -432,10 +445,10 @@ async def list_agent_tools(
432
445
  )
433
446
 
434
447
 
435
- @router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=AgentState, operation_id="attach_tool")
436
- async def attach_tool(
437
- agent_id: str,
438
- tool_id: str,
448
+ @router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=Optional[AgentState], operation_id="attach_tool_to_agent")
449
+ async def attach_tool_to_agent(
450
+ tool_id: ToolId,
451
+ agent_id: AgentId,
439
452
  server: "SyncServer" = Depends(get_letta_server),
440
453
  headers: HeaderParams = Depends(get_headers),
441
454
  ):
@@ -444,14 +457,16 @@ async def attach_tool(
444
457
  """
445
458
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
446
459
  await server.agent_manager.attach_tool_async(agent_id=agent_id, tool_id=tool_id, actor=actor)
460
+ if is_1_0_sdk_version(headers):
461
+ return None
447
462
  # TODO: Unfortunately we need this to preserve our current API behavior
448
463
  return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
449
464
 
450
465
 
451
- @router.patch("/{agent_id}/tools/detach/{tool_id}", response_model=AgentState, operation_id="detach_tool")
452
- async def detach_tool(
453
- agent_id: str,
454
- tool_id: str,
466
+ @router.patch("/{agent_id}/tools/detach/{tool_id}", response_model=Optional[AgentState], operation_id="detach_tool_from_agent")
467
+ async def detach_tool_from_agent(
468
+ tool_id: ToolId,
469
+ agent_id: AgentId,
455
470
  server: "SyncServer" = Depends(get_letta_server),
456
471
  headers: HeaderParams = Depends(get_headers),
457
472
  ):
@@ -460,33 +475,57 @@ async def detach_tool(
460
475
  """
461
476
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
462
477
  await server.agent_manager.detach_tool_async(agent_id=agent_id, tool_id=tool_id, actor=actor)
478
+ if is_1_0_sdk_version(headers):
479
+ return None
463
480
  # TODO: Unfortunately we need this to preserve our current API behavior
464
481
  return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
465
482
 
466
483
 
467
- @router.patch("/{agent_id}/tools/approval/{tool_name}", response_model=AgentState, operation_id="modify_approval")
468
- async def modify_approval(
469
- agent_id: str,
484
+ class ModifyApprovalRequest(BaseModel):
485
+ """Request body for modifying tool approval requirements."""
486
+
487
+ requires_approval: bool = Field(..., description="Whether the tool requires approval before execution")
488
+
489
+ model_config = ConfigDict(extra="forbid")
490
+
491
+
492
+ @router.patch("/{agent_id}/tools/approval/{tool_name}", response_model=Optional[AgentState], operation_id="modify_approval_for_tool")
493
+ async def modify_approval_for_tool(
470
494
  tool_name: str,
471
- requires_approval: bool,
495
+ agent_id: AgentId,
496
+ requires_approval: bool | None = Query(None, description="Whether the tool requires approval before execution", deprecated=True),
497
+ request: ModifyApprovalRequest | None = Body(None),
472
498
  server: "SyncServer" = Depends(get_letta_server),
473
499
  headers: HeaderParams = Depends(get_headers),
474
500
  ):
475
501
  """
476
- Attach a tool to an agent.
502
+ Modify the approval requirement for a tool attached to an agent.
503
+
504
+ Accepts requires_approval via request body (preferred) or query parameter (deprecated).
477
505
  """
506
+ # Prefer body over query param for backwards compatibility
507
+ if request is not None:
508
+ approval_value = request.requires_approval
509
+ elif requires_approval is not None:
510
+ approval_value = requires_approval
511
+ else:
512
+ raise HTTPException(
513
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
514
+ detail="requires_approval must be provided either in request body or as query parameter",
515
+ )
516
+
478
517
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
479
- await server.agent_manager.modify_approvals_async(
480
- agent_id=agent_id, tool_name=tool_name, requires_approval=requires_approval, actor=actor
481
- )
518
+ await server.agent_manager.modify_approvals_async(agent_id=agent_id, tool_name=tool_name, requires_approval=approval_value, actor=actor)
519
+ if is_1_0_sdk_version(headers):
520
+ return None
482
521
  # TODO: Unfortunately we need this to preserve our current API behavior
483
522
  return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
484
523
 
485
524
 
486
- @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent")
525
+ @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent", deprecated=True)
487
526
  async def attach_source(
488
- agent_id: str,
489
- source_id: str,
527
+ source_id: SourceId,
528
+ agent_id: AgentId,
490
529
  server: "SyncServer" = Depends(get_letta_server),
491
530
  headers: HeaderParams = Depends(get_headers),
492
531
  ):
@@ -512,8 +551,8 @@ async def attach_source(
512
551
 
513
552
  @router.patch("/{agent_id}/folders/attach/{folder_id}", response_model=AgentState, operation_id="attach_folder_to_agent")
514
553
  async def attach_folder_to_agent(
515
- agent_id: str,
516
- folder_id: str,
554
+ folder_id: SourceId,
555
+ agent_id: AgentId,
517
556
  server: "SyncServer" = Depends(get_letta_server),
518
557
  headers: HeaderParams = Depends(get_headers),
519
558
  ):
@@ -537,10 +576,10 @@ async def attach_folder_to_agent(
537
576
  return agent_state
538
577
 
539
578
 
540
- @router.patch("/{agent_id}/sources/detach/{source_id}", response_model=AgentState, operation_id="detach_source_from_agent")
579
+ @router.patch("/{agent_id}/sources/detach/{source_id}", response_model=AgentState, operation_id="detach_source_from_agent", deprecated=True)
541
580
  async def detach_source(
542
- agent_id: str,
543
- source_id: str,
581
+ source_id: SourceId,
582
+ agent_id: AgentId,
544
583
  server: "SyncServer" = Depends(get_letta_server),
545
584
  headers: HeaderParams = Depends(get_headers),
546
585
  ):
@@ -569,8 +608,8 @@ async def detach_source(
569
608
 
570
609
  @router.patch("/{agent_id}/folders/detach/{folder_id}", response_model=AgentState, operation_id="detach_folder_from_agent")
571
610
  async def detach_folder_from_agent(
572
- agent_id: str,
573
- folder_id: str,
611
+ folder_id: SourceId,
612
+ agent_id: AgentId,
574
613
  server: "SyncServer" = Depends(get_letta_server),
575
614
  headers: HeaderParams = Depends(get_headers),
576
615
  ):
@@ -597,9 +636,9 @@ async def detach_folder_from_agent(
597
636
  return agent_state
598
637
 
599
638
 
600
- @router.patch("/{agent_id}/files/close-all", response_model=List[str], operation_id="close_all_open_files")
601
- async def close_all_open_files(
602
- agent_id: str,
639
+ @router.patch("/{agent_id}/files/close-all", response_model=List[str], operation_id="close_all_files_for_agent")
640
+ async def close_all_files_for_agent(
641
+ agent_id: AgentId,
603
642
  server: "SyncServer" = Depends(get_letta_server),
604
643
  headers: HeaderParams = Depends(get_headers),
605
644
  ):
@@ -614,10 +653,10 @@ async def close_all_open_files(
614
653
  return await server.file_agent_manager.close_all_other_files(agent_id=agent_id, keep_file_names=[], actor=actor)
615
654
 
616
655
 
617
- @router.patch("/{agent_id}/files/{file_id}/open", response_model=List[str], operation_id="open_file")
618
- async def open_file(
619
- agent_id: str,
620
- file_id: str,
656
+ @router.patch("/{agent_id}/files/{file_id}/open", response_model=List[str], operation_id="open_file_for_agent")
657
+ async def open_file_for_agent(
658
+ file_id: FileId,
659
+ agent_id: AgentId,
621
660
  server: "SyncServer" = Depends(get_letta_server),
622
661
  headers: HeaderParams = Depends(get_headers),
623
662
  ):
@@ -663,10 +702,10 @@ async def open_file(
663
702
  return closed_files
664
703
 
665
704
 
666
- @router.patch("/{agent_id}/files/{file_id}/close", response_model=None, operation_id="close_file")
667
- async def close_file(
668
- agent_id: str,
669
- file_id: str,
705
+ @router.patch("/{agent_id}/files/{file_id}/close", response_model=None, operation_id="close_file_for_agent")
706
+ async def close_file_for_agent(
707
+ file_id: FileId,
708
+ agent_id: AgentId,
670
709
  server: "SyncServer" = Depends(get_letta_server),
671
710
  headers: HeaderParams = Depends(get_headers),
672
711
  ):
@@ -690,33 +729,39 @@ async def close_file(
690
729
 
691
730
  @router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
692
731
  async def retrieve_agent(
693
- agent_id: str,
732
+ agent_id: AgentId,
694
733
  include_relationships: list[str] | None = Query(
695
734
  None,
696
735
  description=(
697
736
  "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. "
698
737
  "If not provided, all relationships are loaded by default. "
699
738
  "Using this can optimize performance by reducing unnecessary joins."
739
+ "This is a legacy parameter, and no longer supported after 1.0.0 SDK versions."
700
740
  ),
701
741
  ),
742
+ include: List[AgentRelationships] = Query(
743
+ [],
744
+ description=("Specify which relational fields to include in the response. No relationships are included by default."),
745
+ ),
702
746
  server: "SyncServer" = Depends(get_letta_server),
703
747
  headers: HeaderParams = Depends(get_headers),
704
748
  ):
705
749
  """
706
750
  Get the state of the agent.
707
751
  """
708
- # Check if agent_id matches uuid4 format
709
- if not AGENT_ID_PATTERN.match(agent_id):
710
- raise HTTPException(status_code=400, detail=f"agent_id {agent_id} is not in the valid format 'agent-<uuid4>'")
711
752
 
712
753
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
713
754
 
714
- return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, include_relationships=include_relationships, actor=actor)
755
+ if include_relationships is None and is_1_0_sdk_version(headers):
756
+ include_relationships = [] # don't default include all if using new SDK version
757
+ return await server.agent_manager.get_agent_by_id_async(
758
+ agent_id=agent_id, include_relationships=include_relationships, include=include, actor=actor
759
+ )
715
760
 
716
761
 
717
762
  @router.delete("/{agent_id}", response_model=None, operation_id="delete_agent")
718
763
  async def delete_agent(
719
- agent_id: str,
764
+ agent_id: AgentId,
720
765
  server: "SyncServer" = Depends(get_letta_server),
721
766
  headers: HeaderParams = Depends(get_headers),
722
767
  ):
@@ -728,9 +773,9 @@ async def delete_agent(
728
773
  return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Agent id={agent_id} successfully deleted"})
729
774
 
730
775
 
731
- @router.get("/{agent_id}/sources", response_model=list[Source], operation_id="list_agent_sources")
776
+ @router.get("/{agent_id}/sources", response_model=list[Source], operation_id="list_agent_sources", deprecated=True)
732
777
  async def list_agent_sources(
733
- agent_id: str,
778
+ agent_id: AgentId,
734
779
  server: "SyncServer" = Depends(get_letta_server),
735
780
  headers: HeaderParams = Depends(get_headers),
736
781
  before: Optional[str] = Query(
@@ -759,9 +804,9 @@ async def list_agent_sources(
759
804
  )
760
805
 
761
806
 
762
- @router.get("/{agent_id}/folders", response_model=list[Source], operation_id="list_agent_folders")
763
- async def list_agent_folders(
764
- agent_id: str,
807
+ @router.get("/{agent_id}/folders", response_model=list[Source], operation_id="list_folders_for_agent")
808
+ async def list_folders_for_agent(
809
+ agent_id: AgentId,
765
810
  server: "SyncServer" = Depends(get_letta_server),
766
811
  headers: HeaderParams = Depends(get_headers),
767
812
  before: Optional[str] = Query(
@@ -790,9 +835,9 @@ async def list_agent_folders(
790
835
  )
791
836
 
792
837
 
793
- @router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_agent_files")
794
- async def list_agent_files(
795
- agent_id: str,
838
+ @router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_files_for_agent")
839
+ async def list_files_for_agent(
840
+ agent_id: AgentId,
796
841
  before: Optional[str] = Query(
797
842
  None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order"
798
843
  ),
@@ -812,7 +857,7 @@ async def list_agent_files(
812
857
  headers: HeaderParams = Depends(get_headers),
813
858
  ):
814
859
  """
815
- Get the files attached to an agent with their open/closed status (paginated).
860
+ Get the files attached to an agent with their open/closed status.
816
861
  """
817
862
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
818
863
 
@@ -855,9 +900,9 @@ async def list_agent_files(
855
900
 
856
901
 
857
902
  # TODO: remove? can also get with agent blocks
858
- @router.get("/{agent_id}/core-memory", response_model=Memory, operation_id="retrieve_agent_memory")
903
+ @router.get("/{agent_id}/core-memory", response_model=Memory, operation_id="retrieve_agent_memory", deprecated=True)
859
904
  async def retrieve_agent_memory(
860
- agent_id: str,
905
+ agent_id: AgentId,
861
906
  server: "SyncServer" = Depends(get_letta_server),
862
907
  headers: HeaderParams = Depends(get_headers),
863
908
  ):
@@ -871,9 +916,9 @@ async def retrieve_agent_memory(
871
916
 
872
917
 
873
918
  @router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="retrieve_core_memory_block")
874
- async def retrieve_block(
875
- agent_id: str,
919
+ async def retrieve_block_for_agent(
876
920
  block_label: str,
921
+ agent_id: AgentId,
877
922
  server: "SyncServer" = Depends(get_letta_server),
878
923
  headers: HeaderParams = Depends(get_headers),
879
924
  ):
@@ -886,8 +931,8 @@ async def retrieve_block(
886
931
 
887
932
 
888
933
  @router.get("/{agent_id}/core-memory/blocks", response_model=list[Block], operation_id="list_core_memory_blocks")
889
- async def list_blocks(
890
- agent_id: str,
934
+ async def list_blocks_for_agent(
935
+ agent_id: AgentId,
891
936
  server: "SyncServer" = Depends(get_letta_server),
892
937
  headers: HeaderParams = Depends(get_headers),
893
938
  before: Optional[str] = Query(
@@ -918,9 +963,9 @@ async def list_blocks(
918
963
 
919
964
 
920
965
  @router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block")
921
- async def modify_block(
922
- agent_id: str,
966
+ async def modify_block_for_agent(
923
967
  block_label: str,
968
+ agent_id: AgentId,
924
969
  block_update: BlockUpdate = Body(...),
925
970
  server: "SyncServer" = Depends(get_letta_server),
926
971
  headers: HeaderParams = Depends(get_headers),
@@ -941,9 +986,9 @@ async def modify_block(
941
986
 
942
987
 
943
988
  @router.patch("/{agent_id}/core-memory/blocks/attach/{block_id}", response_model=AgentState, operation_id="attach_core_memory_block")
944
- async def attach_block(
945
- agent_id: str,
946
- block_id: str,
989
+ async def attach_block_to_agent(
990
+ block_id: BlockId,
991
+ agent_id: AgentId,
947
992
  server: "SyncServer" = Depends(get_letta_server),
948
993
  headers: HeaderParams = Depends(get_headers),
949
994
  ):
@@ -955,9 +1000,9 @@ async def attach_block(
955
1000
 
956
1001
 
957
1002
  @router.patch("/{agent_id}/core-memory/blocks/detach/{block_id}", response_model=AgentState, operation_id="detach_core_memory_block")
958
- async def detach_block(
959
- agent_id: str,
960
- block_id: str,
1003
+ async def detach_block_from_agent(
1004
+ block_id: BlockId,
1005
+ agent_id: AgentId,
961
1006
  server: "SyncServer" = Depends(get_letta_server),
962
1007
  headers: HeaderParams = Depends(get_headers),
963
1008
  ):
@@ -968,9 +1013,85 @@ async def detach_block(
968
1013
  return await server.agent_manager.detach_block_async(agent_id=agent_id, block_id=block_id, actor=actor)
969
1014
 
970
1015
 
971
- @router.get("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="list_passages")
1016
+ @router.patch("/{agent_id}/archives/attach/{archive_id}", response_model=None, operation_id="attach_archive_to_agent")
1017
+ async def attach_archive_to_agent(
1018
+ archive_id: str,
1019
+ agent_id: AgentId,
1020
+ server: "SyncServer" = Depends(get_letta_server),
1021
+ headers: HeaderParams = Depends(get_headers),
1022
+ ):
1023
+ """
1024
+ Attach an archive to an agent.
1025
+ """
1026
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1027
+ await server.archive_manager.attach_agent_to_archive_async(
1028
+ agent_id=agent_id,
1029
+ archive_id=archive_id,
1030
+ actor=actor,
1031
+ )
1032
+ return None
1033
+
1034
+
1035
+ @router.patch("/{agent_id}/archives/detach/{archive_id}", response_model=None, operation_id="detach_archive_from_agent")
1036
+ async def detach_archive_from_agent(
1037
+ archive_id: str,
1038
+ agent_id: AgentId,
1039
+ server: "SyncServer" = Depends(get_letta_server),
1040
+ headers: HeaderParams = Depends(get_headers),
1041
+ ):
1042
+ """
1043
+ Detach an archive from an agent.
1044
+ """
1045
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1046
+ await server.archive_manager.detach_agent_from_archive_async(
1047
+ agent_id=agent_id,
1048
+ archive_id=archive_id,
1049
+ actor=actor,
1050
+ )
1051
+ return None
1052
+
1053
+
1054
+ @router.patch("/{agent_id}/identities/attach/{identity_id}", response_model=None, operation_id="attach_identity_to_agent")
1055
+ async def attach_identity_to_agent(
1056
+ identity_id: str,
1057
+ agent_id: AgentId,
1058
+ server: "SyncServer" = Depends(get_letta_server),
1059
+ headers: HeaderParams = Depends(get_headers),
1060
+ ):
1061
+ """
1062
+ Attach an identity to an agent.
1063
+ """
1064
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1065
+ await server.identity_manager.attach_agent_async(
1066
+ identity_id=identity_id,
1067
+ agent_id=agent_id,
1068
+ actor=actor,
1069
+ )
1070
+ return None
1071
+
1072
+
1073
+ @router.patch("/{agent_id}/identities/detach/{identity_id}", response_model=None, operation_id="detach_identity_from_agent")
1074
+ async def detach_identity_from_agent(
1075
+ identity_id: str,
1076
+ agent_id: AgentId,
1077
+ server: "SyncServer" = Depends(get_letta_server),
1078
+ headers: HeaderParams = Depends(get_headers),
1079
+ ):
1080
+ """
1081
+ Detach an identity from an agent.
1082
+ """
1083
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1084
+ await server.identity_manager.detach_agent_async(
1085
+ identity_id=identity_id,
1086
+ agent_id=agent_id,
1087
+ actor=actor,
1088
+ )
1089
+ return None
1090
+
1091
+
1092
+ @router.get("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="list_passages", deprecated=True)
972
1093
  async def list_passages(
973
- agent_id: str,
1094
+ agent_id: AgentId,
974
1095
  server: "SyncServer" = Depends(get_letta_server),
975
1096
  after: str | None = Query(None, description="Unique ID of the memory to start the query range at."),
976
1097
  before: str | None = Query(None, description="Unique ID of the memory to end the query range at."),
@@ -997,9 +1118,9 @@ async def list_passages(
997
1118
  )
998
1119
 
999
1120
 
1000
- @router.post("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="create_passage")
1121
+ @router.post("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="create_passage", deprecated=True)
1001
1122
  async def create_passage(
1002
- agent_id: str,
1123
+ agent_id: AgentId,
1003
1124
  request: CreateArchivalMemory = Body(...),
1004
1125
  server: "SyncServer" = Depends(get_letta_server),
1005
1126
  headers: HeaderParams = Depends(get_headers),
@@ -1014,9 +1135,14 @@ async def create_passage(
1014
1135
  )
1015
1136
 
1016
1137
 
1017
- @router.get("/{agent_id}/archival-memory/search", response_model=ArchivalMemorySearchResponse, operation_id="search_archival_memory")
1138
+ @router.get(
1139
+ "/{agent_id}/archival-memory/search",
1140
+ response_model=ArchivalMemorySearchResponse,
1141
+ operation_id="search_archival_memory",
1142
+ deprecated=True,
1143
+ )
1018
1144
  async def search_archival_memory(
1019
- agent_id: str,
1145
+ agent_id: AgentId,
1020
1146
  query: str = Query(..., description="String to search for using semantic similarity"),
1021
1147
  tags: Optional[List[str]] = Query(None, description="Optional list of tags to filter search results"),
1022
1148
  tag_match_mode: Literal["any", "all"] = Query(
@@ -1061,10 +1187,10 @@ async def search_archival_memory(
1061
1187
 
1062
1188
  # TODO(ethan): query or path parameter for memory_id?
1063
1189
  # @router.delete("/{agent_id}/archival")
1064
- @router.delete("/{agent_id}/archival-memory/{memory_id}", response_model=None, operation_id="delete_passage")
1190
+ @router.delete("/{agent_id}/archival-memory/{memory_id}", response_model=None, operation_id="delete_passage", deprecated=True)
1065
1191
  async def delete_passage(
1066
- agent_id: str,
1067
1192
  memory_id: str,
1193
+ agent_id: AgentId,
1068
1194
  # memory_id: str = Query(..., description="Unique ID of the memory to be deleted."),
1069
1195
  server: "SyncServer" = Depends(get_letta_server),
1070
1196
  headers: HeaderParams = Depends(get_headers),
@@ -1085,7 +1211,7 @@ AgentMessagesResponse = Annotated[
1085
1211
 
1086
1212
  @router.get("/{agent_id}/messages", response_model=AgentMessagesResponse, operation_id="list_messages")
1087
1213
  async def list_messages(
1088
- agent_id: str,
1214
+ agent_id: AgentId,
1089
1215
  server: "SyncServer" = Depends(get_letta_server),
1090
1216
  before: Optional[str] = Query(
1091
1217
  None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
@@ -1099,9 +1225,9 @@ async def list_messages(
1099
1225
  ),
1100
1226
  order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
1101
1227
  group_id: str | None = Query(None, description="Group ID to filter messages by."),
1102
- use_assistant_message: bool = Query(True, description="Whether to use assistant messages"),
1103
- assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool."),
1104
- assistant_message_tool_kwarg: str = Query(DEFAULT_MESSAGE_TOOL_KWARG, description="The name of the message argument."),
1228
+ use_assistant_message: bool = Query(True, description="Whether to use assistant messages", deprecated=True),
1229
+ assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool.", deprecated=True),
1230
+ assistant_message_tool_kwarg: str = Query(DEFAULT_MESSAGE_TOOL_KWARG, description="The name of the message argument.", deprecated=True),
1105
1231
  include_err: bool | None = Query(
1106
1232
  None, description="Whether to include error messages and error statuses. For debugging purposes only."
1107
1233
  ),
@@ -1130,8 +1256,8 @@ async def list_messages(
1130
1256
 
1131
1257
  @router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_message")
1132
1258
  async def modify_message(
1133
- agent_id: str,
1134
- message_id: str,
1259
+ agent_id: AgentId, # backwards compatible. Consider removing for v1
1260
+ message_id: MessageId,
1135
1261
  request: LettaMessageUpdateUnion = Body(...),
1136
1262
  server: "SyncServer" = Depends(get_letta_server),
1137
1263
  headers: HeaderParams = Depends(get_headers),
@@ -1153,8 +1279,8 @@ async def modify_message(
1153
1279
  operation_id="send_message",
1154
1280
  )
1155
1281
  async def send_message(
1156
- agent_id: str,
1157
1282
  request_obj: Request, # FastAPI Request
1283
+ agent_id: AgentId,
1158
1284
  server: SyncServer = Depends(get_letta_server),
1159
1285
  request: LettaRequest = Body(...),
1160
1286
  headers: HeaderParams = Depends(get_headers),
@@ -1280,8 +1406,8 @@ async def send_message(
1280
1406
  },
1281
1407
  )
1282
1408
  async def send_message_streaming(
1283
- agent_id: str,
1284
1409
  request_obj: Request, # FastAPI Request
1410
+ agent_id: AgentId,
1285
1411
  server: SyncServer = Depends(get_letta_server),
1286
1412
  request: LettaStreamingRequest = Body(...),
1287
1413
  headers: HeaderParams = Depends(get_headers),
@@ -1291,199 +1417,30 @@ async def send_message_streaming(
1291
1417
  This endpoint accepts a message from a user and processes it through the agent.
1292
1418
  It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
1293
1419
  """
1294
- request_start_timestamp_ns = get_utc_timestamp_ns()
1295
- MetricRegistry().user_message_counter.add(1, get_ctx_attributes())
1296
-
1297
- # TODO (cliandy): clean this up
1298
- redis_client = await get_redis_client()
1420
+ from letta.services.streaming_service import StreamingService
1299
1421
 
1300
1422
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1301
- # TODO: This is redundant, remove soon
1302
- agent = await server.agent_manager.get_agent_by_id_async(
1303
- agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
1304
- )
1305
- agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
1306
- model_compatible = agent.llm_config.model_endpoint_type in [
1307
- "anthropic",
1308
- "openai",
1309
- "together",
1310
- "google_ai",
1311
- "google_vertex",
1312
- "bedrock",
1313
- "ollama",
1314
- "azure",
1315
- "xai",
1316
- "groq",
1317
- "deepseek",
1318
- ]
1319
- model_compatible_token_streaming = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "bedrock", "deepseek"]
1320
- if agent.agent_type == AgentType.letta_v1_agent and agent.llm_config.model_endpoint_type in ["google_ai", "google_vertex"]:
1321
- model_compatible_token_streaming = True
1322
1423
 
1323
- # Create a new run for execution tracking
1324
- if settings.track_agent_run:
1325
- runs_manager = RunManager()
1326
- run = await runs_manager.create_run(
1327
- pydantic_run=PydanticRun(
1328
- agent_id=agent_id,
1329
- background=request.background or False,
1330
- metadata={
1331
- "run_type": "send_message_streaming",
1332
- },
1333
- request_config=LettaRequestConfig.from_letta_request(request),
1334
- ),
1335
- actor=actor,
1336
- )
1337
- run_update_metadata = None
1338
- await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
1339
- else:
1340
- run = None
1341
-
1342
- try:
1343
- if agent_eligible and model_compatible:
1344
- agent_loop = AgentLoop.load(agent_state=agent, actor=actor)
1424
+ # use the streaming service for unified stream handling
1425
+ streaming_service = StreamingService(server)
1345
1426
 
1346
- async def error_aware_stream():
1347
- """Stream that handles early LLM errors gracefully in streaming format."""
1348
- from letta.errors import LLMAuthenticationError, LLMError, LLMRateLimitError, LLMTimeoutError
1349
-
1350
- try:
1351
- stream = agent_loop.stream(
1352
- input_messages=request.messages,
1353
- max_steps=request.max_steps,
1354
- stream_tokens=request.stream_tokens and model_compatible_token_streaming,
1355
- run_id=run.id if run else None,
1356
- use_assistant_message=request.use_assistant_message,
1357
- request_start_timestamp_ns=request_start_timestamp_ns,
1358
- include_return_message_types=request.include_return_message_types,
1359
- )
1360
- async for chunk in stream:
1361
- yield chunk
1362
-
1363
- if run:
1364
- runs_manager = RunManager()
1365
- from letta.schemas.enums import RunStatus
1366
-
1367
- await runs_manager.update_run_by_id_async(
1368
- run_id=run.id,
1369
- update=RunUpdate(status=RunStatus.completed, stop_reason=agent_loop.stop_reason.stop_reason.value),
1370
- actor=actor,
1371
- )
1372
-
1373
- except LLMTimeoutError as e:
1374
- error_data = {
1375
- "error": {"type": "llm_timeout", "message": "The LLM request timed out. Please try again.", "detail": str(e)}
1376
- }
1377
- yield (f"data: {json.dumps(error_data)}\n\n", 504)
1378
- except LLMRateLimitError as e:
1379
- error_data = {
1380
- "error": {
1381
- "type": "llm_rate_limit",
1382
- "message": "Rate limit exceeded for LLM model provider. Please wait before making another request.",
1383
- "detail": str(e),
1384
- }
1385
- }
1386
- yield (f"data: {json.dumps(error_data)}\n\n", 429)
1387
- except LLMAuthenticationError as e:
1388
- error_data = {
1389
- "error": {
1390
- "type": "llm_authentication",
1391
- "message": "Authentication failed with the LLM model provider.",
1392
- "detail": str(e),
1393
- }
1394
- }
1395
- yield (f"data: {json.dumps(error_data)}\n\n", 401)
1396
- except LLMError as e:
1397
- error_data = {"error": {"type": "llm_error", "message": "An error occurred with the LLM request.", "detail": str(e)}}
1398
- yield (f"data: {json.dumps(error_data)}\n\n", 502)
1399
- except Exception as e:
1400
- error_data = {"error": {"type": "internal_error", "message": "An internal server error occurred.", "detail": str(e)}}
1401
- yield (f"data: {json.dumps(error_data)}\n\n", 500)
1402
-
1403
- raw_stream = error_aware_stream()
1404
-
1405
- from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode, add_keepalive_to_stream
1406
-
1407
- if request.background and settings.track_agent_run:
1408
- if isinstance(redis_client, NoopAsyncRedisClient):
1409
- raise HTTPException(
1410
- status_code=503,
1411
- detail=(
1412
- "Background streaming requires Redis to be running. "
1413
- "Please ensure Redis is properly configured. "
1414
- f"LETTA_REDIS_HOST: {settings.redis_host}, LETTA_REDIS_PORT: {settings.redis_port}"
1415
- ),
1416
- )
1417
-
1418
- safe_create_task(
1419
- create_background_stream_processor(
1420
- stream_generator=raw_stream,
1421
- redis_client=redis_client,
1422
- run_id=run.id,
1423
- run_manager=server.run_manager,
1424
- actor=actor,
1425
- ),
1426
- label=f"background_stream_processor_{run.id}",
1427
- )
1427
+ run, result = await streaming_service.create_agent_stream(
1428
+ agent_id=agent_id,
1429
+ actor=actor,
1430
+ request=request,
1431
+ run_type="send_message_streaming",
1432
+ )
1428
1433
 
1429
- raw_stream = redis_sse_stream_generator(
1430
- redis_client=redis_client,
1431
- run_id=run.id,
1432
- )
1433
-
1434
- # Conditionally wrap with keepalive based on request parameter
1435
- if request.include_pings and settings.enable_keepalive:
1436
- stream = add_keepalive_to_stream(raw_stream, keepalive_interval=settings.keepalive_interval)
1437
- else:
1438
- stream = raw_stream
1439
-
1440
- result = StreamingResponseWithStatusCode(
1441
- stream,
1442
- media_type="text/event-stream",
1443
- )
1444
- else:
1445
- result = await server.send_message_to_agent(
1446
- agent_id=agent_id,
1447
- actor=actor,
1448
- input_messages=request.messages,
1449
- stream_steps=True,
1450
- stream_tokens=request.stream_tokens,
1451
- # Support for AssistantMessage
1452
- use_assistant_message=request.use_assistant_message,
1453
- assistant_message_tool_name=request.assistant_message_tool_name,
1454
- assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
1455
- request_start_timestamp_ns=request_start_timestamp_ns,
1456
- include_return_message_types=request.include_return_message_types,
1457
- )
1458
- if settings.track_agent_run:
1459
- run_status = RunStatus.running
1460
- return result
1461
- except PendingApprovalError as e:
1462
- if settings.track_agent_run:
1463
- run_update_metadata = {"error": str(e)}
1464
- run_status = RunStatus.failed
1465
- raise HTTPException(
1466
- status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
1467
- )
1468
- except Exception as e:
1469
- if settings.track_agent_run:
1470
- run_update_metadata = {"error": str(e)}
1471
- run_status = RunStatus.failed
1472
- raise
1473
- finally:
1474
- if settings.track_agent_run:
1475
- await server.run_manager.update_run_by_id_async(
1476
- run_id=run.id, update=RunUpdate(status=run_status, metadata=run_update_metadata), actor=actor
1477
- )
1434
+ return result
1478
1435
 
1479
1436
 
1480
1437
  class CancelAgentRunRequest(BaseModel):
1481
1438
  run_ids: list[str] | None = Field(None, description="Optional list of run IDs to cancel")
1482
1439
 
1483
1440
 
1484
- @router.post("/{agent_id}/messages/cancel", operation_id="cancel_agent_run")
1485
- async def cancel_agent_run(
1486
- agent_id: str,
1441
+ @router.post("/{agent_id}/messages/cancel", operation_id="cancel_message")
1442
+ async def cancel_message(
1443
+ agent_id: AgentId,
1487
1444
  request: CancelAgentRunRequest = Body(None),
1488
1445
  server: SyncServer = Depends(get_letta_server),
1489
1446
  headers: HeaderParams = Depends(get_headers),
@@ -1574,6 +1531,9 @@ async def _process_message_background(
1574
1531
  ) -> None:
1575
1532
  """Background task to process the message and update run status."""
1576
1533
  request_start_timestamp_ns = get_utc_timestamp_ns()
1534
+ agent_loop = None
1535
+ result = None
1536
+
1577
1537
  try:
1578
1538
  agent = await server.agent_manager.get_agent_by_id_async(
1579
1539
  agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
@@ -1620,9 +1580,14 @@ async def _process_message_background(
1620
1580
  runs_manager = RunManager()
1621
1581
  from letta.schemas.enums import RunStatus
1622
1582
 
1583
+ if result.stop_reason.stop_reason == "cancelled":
1584
+ run_status = RunStatus.cancelled
1585
+ else:
1586
+ run_status = RunStatus.completed
1587
+
1623
1588
  await runs_manager.update_run_by_id_async(
1624
1589
  run_id=run_id,
1625
- update=RunUpdate(status=RunStatus.completed, stop_reason=result.stop_reason.stop_reason),
1590
+ update=RunUpdate(status=run_status, stop_reason=result.stop_reason.stop_reason),
1626
1591
  actor=actor,
1627
1592
  )
1628
1593
 
@@ -1646,6 +1611,41 @@ async def _process_message_background(
1646
1611
  update=RunUpdate(status=RunStatus.failed),
1647
1612
  actor=actor,
1648
1613
  )
1614
+ finally:
1615
+ # Critical: Explicit resource cleanup to prevent accumulation
1616
+ if agent_loop and result:
1617
+ await _cleanup_background_task_resources(agent_loop, result)
1618
+
1619
+
1620
+ async def _cleanup_background_task_resources(agent_loop: BaseAgentV2 | LettaAgent, result: StreamingResponse | LettaResponse) -> None:
1621
+ """
1622
+ Explicit cleanup of resources created during background message processing.
1623
+
1624
+ Proper cleanup of:
1625
+ - Agent instances and their internal state
1626
+ - Message buffers and response accumulation
1627
+ - Any database connections or sessions
1628
+ - LLM client resources
1629
+ """
1630
+ import gc
1631
+
1632
+ try:
1633
+ if agent_loop is not None:
1634
+ if agent_loop.response_messages:
1635
+ # Clear response message buffer to prevent accumulation
1636
+ agent_loop.response_messages.clear()
1637
+ # Clean up agent loop resources
1638
+ del agent_loop
1639
+
1640
+ if result is not None:
1641
+ del result # Clear result data to free memory
1642
+
1643
+ # Force garbage collection to clean up references and release memory
1644
+ gc.collect()
1645
+ except Exception as e:
1646
+ # Handle errors for logging but don't fail the background task
1647
+ logger.warning(f"Error during background task resource cleanup: {e}")
1648
+ pass
1649
1649
 
1650
1650
 
1651
1651
  @router.post(
@@ -1654,7 +1654,7 @@ async def _process_message_background(
1654
1654
  operation_id="create_agent_message_async",
1655
1655
  )
1656
1656
  async def send_message_async(
1657
- agent_id: str,
1657
+ agent_id: AgentId,
1658
1658
  server: SyncServer = Depends(get_letta_server),
1659
1659
  request: LettaAsyncRequest = Body(...),
1660
1660
  headers: HeaderParams = Depends(get_headers),
@@ -1667,8 +1667,14 @@ async def send_message_async(
1667
1667
  """
1668
1668
  MetricRegistry().user_message_counter.add(1, get_ctx_attributes())
1669
1669
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1670
+
1671
+ try:
1672
+ is_message_input = request.messages[0].type == MessageCreateType.message
1673
+ except:
1674
+ is_message_input = True
1675
+ use_lettuce = headers.experimental_params.message_async and is_message_input
1676
+
1670
1677
  # Create a new run
1671
- use_lettuce = headers.experimental_params.message_async
1672
1678
  run = PydanticRun(
1673
1679
  callback_url=request.callback_url,
1674
1680
  agent_id=agent_id,
@@ -1750,23 +1756,32 @@ async def send_message_async(
1750
1756
  return run
1751
1757
 
1752
1758
 
1759
+ class ResetMessagesRequest(BaseModel):
1760
+ """Request body for resetting messages on an agent."""
1761
+
1762
+ add_default_initial_messages: bool = Field(
1763
+ False,
1764
+ description="If true, adds the default initial messages after resetting.",
1765
+ )
1766
+
1767
+
1753
1768
  @router.patch("/{agent_id}/reset-messages", response_model=AgentState, operation_id="reset_messages")
1754
1769
  async def reset_messages(
1755
- agent_id: str,
1756
- add_default_initial_messages: bool = Query(default=False, description="If true, adds the default initial messages after resetting."),
1770
+ agent_id: AgentId,
1771
+ request: ResetMessagesRequest = Body(...),
1757
1772
  server: "SyncServer" = Depends(get_letta_server),
1758
1773
  headers: HeaderParams = Depends(get_headers),
1759
1774
  ):
1760
1775
  """Resets the messages for an agent"""
1761
1776
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1762
1777
  return await server.agent_manager.reset_messages_async(
1763
- agent_id=agent_id, actor=actor, add_default_initial_messages=add_default_initial_messages
1778
+ agent_id=agent_id, actor=actor, add_default_initial_messages=request.add_default_initial_messages
1764
1779
  )
1765
1780
 
1766
1781
 
1767
- @router.get("/{agent_id}/groups", response_model=list[Group], operation_id="list_agent_groups")
1768
- async def list_agent_groups(
1769
- agent_id: str,
1782
+ @router.get("/{agent_id}/groups", response_model=list[Group], operation_id="list_groups_for_agent")
1783
+ async def list_groups_for_agent(
1784
+ agent_id: AgentId,
1770
1785
  manager_type: str | None = Query(None, description="Manager type to filter groups by"),
1771
1786
  server: "SyncServer" = Depends(get_letta_server),
1772
1787
  headers: HeaderParams = Depends(get_headers),
@@ -1782,7 +1797,7 @@ async def list_agent_groups(
1782
1797
  ),
1783
1798
  order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
1784
1799
  ):
1785
- """Lists the groups for an agent"""
1800
+ """Lists the groups for an agent."""
1786
1801
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1787
1802
  logger.info("in list agents with manager_type", manager_type)
1788
1803
  return await server.agent_manager.list_groups_async(
@@ -1799,10 +1814,10 @@ async def list_agent_groups(
1799
1814
  @router.post(
1800
1815
  "/{agent_id}/messages/preview-raw-payload",
1801
1816
  response_model=Dict[str, Any],
1802
- operation_id="preview_raw_payload",
1817
+ operation_id="preview_model_request",
1803
1818
  )
1804
- async def preview_raw_payload(
1805
- agent_id: str,
1819
+ async def preview_model_request(
1820
+ agent_id: AgentId,
1806
1821
  request: Union[LettaRequest, LettaStreamingRequest] = Body(...),
1807
1822
  server: SyncServer = Depends(get_letta_server),
1808
1823
  headers: HeaderParams = Depends(get_headers),
@@ -1845,19 +1860,14 @@ async def preview_raw_payload(
1845
1860
  )
1846
1861
 
1847
1862
 
1848
- @router.post("/{agent_id}/summarize", status_code=204, operation_id="summarize_agent_conversation")
1849
- async def summarize_agent_conversation(
1850
- agent_id: str,
1851
- request_obj: Request, # FastAPI Request
1852
- max_message_length: int = Query(..., description="Maximum number of messages to retain after summarization."),
1863
+ @router.post("/{agent_id}/summarize", status_code=204, operation_id="summarize_messages")
1864
+ async def summarize_messages(
1865
+ agent_id: AgentId,
1853
1866
  server: SyncServer = Depends(get_letta_server),
1854
1867
  headers: HeaderParams = Depends(get_headers),
1855
1868
  ):
1856
1869
  """
1857
- Summarize an agent's conversation history to a target message length.
1858
-
1859
- This endpoint summarizes the current message history for a given agent,
1860
- truncating and compressing it down to the specified `max_message_length`.
1870
+ Summarize an agent's conversation history.
1861
1871
  """
1862
1872
 
1863
1873
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)