letta-nightly 0.11.7.dev20250916104104__py3-none-any.whl → 0.11.7.dev20250918104055__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 (63) hide show
  1. letta/__init__.py +10 -2
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +0 -1
  4. letta/agent.py +4 -4
  5. letta/agents/agent_loop.py +2 -1
  6. letta/agents/base_agent.py +1 -1
  7. letta/agents/letta_agent.py +1 -4
  8. letta/agents/letta_agent_v2.py +5 -4
  9. letta/agents/temporal/activities/__init__.py +4 -0
  10. letta/agents/temporal/activities/example_activity.py +7 -0
  11. letta/agents/temporal/activities/prepare_messages.py +10 -0
  12. letta/agents/temporal/temporal_agent_workflow.py +56 -0
  13. letta/agents/temporal/types.py +25 -0
  14. letta/agents/voice_agent.py +3 -3
  15. letta/helpers/converters.py +8 -2
  16. letta/helpers/crypto_utils.py +144 -0
  17. letta/llm_api/llm_api_tools.py +0 -1
  18. letta/llm_api/llm_client_base.py +0 -2
  19. letta/orm/__init__.py +1 -0
  20. letta/orm/agent.py +9 -4
  21. letta/orm/job.py +3 -1
  22. letta/orm/mcp_oauth.py +6 -0
  23. letta/orm/mcp_server.py +7 -1
  24. letta/orm/sqlalchemy_base.py +2 -1
  25. letta/prompts/prompt_generator.py +4 -4
  26. letta/schemas/agent.py +14 -200
  27. letta/schemas/enums.py +15 -0
  28. letta/schemas/job.py +10 -0
  29. letta/schemas/mcp.py +146 -6
  30. letta/schemas/memory.py +216 -103
  31. letta/schemas/provider_trace.py +0 -2
  32. letta/schemas/run.py +2 -0
  33. letta/schemas/secret.py +378 -0
  34. letta/schemas/step.py +5 -1
  35. letta/schemas/tool_rule.py +34 -44
  36. letta/serialize_schemas/marshmallow_agent.py +4 -0
  37. letta/server/rest_api/routers/v1/__init__.py +2 -0
  38. letta/server/rest_api/routers/v1/agents.py +9 -4
  39. letta/server/rest_api/routers/v1/archives.py +113 -0
  40. letta/server/rest_api/routers/v1/jobs.py +7 -2
  41. letta/server/rest_api/routers/v1/runs.py +9 -1
  42. letta/server/rest_api/routers/v1/steps.py +29 -0
  43. letta/server/rest_api/routers/v1/tools.py +7 -26
  44. letta/server/server.py +2 -2
  45. letta/services/agent_manager.py +21 -15
  46. letta/services/agent_serialization_manager.py +11 -3
  47. letta/services/archive_manager.py +73 -0
  48. letta/services/helpers/agent_manager_helper.py +10 -5
  49. letta/services/job_manager.py +18 -2
  50. letta/services/mcp_manager.py +198 -82
  51. letta/services/step_manager.py +26 -0
  52. letta/services/summarizer/summarizer.py +25 -3
  53. letta/services/telemetry_manager.py +2 -0
  54. letta/services/tool_executor/composio_tool_executor.py +1 -1
  55. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  56. letta/services/tool_sandbox/base.py +135 -9
  57. letta/settings.py +2 -2
  58. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/METADATA +6 -3
  59. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/RECORD +62 -55
  60. letta/templates/template_helper.py +0 -53
  61. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/WHEEL +0 -0
  62. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/entry_points.txt +0 -0
  63. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,113 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from fastapi import APIRouter, Body, Depends, HTTPException, Query
