letta-nightly 0.11.7.dev20250915104130__py3-none-any.whl → 0.11.7.dev20250917104122__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 (99) 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 +1 -1
  5. letta/agents/letta_agent.py +1 -4
  6. letta/agents/letta_agent_v2.py +2 -1
  7. letta/agents/voice_agent.py +1 -1
  8. letta/functions/function_sets/multi_agent.py +1 -1
  9. letta/functions/helpers.py +1 -1
  10. letta/helpers/converters.py +8 -2
  11. letta/helpers/crypto_utils.py +144 -0
  12. letta/llm_api/llm_api_tools.py +0 -1
  13. letta/llm_api/llm_client_base.py +0 -2
  14. letta/orm/__init__.py +1 -0
  15. letta/orm/agent.py +5 -1
  16. letta/orm/job.py +3 -1
  17. letta/orm/mcp_oauth.py +6 -0
  18. letta/orm/mcp_server.py +7 -1
  19. letta/orm/sqlalchemy_base.py +2 -1
  20. letta/prompts/gpt_system.py +13 -15
  21. letta/prompts/system_prompts/__init__.py +27 -0
  22. letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
  23. letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
  24. letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
  25. letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
  26. letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
  27. letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
  28. letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
  29. letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
  30. letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
  31. letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
  32. letta/schemas/agent.py +10 -7
  33. letta/schemas/job.py +10 -0
  34. letta/schemas/mcp.py +146 -6
  35. letta/schemas/provider_trace.py +0 -2
  36. letta/schemas/run.py +2 -0
  37. letta/schemas/secret.py +378 -0
  38. letta/serialize_schemas/marshmallow_agent.py +4 -0
  39. letta/server/rest_api/dependencies.py +37 -0
  40. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
  41. letta/server/rest_api/routers/v1/__init__.py +2 -0
  42. letta/server/rest_api/routers/v1/agents.py +115 -107
  43. letta/server/rest_api/routers/v1/archives.py +113 -0
  44. letta/server/rest_api/routers/v1/blocks.py +44 -20
  45. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  46. letta/server/rest_api/routers/v1/folders.py +107 -47
  47. letta/server/rest_api/routers/v1/groups.py +52 -32
  48. letta/server/rest_api/routers/v1/identities.py +110 -21
  49. letta/server/rest_api/routers/v1/internal_templates.py +28 -13
  50. letta/server/rest_api/routers/v1/jobs.py +19 -14
  51. letta/server/rest_api/routers/v1/llms.py +6 -8
  52. letta/server/rest_api/routers/v1/messages.py +14 -14
  53. letta/server/rest_api/routers/v1/organizations.py +1 -1
  54. letta/server/rest_api/routers/v1/providers.py +40 -16
  55. letta/server/rest_api/routers/v1/runs.py +28 -20
  56. letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
  57. letta/server/rest_api/routers/v1/sources.py +44 -45
  58. letta/server/rest_api/routers/v1/steps.py +27 -25
  59. letta/server/rest_api/routers/v1/tags.py +11 -7
  60. letta/server/rest_api/routers/v1/telemetry.py +11 -6
  61. letta/server/rest_api/routers/v1/tools.py +78 -80
  62. letta/server/rest_api/routers/v1/users.py +1 -1
  63. letta/server/rest_api/routers/v1/voice.py +6 -5
  64. letta/server/rest_api/utils.py +1 -18
  65. letta/services/agent_manager.py +17 -9
  66. letta/services/agent_serialization_manager.py +11 -3
  67. letta/services/archive_manager.py +73 -0
  68. letta/services/file_manager.py +6 -0
  69. letta/services/group_manager.py +2 -1
  70. letta/services/helpers/agent_manager_helper.py +6 -1
  71. letta/services/identity_manager.py +67 -0
  72. letta/services/job_manager.py +18 -2
  73. letta/services/mcp_manager.py +198 -82
  74. letta/services/provider_manager.py +14 -1
  75. letta/services/source_manager.py +11 -1
  76. letta/services/telemetry_manager.py +2 -0
  77. letta/services/tool_executor/composio_tool_executor.py +1 -1
  78. letta/services/tool_manager.py +46 -9
  79. letta/services/tool_sandbox/base.py +2 -3
  80. letta/utils.py +4 -2
  81. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
  82. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
  83. letta/prompts/system/memgpt_base.txt +0 -54
  84. letta/prompts/system/memgpt_chat_compressed.txt +0 -13
  85. letta/prompts/system/memgpt_chat_fstring.txt +0 -51
  86. letta/prompts/system/memgpt_convo_only.txt +0 -12
  87. letta/prompts/system/memgpt_doc.txt +0 -50
  88. letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
  89. letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
  90. letta/prompts/system/memgpt_memory_only.txt +0 -29
  91. letta/prompts/system/memgpt_modified_chat.txt +0 -23
  92. letta/prompts/system/memgpt_modified_o1.txt +0 -31
  93. letta/prompts/system/memgpt_offline_memory.txt +0 -23
  94. letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
  95. letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
  96. letta/prompts/system/sleeptime.txt +0 -37
  97. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
  98. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
  99. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