4
+ from pydantic import BaseModel
5
+
6
+ from letta.orm.errors import NoResultFound
7
+ from letta.schemas.archive import Archive as PydanticArchive
8
+ from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
9
+ from letta.server.server import SyncServer
10
+
11
+ router = APIRouter(prefix="/archives", tags=["archives"])
12
+
13
+
14
+ class ArchiveCreateRequest(BaseModel):
15
+ """Request model for creating an archive.
16
+
17
+ Intentionally excludes vector_db_provider. These are derived internally (vector DB provider from env).
18
+ """
19
+
20
+ name: str
21
+ description: Optional[str] = None
22
+
23
+
24
+ class ArchiveUpdateRequest(BaseModel):
25
+ """Request model for updating an archive (partial).
26
+
27
+ Supports updating only name and description.
28
+ """
29
+
30
+ name: Optional[str] = None
31
+ description: Optional[str] = None
32
+
33
+
34
+ @router.post("/", response_model=PydanticArchive, operation_id="create_archive")
35
+ async def create_archive(
36
+ archive: ArchiveCreateRequest = Body(...),
37
+ server: "SyncServer" = Depends(get_letta_server),
38
+ headers: HeaderParams = Depends(get_headers),
39
+ ):
40
+ """
41
+ Create a new archive.
42
+ """
43
+ try:
44
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
45
+ return await server.archive_manager.create_archive_async(
46
+ name=archive.name,
47
+ description=archive.description,
48
+ actor=actor,
49
+ )
50
+ except Exception as e:
51
+ raise HTTPException(status_code=500, detail=str(e))
52
+
53
+
54
+ @router.get("/", response_model=List[PydanticArchive], operation_id="list_archives")
55
+ async def list_archives(
56
+ before: Optional[str] = Query(
57
+ None,
58
+ description="Archive ID cursor for pagination. Returns archives that come before this archive ID in the specified sort order",
59
+ ),
60
+ after: Optional[str] = Query(
61
+ None,
62
+ description="Archive ID cursor for pagination. Returns archives that come after this archive ID in the specified sort order",
63
+ ),
64
+ limit: Optional[int] = Query(50, description="Maximum number of archives to return"),
65
+ order: Literal["asc", "desc"] = Query(
66
+ "desc", description="Sort order for archives by creation time. 'asc' for oldest first, 'desc' for newest first"
67
+ ),
68
+ name: Optional[str] = Query(None, description="Filter by archive name (exact match)"),
69
+ agent_id: Optional[str] = Query(None, description="Only archives attached to this agent ID"),
70
+ server: "SyncServer" = Depends(get_letta_server),
71
+ headers: HeaderParams = Depends(get_headers),
72
+ ):
73
+ """
74
+ Get a list of all archives for the current organization with optional filters and pagination.
75
+ """
76
+ try:
77
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
78
+ archives = await server.archive_manager.list_archives_async(
79
+ actor=actor,
80
+ before=before,
81
+ after=after,
82
+ limit=limit,
83
+ ascending=(order == "asc"),
84
+ name=name,
85
+ agent_id=agent_id,
86
+ )
87
+ return archives
88
+ except Exception as e:
89
+ raise HTTPException(status_code=500, detail=str(e))
90
+
91
+
92
+ @router.patch("/{archive_id}", response_model=PydanticArchive, operation_id="modify_archive")
93
+ async def modify_archive(
94
+ archive_id: str,
95
+ archive: ArchiveUpdateRequest = Body(...),
96
+ server: "SyncServer" = Depends(get_letta_server),
97
+ headers: HeaderParams = Depends(get_headers),
98
+ ):
99
+ """
100
+ Update an existing archive's name and/or description.
101
+ """
102
+ try:
103
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
104
+ return await server.archive_manager.update_archive_async(
105
+ archive_id=archive_id,
106
+ name=archive.name,
107
+ description=archive.description,
108
+ actor=actor,
109
+ )
110
+ except NoResultFound as e:
111
+ raise HTTPException(status_code=404, detail=str(e))
112
+ except Exception as e:
113
+ raise HTTPException(status_code=500, detail=str(e))
@@ -19,18 +19,23 @@ async def list_jobs(
19
19
  before: Optional[str] = Query(None, description="Cursor for pagination"),
20
20
  after: Optional[str] = Query(None, description="Cursor for pagination"),
21
21
  limit: Optional[int] = Query(50, description="Limit for pagination"),
22
+ active: bool = Query(False, description="Filter for active jobs."),
22
23
  ascending: bool = Query(True, description="Whether to sort jobs oldest to newest (True, default) or newest to oldest (False)"),
23
24
  headers: HeaderParams = Depends(get_headers),
24
25
  ):
25
26
  """
26
27
  List all jobs.
27
- TODO (cliandy): implementation for pagination
28
28
  """
29
29
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
30
30
 
31
+ statuses = None
32
+ if active:
33
+ statuses = [JobStatus.created, JobStatus.running]
34
+
31
35
  # TODO: add filtering by status
32
36
  return await server.job_manager.list_jobs_async(
33
37
  actor=actor,
38
+ statuses=statuses,
34
39
  source_id=source_id,
35
40
  before=before,
36
41
  after=after,
@@ -39,7 +44,7 @@ async def list_jobs(
39
44
  )
40
45
 
41
46
 
42
- @router.get("/active", response_model=List[Job], operation_id="list_active_jobs")
47
+ @router.get("/active", response_model=List[Job], operation_id="list_active_jobs", deprecated=True)
43
48
  async def list_active_jobs(
44
49
  server: "SyncServer" = Depends(get_letta_server),
45
50
  headers: HeaderParams = Depends(get_headers),
@@ -10,6 +10,7 @@ from letta.orm.errors import NoResultFound
10
10
  from letta.schemas.enums import JobStatus, JobType
11
11
  from letta.schemas.letta_message import LettaMessageUnion
12
12
  from letta.schemas.letta_request import RetrieveStreamRequest
13
+ from letta.schemas.letta_stop_reason import StopReasonType
13
14
  from letta.schemas.openai.chat_completion_response import UsageStatistics
14
15
  from letta.schemas.run import Run
15
16
  from letta.schemas.step import Step
@@ -31,9 +32,11 @@ def list_runs(
31
32
  server: "SyncServer" = Depends(get_letta_server),
32
33
  agent_ids: Optional[List[str]] = Query(None, description="The unique identifier of the agent associated with the run."),
33
34
  background: Optional[bool] = Query(None, description="If True, filters for runs that were created in background mode."),
35
+ stop_reason: Optional[StopReasonType] = Query(None, description="Filter runs by stop reason."),
34
36
  after: Optional[str] = Query(None, description="Cursor for pagination"),
35
37
  before: Optional[str] = Query(None, description="Cursor for pagination"),
36
38
  limit: Optional[int] = Query(50, description="Maximum number of runs to return"),
39
+ active: bool = Query(False, description="Filter for active runs."),
37
40
  ascending: bool = Query(
38
41
  False,
39
42
  description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default)",
@@ -44,16 +47,21 @@ def list_runs(
44
47
  List all runs.
45
48
  """
46
49
  actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
50
+ statuses = None
51
+ if active:
52
+ statuses = [JobStatus.created, JobStatus.running]
47
53
 
48
54
  runs = [
49
55
  Run.from_job(job)
50
56
  for job in server.job_manager.list_jobs(
51
57
  actor=actor,
58
+ statuses=statuses,
52
59
  job_type=JobType.RUN,
53
60
  limit=limit,
54
61
  before=before,
55
62
  after=after,
56
63
  ascending=False,
64
+ stop_reason=stop_reason,
57
65
  )
58
66
  ]
59
67
  if agent_ids:
@@ -63,7 +71,7 @@ def list_runs(
63
71
  return runs
64
72
 
65
73
 
66
- @router.get("/active", response_model=List[Run], operation_id="list_active_runs")
74
+ @router.get("/active", response_model=List[Run], operation_id="list_active_runs", deprecated=True)
67
75
  def list_active_runs(
68
76
  server: "SyncServer" = Depends(get_letta_server),
69
77
  agent_ids: Optional[List[str]] = Query(None, description="The unique identifier of the agent associated with the run."),
@@ -5,6 +5,8 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
5
5
  from pydantic import BaseModel, Field
6
6
 
7
7
  from letta.orm.errors import NoResultFound
8
+ from letta.schemas.letta_message import LettaMessageUnion
9
+ from letta.schemas.message import Message
8
10
  from letta.schemas.provider_trace import ProviderTrace
9
11
  from letta.schemas.step import Step
10
12
  from letta.schemas.step_metrics import StepMetrics
@@ -138,6 +140,33 @@ async def modify_feedback_for_step(
138
140
  raise HTTPException(status_code=404, detail="Step not found")
139
141
 
140
142
 
143
+ @router.get("/{step_id}/messages", response_model=List[LettaMessageUnion], operation_id="list_messages_for_step")
144
+ async def list_messages_for_step(
145
+ step_id: str,
146
+ headers: HeaderParams = Depends(get_headers),
147
+ server: SyncServer = Depends(get_letta_server),
148
+ before: Optional[str] = Query(
149
+ None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
150
+ ),
151
+ after: Optional[str] = Query(
152
+ None, description="Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order"
153
+ ),
154
+ limit: Optional[int] = Query(100, description="Maximum number of messages to return"),
155
+ order: Literal["asc", "desc"] = Query(
156
+ "asc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
157
+ ),
158
+ order_by: Literal["created_at"] = Query("created_at", description="Sort by field"),
159
+ ):
160
+ """
161
+ List messages for a given step.
162
+ """
163
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
164
+ messages = await server.step_manager.list_step_messages_async(
165
+ step_id=step_id, actor=actor, before=before, after=after, limit=limit, ascending=(order == "asc")
166
+ )
167
+ return Message.to_letta_messages_from_list(messages)
168
+
169
+
141
170
  @router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id")
142
171
  async def update_step_transaction_id(
143
172
  step_id: str,
@@ -728,35 +728,16 @@ async def add_mcp_server_to_config(
728
728
  return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
729
729
  else:
730
730
  # log to DB
731
- from letta.schemas.mcp import MCPServer
732
-
733
- if isinstance(request, StdioServerConfig):
734
- mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, stdio_config=request)
735
- # don't allow stdio servers
736
- if tool_settings.mcp_disable_stdio: # protected server
737
- raise HTTPException(
738
- status_code=400,
739
- detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
740
- )
741
- elif isinstance(request, SSEServerConfig):
742
- mapped_request = MCPServer(
743
- server_name=request.server_name,
744
- server_type=request.type,
745
- server_url=request.server_url,
746
- token=request.resolve_token(),
747
- custom_headers=request.custom_headers,
748
- )
749
- elif isinstance(request, StreamableHTTPServerConfig):
750
- mapped_request = MCPServer(
751
- server_name=request.server_name,
752
- server_type=request.type,
753
- server_url=request.server_url,
754
- token=request.resolve_token(),
755
- custom_headers=request.custom_headers,
731
+ # Check if stdio servers are disabled
732
+ if isinstance(request, StdioServerConfig) and tool_settings.mcp_disable_stdio:
733
+ raise HTTPException(
734
+ status_code=400,
735
+ detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
756
736
  )
757
737
 
758
738
  # Create MCP server and optimistically sync tools
759
- await server.mcp_manager.create_mcp_server_with_tools(mapped_request, actor=actor)
739
+ # The mcp_manager will handle encryption of sensitive fields
740
+ await server.mcp_manager.create_mcp_server_from_config_with_tools(request, actor=actor)
760
741
 
761
742
  # TODO: don't do this in the future (just return MCPServer)
762
743
  all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
letta/server/server.py CHANGED
@@ -38,12 +38,12 @@ from letta.log import get_logger
38
38
  from letta.orm.errors import NoResultFound
39
39
  from letta.otel.tracing import log_event, trace_method
40
40
  from letta.prompts.gpt_system import get_system_text
41
- from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgent
41
+ from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
42
42
  from letta.schemas.block import Block, BlockUpdate, CreateBlock
43
43
  from letta.schemas.embedding_config import EmbeddingConfig
44
44
 
45
45
  # openai schemas
46
- from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType, ToolSourceType
46
+ from letta.schemas.enums import AgentType, JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType, ToolSourceType
47
47
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
48
48
  from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
49
49
  from letta.schemas.job import Job, JobUpdate
@@ -50,15 +50,13 @@ from letta.otel.tracing import trace_method
50
50
  from letta.prompts.prompt_generator import PromptGenerator
51
51
  from letta.schemas.agent import (
52
52
  AgentState as PydanticAgentState,
53
- AgentType,
54
53
  CreateAgent,
55
54
  InternalTemplateAgentCreate,
56
55
  UpdateAgent,
57
- get_prompt_template_for_agent_type,
58
56
  )
59
57
  from letta.schemas.block import DEFAULT_BLOCKS, Block as PydanticBlock, BlockUpdate
60
58
  from letta.schemas.embedding_config import EmbeddingConfig
61
- from letta.schemas.enums import ProviderType, TagMatchMode, ToolType, VectorDBProvider
59
+ from letta.schemas.enums import AgentType, ProviderType, TagMatchMode, ToolType, VectorDBProvider
62
60
  from letta.schemas.file import FileMetadata as PydanticFileMetadata
63
61
  from letta.schemas.group import Group as PydanticGroup, ManagerType
64
62
  from letta.schemas.llm_config import LLMConfig
@@ -455,7 +453,8 @@ class AgentManager:
455
453
  [{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
456
454
  )
457
455
 
458
- if agent_create.tool_exec_environment_variables:
456
+ agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
457
+ if agent_secrets:
459
458
  env_rows = [
460
459
  {
461
460
  "agent_id": aid,
@@ -463,7 +462,7 @@ class AgentManager:
463
462
  "value": val,
464
463
  "organization_id": actor.organization_id,
465
464
  }
466
- for key, val in agent_create.tool_exec_environment_variables.items()
465
+ for key, val in agent_secrets.items()
467
466
  ]
468
467
  session.execute(insert(AgentEnvironmentVariable).values(env_rows))
469
468
 
@@ -674,7 +673,8 @@ class AgentManager:
674
673
  )
675
674
 
676
675
  env_rows = []
677
- if agent_create.tool_exec_environment_variables:
676
+ agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
677
+ if agent_secrets:
678
678
  env_rows = [
679
679
  {
680
680
  "agent_id": aid,
@@ -682,7 +682,7 @@ class AgentManager:
682
682
  "value": val,
683
683
  "organization_id": actor.organization_id,
684
684
  }
685
- for key, val in agent_create.tool_exec_environment_variables.items()
685
+ for key, val in agent_secrets.items()
686
686
  ]
687
687
  result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
688
688
  env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
@@ -701,8 +701,9 @@ class AgentManager:
701
701
 
702
702
  result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
703
703
 
704
- if agent_create.tool_exec_environment_variables and env_rows:
704
+ if agent_secrets and env_rows:
705
705
  result.tool_exec_environment_variables = [AgentEnvironmentVariable(**row) for row in env_rows]
706
+ result.secrets = [AgentEnvironmentVariable(**row) for row in env_rows]
706
707
 
707
708
  # initial message sequence (skip if _init_with_no_messages is True)
708
709
  if not _init_with_no_messages:
@@ -894,7 +895,8 @@ class AgentManager:
894
895
  )
895
896
  session.expire(agent, ["tags"])
896
897
 
897
- if agent_update.tool_exec_environment_variables is not None:
898
+ agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
899
+ if agent_secrets is not None:
898
900
  session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
899
901
  env_rows = [
900
902
  {
@@ -903,7 +905,7 @@ class AgentManager:
903
905
  "value": v,
904
906
  "organization_id": agent.organization_id,
905
907
  }
906
- for k, v in agent_update.tool_exec_environment_variables.items()
908
+ for k, v in agent_secrets.items()
907
909
  ]
908
910
  if env_rows:
909
911
  self._bulk_insert_pivot(session, AgentEnvironmentVariable.__table__, env_rows)
@@ -1019,7 +1021,8 @@ class AgentManager:
1019
1021
  )
1020
1022
  session.expire(agent, ["tags"])
1021
1023
 
1022
- if agent_update.tool_exec_environment_variables is not None:
1024
+ agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
1025
+ if agent_secrets is not None:
1023
1026
  await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
1024
1027
  env_rows = [
1025
1028
  {
@@ -1028,7 +1031,7 @@ class AgentManager:
1028
1031
  "value": v,
1029
1032
  "organization_id": agent.organization_id,
1030
1033
  }
1031
- for k, v in agent_update.tool_exec_environment_variables.items()
1034
+ for k, v in agent_secrets.items()
1032
1035
  ]
1033
1036
  if env_rows:
1034
1037
  await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
@@ -1544,6 +1547,8 @@ class AgentManager:
1544
1547
  if env_vars:
1545
1548
  for var in agent.tool_exec_environment_variables:
1546
1549
  var.value = env_vars.get(var.key, "")
1550
+ for var in agent.secrets:
1551
+ var.value = env_vars.get(var.key, "")
1547
1552
 
1548
1553
  agent = agent.create(session, actor=actor)
1549
1554
 
@@ -1627,6 +1632,7 @@ class AgentManager:
1627
1632
  # Remove stale variables
1628
1633
  stale_keys = set(existing_vars) - set(env_vars)
1629
1634
  agent.tool_exec_environment_variables = [var for var in updated_vars if var.key not in stale_keys]
1635
+ agent.secrets = [var for var in updated_vars if var.key not in stale_keys]
1630
1636
 
1631
1637
  # Update the agent in the database
1632
1638
  agent.update(session, actor=actor)
@@ -1786,7 +1792,7 @@ class AgentManager:
1786
1792
 
1787
1793
  # note: we only update the system prompt if the core memory is changed
1788
1794
  # this means that the archival/recall memory statistics may be someout out of date
1789
- curr_memory_str = await agent_state.memory.compile_in_thread_async(
1795
+ curr_memory_str = agent_state.memory.compile(
1790
1796
  sources=agent_state.sources,
1791
1797
  tool_usage_rules=tool_rules_solver.compile_tool_rule_prompts(),
1792
1798
  max_files_open=agent_state.max_files_open,
@@ -1976,7 +1982,7 @@ class AgentManager:
1976
1982
  agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor, include_relationships=["memory", "sources"])
1977
1983
  system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
1978
1984
  temp_tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
1979
- new_memory_str = await new_memory.compile_in_thread_async(
1985
+ new_memory_str = new_memory.compile(
1980
1986
  sources=agent_state.sources,
1981
1987
  tool_usage_rules=temp_tool_rules_solver.compile_tool_rule_prompts(),
1982
1988
  max_files_open=agent_state.max_files_open,
@@ -2000,7 +2006,7 @@ class AgentManager:
2000
2006
  agent_state.memory = Memory(
2001
2007
  blocks=blocks,
2002
2008
  file_blocks=agent_state.memory.file_blocks,
2003
- prompt_template=get_prompt_template_for_agent_type(agent_state.agent_type),
2009
+ agent_type=agent_state.agent_type,
2004
2010
  )
2005
2011
 
2006
2012
  # NOTE: don't do this since re-buildin the memory is handled at the start of the step
@@ -209,8 +209,10 @@ class AgentSerializationManager:
209
209
  agent_schema.id = agent_file_id
210
210
 
211
211
  # wipe the values of tool_exec_environment_variables (they contain secrets)
212
- if agent_schema.tool_exec_environment_variables:
213
- agent_schema.tool_exec_environment_variables = {key: "" for key in agent_schema.tool_exec_environment_variables}
212
+ agent_secrets = agent_schema.secrets or agent_schema.tool_exec_environment_variables
213
+ if agent_secrets:
214
+ agent_schema.tool_exec_environment_variables = {key: "" for key in agent_secrets}
215
+ agent_schema.secrets = {key: "" for key in agent_secrets}
214
216
 
215
217
  if agent_schema.messages:
216
218
  for message in agent_schema.messages:
@@ -655,10 +657,16 @@ class AgentSerializationManager:
655
657
  if agent_data.get("source_ids"):
656
658
  agent_data["source_ids"] = [file_to_db_ids[file_id] for file_id in agent_data["source_ids"]]
657
659
 
658
- if env_vars and agent_data.get("tool_exec_environment_variables"):
660
+ if env_vars and agent_data.get("secrets"):
659
661
  # update environment variable values from the provided env_vars dict
662
+ for key in agent_data["secrets"]:
663
+ agent_data["secrets"][key] = env_vars.get(key, "")
664
+ agent_data["tool_exec_environment_variables"][key] = env_vars.get(key, "")
665
+ elif env_vars and agent_data.get("tool_exec_environment_variables"):
666
+ # also handle tool_exec_environment_variables for backwards compatibility
660
667
  for key in agent_data["tool_exec_environment_variables"]:
661
668
  agent_data["tool_exec_environment_variables"][key] = env_vars.get(key, "")
669
+ agent_data["secrets"][key] = env_vars.get(key, "")
662
670
 
663
671
  # Override project_id if provided
664
672
  if project_id:
@@ -87,6 +87,79 @@ class ArchiveManager:
87
87
  )
88
88
  return archive.to_pydantic()
89
89
 
90
+ @enforce_types
91
+ @trace_method
92
+ async def update_archive_async(
93
+ self,
94
+ archive_id: str,
95
+ name: Optional[str] = None,
96
+ description: Optional[str] = None,
97
+ actor: PydanticUser = None,
98
+ ) -> PydanticArchive:
99
+ """Update archive name and/or description."""
100
+ async with db_registry.async_session() as session:
101
+ archive = await ArchiveModel.read_async(
102
+ db_session=session,
103
+ identifier=archive_id,
104
+ actor=actor,
105
+ check_is_deleted=True,
106
+ )
107
+
108
+ if name is not None:
109
+ archive.name = name
110
+ if description is not None:
111
+ archive.description = description
112
+
113
+ await archive.update_async(session, actor=actor)
114
+ return archive.to_pydantic()
115
+
116
+ @enforce_types
117
+ @trace_method
118
+ async def list_archives_async(
119
+ self,
120
+ *,
121
+ actor: PydanticUser,
122
+ before: Optional[str] = None,
123
+ after: Optional[str] = None,
124
+ limit: Optional[int] = 50,
125
+ ascending: bool = False,
126
+ name: Optional[str] = None,
127
+ agent_id: Optional[str] = None,
128
+ ) -> List[PydanticArchive]:
129
+ """List archives with pagination and optional filters.
130
+
131
+ Filters:
132
+ - name: exact match on name
133
+ - agent_id: only archives attached to given agent
134
+ """
135
+ filter_kwargs = {}
136
+ if name is not None:
137
+ filter_kwargs["name"] = name
138
+
139
+ join_model = None
140
+ join_conditions = None
141
+ if agent_id is not None:
142
+ join_model = ArchivesAgents
143
+ join_conditions = [
144
+ ArchivesAgents.archive_id == ArchiveModel.id,
145
+ ArchivesAgents.agent_id == agent_id,
146
+ ]
147
+
148
+ async with db_registry.async_session() as session:
149
+ archives = await ArchiveModel.list_async(
150
+ db_session=session,
151
+ before=before,
152
+ after=after,
153
+ limit=limit,
154
+ ascending=ascending,
155
+ actor=actor,
156
+ check_is_deleted=True,
157
+ join_model=join_model,
158
+ join_conditions=join_conditions,
159
+ **filter_kwargs,
160
+ )
161
+ return [a.to_pydantic() for a in archives]
162
+
90
163
  @enforce_types
91
164
  @trace_method
92
165
  def attach_agent_to_archive(
@@ -29,13 +29,12 @@ from letta.orm.errors import NoResultFound
29
29
  from letta.orm.identity import Identity
30
30
  from letta.orm.passage import ArchivalPassage, SourcePassage
31
31
  from letta.orm.sources_agents import SourcesAgents
32
- from letta.orm.sqlite_functions import adapt_array
33
32
  from letta.otel.tracing import trace_method
34
33
  from letta.prompts import gpt_system
35
34
  from letta.prompts.prompt_generator import PromptGenerator
36
- from letta.schemas.agent import AgentState, AgentType
35
+ from letta.schemas.agent import AgentState
37
36
  from letta.schemas.embedding_config import EmbeddingConfig
38
- from letta.schemas.enums import MessageRole
37
+ from letta.schemas.enums import AgentType, MessageRole
39
38
  from letta.schemas.letta_message_content import TextContent
40
39
  from letta.schemas.memory import Memory
41
40
  from letta.schemas.message import Message, MessageCreate
@@ -246,7 +245,7 @@ def compile_system_message(
246
245
  timezone: str,
247
246
  user_defined_variables: Optional[dict] = None,
248
247
  append_icm_if_missing: bool = True,
249
- template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
248
+ template_format: Literal["f-string", "mustache"] = "f-string",
250
249
  previous_message_count: int = 0,
251
250
  archival_memory_size: int | None = 0,
252
251
  tool_rules_solver: Optional[ToolRulesSolver] = None,
@@ -312,7 +311,7 @@ def compile_system_message(
312
311
  raise ValueError(f"Failed to format system prompt - {str(e)}. System prompt value:\n{system_prompt}")
313
312
 
314
313
  else:
315
- # TODO support for mustache and jinja2
314
+ # TODO support for mustache
316
315
  raise NotImplementedError(template_format)
317
316
 
318
317
  return formatted_prompt
@@ -921,6 +920,8 @@ async def build_passage_query(
921
920
  main_query = main_query.order_by(combined_query.c.embedding.cosine_distance(embedded_text).asc())
922
921
  else:
923
922
  # SQLite with custom vector type
923
+ from letta.orm.sqlite_functions import adapt_array
924
+
924
925
  query_embedding_binary = adapt_array(embedded_text)
925
926
  main_query = main_query.order_by(
926
927
  func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
@@ -1054,6 +1055,8 @@ async def build_source_passage_query(
1054
1055
  query = query.order_by(SourcePassage.embedding.cosine_distance(embedded_text).asc())
1055
1056
  else:
1056
1057
  # SQLite with custom vector type
1058
+ from letta.orm.sqlite_functions import adapt_array
1059
+
1057
1060
  query_embedding_binary = adapt_array(embedded_text)
1058
1061
  query = query.order_by(
1059
1062
  func.cosine_distance(SourcePassage.embedding, query_embedding_binary).asc(),
@@ -1151,6 +1154,8 @@ async def build_agent_passage_query(
1151
1154
  query = query.order_by(ArchivalPassage.embedding.cosine_distance(embedded_text).asc())
1152
1155
  else:
1153
1156
  # SQLite with custom vector type
1157
+ from letta.orm.sqlite_functions import adapt_array
1158
+
1154
1159
  query_embedding_binary = adapt_array(embedded_text)
1155
1160
  query = query.order_by(
1156
1161
  func.cosine_distance(ArchivalPassage.embedding, query_embedding_binary).asc(),