@@ -455,7 +455,8 @@ class AgentManager:
455
455
  [{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
456
456
  )
457
457
 
458
- if agent_create.tool_exec_environment_variables:
458
+ agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
459
+ if agent_secrets:
459
460
  env_rows = [
460
461
  {
461
462
  "agent_id": aid,
@@ -463,7 +464,7 @@ class AgentManager:
463
464
  "value": val,
464
465
  "organization_id": actor.organization_id,
465
466
  }
466
- for key, val in agent_create.tool_exec_environment_variables.items()
467
+ for key, val in agent_secrets.items()
467
468
  ]
468
469
  session.execute(insert(AgentEnvironmentVariable).values(env_rows))
469
470
 
@@ -674,7 +675,8 @@ class AgentManager:
674
675
  )
675
676
 
676
677
  env_rows = []
677
- if agent_create.tool_exec_environment_variables:
678
+ agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
679
+ if agent_secrets:
678
680
  env_rows = [
679
681
  {
680
682
  "agent_id": aid,
@@ -682,7 +684,7 @@ class AgentManager:
682
684
  "value": val,
683
685
  "organization_id": actor.organization_id,
684
686
  }
685
- for key, val in agent_create.tool_exec_environment_variables.items()
687
+ for key, val in agent_secrets.items()
686
688
  ]
687
689
  result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
688
690
  env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
@@ -701,8 +703,9 @@ class AgentManager:
701
703
 
702
704
  result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
703
705
 
704
- if agent_create.tool_exec_environment_variables and env_rows:
706
+ if agent_secrets and env_rows:
705
707
  result.tool_exec_environment_variables = [AgentEnvironmentVariable(**row) for row in env_rows]
708
+ result.secrets = [AgentEnvironmentVariable(**row) for row in env_rows]
706
709
 
707
710
  # initial message sequence (skip if _init_with_no_messages is True)
708
711
  if not _init_with_no_messages:
@@ -894,7 +897,8 @@ class AgentManager:
894
897
  )
895
898
  session.expire(agent, ["tags"])
896
899
 
897
- if agent_update.tool_exec_environment_variables is not None:
900
+ agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
901
+ if agent_secrets is not None:
898
902
  session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
899
903
  env_rows = [
900
904
  {
@@ -903,7 +907,7 @@ class AgentManager:
903
907
  "value": v,
904
908
  "organization_id": agent.organization_id,
905
909
  }
906
- for k, v in agent_update.tool_exec_environment_variables.items()
910
+ for k, v in agent_secrets.items()
907
911
  ]
908
912
  if env_rows:
909
913
  self._bulk_insert_pivot(session, AgentEnvironmentVariable.__table__, env_rows)
@@ -1019,7 +1023,8 @@ class AgentManager:
1019
1023
  )
1020
1024
  session.expire(agent, ["tags"])
1021
1025
 
1022
- if agent_update.tool_exec_environment_variables is not None:
1026
+ agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
1027
+ if agent_secrets is not None:
1023
1028
  await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
1024
1029
  env_rows = [
1025
1030
  {
@@ -1028,7 +1033,7 @@ class AgentManager:
1028
1033
  "value": v,
1029
1034
  "organization_id": agent.organization_id,
1030
1035
  }
1031
- for k, v in agent_update.tool_exec_environment_variables.items()
1036
+ for k, v in agent_secrets.items()
1032
1037
  ]
1033
1038
  if env_rows:
1034
1039
  await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
@@ -1544,6 +1549,8 @@ class AgentManager:
1544
1549
  if env_vars:
1545
1550
  for var in agent.tool_exec_environment_variables:
1546
1551
  var.value = env_vars.get(var.key, "")
1552
+ for var in agent.secrets:
1553
+ var.value = env_vars.get(var.key, "")
1547
1554
 
1548
1555
  agent = agent.create(session, actor=actor)
1549
1556
 
@@ -1627,6 +1634,7 @@ class AgentManager:
1627
1634
  # Remove stale variables
1628
1635
  stale_keys = set(existing_vars) - set(env_vars)
1629
1636
  agent.tool_exec_environment_variables = [var for var in updated_vars if var.key not in stale_keys]
1637
+ agent.secrets = [var for var in updated_vars if var.key not in stale_keys]
1630
1638
 
1631
1639
  # Update the agent in the database
1632
1640
  agent.update(session, actor=actor)
@@ -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(
@@ -404,8 +404,10 @@ class FileManager:
404
404
  self,
405
405
  source_id: str,
406
406
  actor: PydanticUser,
407
+ before: Optional[str] = None,
407
408
  after: Optional[str] = None,
408
409
  limit: Optional[int] = 50,
410
+ ascending: Optional[bool] = True,
409
411
  include_content: bool = False,
410
412
  strip_directory_prefix: bool = False,
411
413
  check_status_updates: bool = False,
@@ -415,8 +417,10 @@ class FileManager:
415
417
  Args:
416
418
  source_id: Source to list files from
417
419
  actor: User performing the request
420
+ before: Before filter
418
421
  after: Pagination cursor
419
422
  limit: Maximum number of files to return
423
+ ascending: Sort by ascending or descending order
420
424
  include_content: Whether to include file content
421
425
  strip_directory_prefix: Whether to strip directory prefix from filenames
422
426
  check_status_updates: Whether to check and update status for timeout and embedding completion
@@ -429,8 +433,10 @@ class FileManager:
429
433
 
430
434
  files = await FileMetadataModel.list_async(
431
435
  db_session=session,
436
+ before=before,
432
437
  after=after,
433
438
  limit=limit,
439
+ ascending=ascending,
434
440
  organization_id=actor.organization_id,
435
441
  source_id=source_id,
436
442
  query_options=options,
@@ -29,6 +29,7 @@ class GroupManager:
29
29
  before: Optional[str] = None,
30
30
  after: Optional[str] = None,
31
31
  limit: Optional[int] = 50,
32
+ ascending: bool = True,
32
33
  show_hidden_groups: Optional[bool] = None,
33
34
  ) -> list[PydanticGroup]:
34
35
  async with db_registry.async_session() as session:
@@ -50,7 +51,7 @@ class GroupManager:
50
51
  query = query.where((GroupModel.hidden.is_(None)) | (GroupModel.hidden == False))
51
52
 
52
53
  # Apply pagination
53
- query = await _apply_group_pagination_async(query, before, after, session, ascending=True)
54
+ query = await _apply_group_pagination_async(query, before, after, session, ascending=ascending)
54
55
 
55
56
  if limit:
56
57
  query = query.limit(limit)
@@ -29,7 +29,6 @@ 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
@@ -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(),
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from typing import List, Optional
2
3
 
3
4
  from fastapi import HTTPException
@@ -9,6 +10,8 @@ from letta.orm.block import Block as BlockModel
9
10
  from letta.orm.errors import UniqueConstraintViolationError
10
11
  from letta.orm.identity import Identity as IdentityModel
11
12
  from letta.otel.tracing import trace_method
13
+ from letta.schemas.agent import AgentState
14
+ from letta.schemas.block import Block
12
15
  from letta.schemas.identity import (
13
16
  Identity as PydanticIdentity,
14
17
  IdentityCreate,
@@ -35,6 +38,7 @@ class IdentityManager:
35
38
  before: Optional[str] = None,
36
39
  after: Optional[str] = None,
37
40
  limit: Optional[int] = 50,
41
+ ascending: bool = False,
38
42
  actor: PydanticUser = None,
39
43
  ) -> list[PydanticIdentity]:
40
44
  async with db_registry.async_session() as session:
@@ -51,6 +55,7 @@ class IdentityManager:
51
55
  before=before,
52
56
  after=after,
53
57
  limit=limit,
58
+ ascending=ascending,
54
59
  **filters,
55
60
  )
56
61
  return [identity.to_pydantic() for identity in identities]
@@ -272,3 +277,65 @@ class IdentityManager:
272
277
  current_ids = {item.id for item in current_relationship}
273
278
  new_items = [item for item in found_items if item.id not in current_ids]
274
279
  current_relationship.extend(new_items)
280
+
281
+ @enforce_types
282
+ @trace_method
283
+ async def list_agents_for_identity_async(
284
+ self,
285
+ identity_id: str,
286
+ before: Optional[str] = None,
287
+ after: Optional[str] = None,
288
+ limit: Optional[int] = 50,
289
+ ascending: bool = False,
290
+ actor: PydanticUser = None,
291
+ ) -> List[AgentState]:
292
+ """
293
+ Get all agents associated with the specified identity.
294
+ """
295
+ async with db_registry.async_session() as session:
296
+ # First verify the identity exists and belongs to the user
297
+ identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
298
+ if identity is None:
299
+ raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found")
300
+
301
+ # Get agents associated with this identity with pagination
302
+ agents = await AgentModel.list_async(
303
+ db_session=session,
304
+ before=before,
305
+ after=after,
306
+ limit=limit,
307
+ ascending=ascending,
308
+ identity_id=identity.id,
309
+ )
310
+ return await asyncio.gather(*[agent.to_pydantic_async() for agent in agents])
311
+
312
+ @enforce_types
313
+ @trace_method
314
+ async def list_blocks_for_identity_async(
315
+ self,
316
+ identity_id: str,
317
+ before: Optional[str] = None,
318
+ after: Optional[str] = None,
319
+ limit: Optional[int] = 50,
320
+ ascending: bool = False,
321
+ actor: PydanticUser = None,
322
+ ) -> List[Block]:
323
+ """
324
+ Get all blocks associated with the specified identity.
325
+ """
326
+ async with db_registry.async_session() as session:
327
+ # First verify the identity exists and belongs to the user
328
+ identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
329
+ if identity is None:
330
+ raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found")
331
+
332
+ # Get blocks associated with this identity with pagination
333
+ blocks = await BlockModel.list_async(
334
+ db_session=session,
335
+ before=before,
336
+ after=after,
337
+ limit=limit,
338
+ ascending=ascending,
339
+ identity_id=identity.id,
340
+ )
341
+ return [block.to_pydantic() for block in blocks]
@@ -18,6 +18,7 @@ from letta.otel.tracing import log_event, trace_method
18
18
  from letta.schemas.enums import JobStatus, JobType, MessageRole
19
19
  from letta.schemas.job import BatchJob as PydanticBatchJob, Job as PydanticJob, JobUpdate, LettaRequestConfig
20
20
  from letta.schemas.letta_message import LettaMessage
21
+ from letta.schemas.letta_stop_reason import StopReasonType
21
22
  from letta.schemas.message import Message as PydanticMessage
22
23
  from letta.schemas.run import Run as PydanticRun
23
24
  from letta.schemas.step import Step as PydanticStep
@@ -207,7 +208,12 @@ class JobManager:
207
208
  @enforce_types
208
209
  @trace_method
209
210
  async def safe_update_job_status_async(
210
- self, job_id: str, new_status: JobStatus, actor: PydanticUser, metadata: Optional[dict] = None
211
+ self,
212
+ job_id: str,
213
+ new_status: JobStatus,
214
+ actor: PydanticUser,
215
+ stop_reason: Optional[StopReasonType] = None,
216
+ metadata: Optional[dict] = None,
211
217
  ) -> bool:
212
218
  """
213
219
  Safely update job status with state transition guards.
@@ -217,7 +223,7 @@ class JobManager:
217
223
  True if update was successful, False if update was skipped due to invalid transition
218
224
  """
219
225
  try:
220
- job_update_builder = partial(JobUpdate, status=new_status)
226
+ job_update_builder = partial(JobUpdate, status=new_status, stop_reason=stop_reason)
221
227
 
222
228
  # If metadata is provided, merge it with existing metadata
223
229
  if metadata:
@@ -268,6 +274,7 @@ class JobManager:
268
274
  statuses: Optional[List[JobStatus]] = None,
269
275
  job_type: JobType = JobType.JOB,
270
276
  ascending: bool = True,
277
+ stop_reason: Optional[StopReasonType] = None,
271
278
  ) -> List[PydanticJob]:
272
279
  """List all jobs with optional pagination and status filter."""
273
280
  with db_registry.session() as session:
@@ -277,6 +284,10 @@ class JobManager:
277
284
  if statuses:
278
285
  filter_kwargs["status"] = statuses
279
286
 
287
+ # Add stop_reason filter if provided
288
+ if stop_reason is not None:
289
+ filter_kwargs["stop_reason"] = stop_reason
290
+
280
291
  jobs = JobModel.list(
281
292
  db_session=session,
282
293
  before=before,
@@ -299,6 +310,7 @@ class JobManager:
299
310
  job_type: JobType = JobType.JOB,
300
311
  ascending: bool = True,
301
312
  source_id: Optional[str] = None,
313
+ stop_reason: Optional[StopReasonType] = None,
302
314
  ) -> List[PydanticJob]:
303
315
  """List all jobs with optional pagination and status filter."""
304
316
  from sqlalchemy import and_, or_, select
@@ -317,6 +329,10 @@ class JobManager:
317
329
  column = column.op("->>")("source_id")
318
330
  query = query.where(column == source_id)
319
331
 
332
+ # add stop_reason filter if provided
333
+ if stop_reason is not None:
334
+ query = query.where(JobModel.stop_reason == stop_reason)
335
+
320
336
  # handle cursor-based pagination
321
337
  if before or after:
322
338
  # get cursor objects