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
@@ -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
@@ -35,6 +35,7 @@ from letta.schemas.mcp import (
35
35
  UpdateStdioMCPServer,
36
36
  UpdateStreamableHTTPMCPServer,
37
37
  )
38
+ from letta.schemas.secret import Secret, SecretDict
38
39
  from letta.schemas.tool import Tool as PydanticTool, ToolCreate, ToolUpdate
39
40
  from letta.schemas.user import User as PydanticUser
40
41
  from letta.server.db import db_registry
@@ -354,6 +355,69 @@ class MCPManager:
354
355
  logger.error(f"Failed to create MCP server: {e}")
355
356
  raise
356
357
 
358
+ @enforce_types
359
+ async def create_mcp_server_from_config(
360
+ self, server_config: Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig], actor: PydanticUser
361
+ ) -> MCPServer:
362
+ """
363
+ Create an MCP server from a config object, handling encryption of sensitive fields.
364
+
365
+ This method converts the server config to an MCPServer model and encrypts
366
+ sensitive fields like tokens and custom headers.
367
+ """
368
+ # Create base MCPServer object
369
+ if isinstance(server_config, StdioServerConfig):
370
+ mcp_server = MCPServer(server_name=server_config.server_name, server_type=server_config.type, stdio_config=server_config)
371
+ elif isinstance(server_config, SSEServerConfig):
372
+ mcp_server = MCPServer(
373
+ server_name=server_config.server_name,
374
+ server_type=server_config.type,
375
+ server_url=server_config.server_url,
376
+ )
377
+ # Encrypt sensitive fields
378
+ token = server_config.resolve_token()
379
+ if token:
380
+ token_secret = Secret.from_plaintext(token)
381
+ mcp_server.set_token_secret(token_secret)
382
+ if server_config.custom_headers:
383
+ headers_secret = SecretDict.from_plaintext(server_config.custom_headers)
384
+ mcp_server.set_custom_headers_secret(headers_secret)
385
+
386
+ elif isinstance(server_config, StreamableHTTPServerConfig):
387
+ mcp_server = MCPServer(
388
+ server_name=server_config.server_name,
389
+ server_type=server_config.type,
390
+ server_url=server_config.server_url,
391
+ )
392
+ # Encrypt sensitive fields
393
+ token = server_config.resolve_token()
394
+ if token:
395
+ token_secret = Secret.from_plaintext(token)
396
+ mcp_server.set_token_secret(token_secret)
397
+ if server_config.custom_headers:
398
+ headers_secret = SecretDict.from_plaintext(server_config.custom_headers)
399
+ mcp_server.set_custom_headers_secret(headers_secret)
400
+ else:
401
+ raise ValueError(f"Unsupported server config type: {type(server_config)}")
402
+
403
+ return mcp_server
404
+
405
+ @enforce_types
406
+ async def create_mcp_server_from_config_with_tools(
407
+ self, server_config: Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig], actor: PydanticUser
408
+ ) -> MCPServer:
409
+ """
410
+ Create an MCP server from a config object and optimistically sync its tools.
411
+
412
+ This method handles encryption of sensitive fields and then creates the server
413
+ with automatic tool synchronization.
414
+ """
415
+ # Convert config to MCPServer with encryption
416
+ mcp_server = await self.create_mcp_server_from_config(server_config, actor)
417
+
418
+ # Create the server with tools
419
+ return await self.create_mcp_server_with_tools(mcp_server, actor)
420
+
357
421
  @enforce_types
358
422
  async def create_mcp_server_with_tools(self, pydantic_mcp_server: MCPServer, actor: PydanticUser) -> MCPServer:
359
423
  """
@@ -420,10 +484,33 @@ class MCPManager:
420
484
  # Update tool attributes with only the fields that were explicitly set
421
485
  update_data = mcp_server_update.model_dump(to_orm=True, exclude_unset=True)
422
486
 
423
- # Ensure custom_headers None is stored as SQL NULL, not JSON null
424
- if update_data.get("custom_headers") is None:
425
- update_data.pop("custom_headers", None)
426
- setattr(mcp_server, "custom_headers", null())
487
+ # Handle encryption for token if provided
488
+ if "token" in update_data and update_data["token"] is not None:
489
+ token_secret = Secret.from_plaintext(update_data["token"])
490
+ secret_dict = token_secret.to_dict()
491
+ update_data["token_enc"] = secret_dict["encrypted"]
492
+ # During migration phase, also update plaintext
493
+ if not token_secret._was_encrypted:
494
+ update_data["token"] = secret_dict["plaintext"]
495
+ else:
496
+ update_data["token"] = None
497
+
498
+ # Handle encryption for custom_headers if provided
499
+ if "custom_headers" in update_data:
500
+ if update_data["custom_headers"] is not None:
501
+ headers_secret = SecretDict.from_plaintext(update_data["custom_headers"])
502
+ secret_dict = headers_secret.to_dict()
503
+ update_data["custom_headers_enc"] = secret_dict["encrypted"]
504
+ # During migration phase, also update plaintext
505
+ if not headers_secret._was_encrypted:
506
+ update_data["custom_headers"] = secret_dict["plaintext"]
507
+ else:
508
+ update_data["custom_headers"] = None
509
+ else:
510
+ # Ensure custom_headers None is stored as SQL NULL, not JSON null
511
+ update_data.pop("custom_headers", None)
512
+ setattr(mcp_server, "custom_headers", null())
513
+ setattr(mcp_server, "custom_headers_enc", None)
427
514
 
428
515
  for key, value in update_data.items():
429
516
  setattr(mcp_server, key, value)
@@ -664,6 +751,64 @@ class MCPManager:
664
751
  raise ValueError(f"Unsupported server config type: {type(server_config)}")
665
752
 
666
753
  # OAuth-related methods
754
+ def _oauth_orm_to_pydantic(self, oauth_session: MCPOAuth) -> MCPOAuthSession:
755
+ """
756
+ Convert OAuth ORM model to Pydantic model, handling decryption of sensitive fields.
757
+ """
758
+ from letta.settings import settings
759
+
760
+ # Get decrypted values using the dual-read approach
761
+ # Secret.from_db() will automatically use settings.encryption_key if available
762
+ access_token = None
763
+ if oauth_session.access_token_enc or oauth_session.access_token:
764
+ if settings.encryption_key:
765
+ secret = Secret.from_db(oauth_session.access_token_enc, oauth_session.access_token)
766
+ access_token = secret.get_plaintext()
767
+ else:
768
+ # No encryption key, use plaintext if available
769
+ access_token = oauth_session.access_token
770
+
771
+ refresh_token = None
772
+ if oauth_session.refresh_token_enc or oauth_session.refresh_token:
773
+ if settings.encryption_key:
774
+ secret = Secret.from_db(oauth_session.refresh_token_enc, oauth_session.refresh_token)
775
+ refresh_token = secret.get_plaintext()
776
+ else:
777
+ # No encryption key, use plaintext if available
778
+ refresh_token = oauth_session.refresh_token
779
+
780
+ client_secret = None
781
+ if oauth_session.client_secret_enc or oauth_session.client_secret:
782
+ if settings.encryption_key:
783
+ secret = Secret.from_db(oauth_session.client_secret_enc, oauth_session.client_secret)
784
+ client_secret = secret.get_plaintext()
785
+ else:
786
+ # No encryption key, use plaintext if available
787
+ client_secret = oauth_session.client_secret
788
+
789
+ return MCPOAuthSession(
790
+ id=oauth_session.id,
791
+ state=oauth_session.state,
792
+ server_id=oauth_session.server_id,
793
+ server_url=oauth_session.server_url,
794
+ server_name=oauth_session.server_name,
795
+ user_id=oauth_session.user_id,
796
+ organization_id=oauth_session.organization_id,
797
+ authorization_url=oauth_session.authorization_url,
798
+ authorization_code=oauth_session.authorization_code,
799
+ access_token=access_token,
800
+ refresh_token=refresh_token,
801
+ token_type=oauth_session.token_type,
802
+ expires_at=oauth_session.expires_at,
803
+ scope=oauth_session.scope,
804
+ client_id=oauth_session.client_id,
805
+ client_secret=client_secret,
806
+ redirect_uri=oauth_session.redirect_uri,
807
+ status=oauth_session.status,
808
+ created_at=oauth_session.created_at,
809
+ updated_at=oauth_session.updated_at,
810
+ )
811
+
667
812
  @enforce_types
668
813
  async def create_oauth_session(self, session_create: MCPOAuthSessionCreate, actor: PydanticUser) -> MCPOAuthSession:
669
814
  """Create a new OAuth session for MCP server authentication."""
@@ -682,18 +827,8 @@ class MCPManager:
682
827
  )
683
828
  oauth_session = await oauth_session.create_async(session, actor=actor)
684
829
 
685
- # Convert to Pydantic model
686
- return MCPOAuthSession(
687
- id=oauth_session.id,
688
- state=oauth_session.state,
689
- server_url=oauth_session.server_url,
690
- server_name=oauth_session.server_name,
691
- user_id=oauth_session.user_id,
692
- organization_id=oauth_session.organization_id,
693
- status=oauth_session.status,
694
- created_at=oauth_session.created_at,
695
- updated_at=oauth_session.updated_at,
696
- )
830
+ # Convert to Pydantic model - note: new sessions won't have tokens yet
831
+ return self._oauth_orm_to_pydantic(oauth_session)
697
832
 
698
833
  @enforce_types
699
834
  async def get_oauth_session_by_id(self, session_id: str, actor: PydanticUser) -> Optional[MCPOAuthSession]:
@@ -701,27 +836,7 @@ class MCPManager:
701
836
  async with db_registry.async_session() as session:
702
837
  try:
703
838
  oauth_session = await MCPOAuth.read_async(db_session=session, identifier=session_id, actor=actor)
704
- return MCPOAuthSession(
705
- id=oauth_session.id,
706
- state=oauth_session.state,
707
- server_url=oauth_session.server_url,
708
- server_name=oauth_session.server_name,
709
- user_id=oauth_session.user_id,
710
- organization_id=oauth_session.organization_id,
711
- authorization_url=oauth_session.authorization_url,
712
- authorization_code=oauth_session.authorization_code,
713
- access_token=oauth_session.access_token,
714
- refresh_token=oauth_session.refresh_token,
715
- token_type=oauth_session.token_type,
716
- expires_at=oauth_session.expires_at,
717
- scope=oauth_session.scope,
718
- client_id=oauth_session.client_id,
719
- client_secret=oauth_session.client_secret,
720
- redirect_uri=oauth_session.redirect_uri,
721
- status=oauth_session.status,
722
- created_at=oauth_session.created_at,
723
- updated_at=oauth_session.updated_at,
724
- )
839
+ return self._oauth_orm_to_pydantic(oauth_session)
725
840
  except NoResultFound:
726
841
  return None
727
842
 
@@ -747,27 +862,7 @@ class MCPManager:
747
862
  if not oauth_session:
748
863
  return None
749
864
 
750
- return MCPOAuthSession(
751
- id=oauth_session.id,
752
- state=oauth_session.state,
753
- server_url=oauth_session.server_url,
754
- server_name=oauth_session.server_name,
755
- user_id=oauth_session.user_id,
756
- organization_id=oauth_session.organization_id,
757
- authorization_url=oauth_session.authorization_url,
758
- authorization_code=oauth_session.authorization_code,
759
- access_token=oauth_session.access_token,
760
- refresh_token=oauth_session.refresh_token,
761
- token_type=oauth_session.token_type,
762
- expires_at=oauth_session.expires_at,
763
- scope=oauth_session.scope,
764
- client_id=oauth_session.client_id,
765
- client_secret=oauth_session.client_secret,
766
- redirect_uri=oauth_session.redirect_uri,
767
- status=oauth_session.status,
768
- created_at=oauth_session.created_at,
769
- updated_at=oauth_session.updated_at,
770
- )
865
+ return self._oauth_orm_to_pydantic(oauth_session)
771
866
 
772
867
  @enforce_types
773
868
  async def update_oauth_session(self, session_id: str, session_update: MCPOAuthSessionUpdate, actor: PydanticUser) -> MCPOAuthSession:
@@ -780,10 +875,37 @@ class MCPManager:
780
875
  oauth_session.authorization_url = session_update.authorization_url
781
876
  if session_update.authorization_code is not None:
782
877
  oauth_session.authorization_code = session_update.authorization_code
878
+
879
+ # Handle encryption for access_token
783
880
  if session_update.access_token is not None:
784
- oauth_session.access_token = session_update.access_token
881
+ from letta.settings import settings
882
+
883
+ if settings.encryption_key:
884
+ token_secret = Secret.from_plaintext(session_update.access_token)
885
+ secret_dict = token_secret.to_dict()
886
+ oauth_session.access_token_enc = secret_dict["encrypted"]
887
+ # During migration phase, also update plaintext
888
+ oauth_session.access_token = secret_dict["plaintext"] if not token_secret._was_encrypted else None
889
+ else:
890
+ # No encryption, store plaintext
891
+ oauth_session.access_token = session_update.access_token
892
+ oauth_session.access_token_enc = None
893
+
894
+ # Handle encryption for refresh_token
785
895
  if session_update.refresh_token is not None:
786
- oauth_session.refresh_token = session_update.refresh_token
896
+ from letta.settings import settings
897
+
898
+ if settings.encryption_key:
899
+ token_secret = Secret.from_plaintext(session_update.refresh_token)
900
+ secret_dict = token_secret.to_dict()
901
+ oauth_session.refresh_token_enc = secret_dict["encrypted"]
902
+ # During migration phase, also update plaintext
903
+ oauth_session.refresh_token = secret_dict["plaintext"] if not token_secret._was_encrypted else None
904
+ else:
905
+ # No encryption, store plaintext
906
+ oauth_session.refresh_token = session_update.refresh_token
907
+ oauth_session.refresh_token_enc = None
908
+
787
909
  if session_update.token_type is not None:
788
910
  oauth_session.token_type = session_update.token_type
789
911
  if session_update.expires_at is not None:
@@ -792,8 +914,22 @@ class MCPManager:
792
914
  oauth_session.scope = session_update.scope
793
915
  if session_update.client_id is not None:
794
916
  oauth_session.client_id = session_update.client_id
917
+
918
+ # Handle encryption for client_secret
795
919
  if session_update.client_secret is not None:
796
- oauth_session.client_secret = session_update.client_secret
920
+ from letta.settings import settings
921
+
922
+ if settings.encryption_key:
923
+ secret_secret = Secret.from_plaintext(session_update.client_secret)
924
+ secret_dict = secret_secret.to_dict()
925
+ oauth_session.client_secret_enc = secret_dict["encrypted"]
926
+ # During migration phase, also update plaintext
927
+ oauth_session.client_secret = secret_dict["plaintext"] if not secret_secret._was_encrypted else None
928
+ else:
929
+ # No encryption, store plaintext
930
+ oauth_session.client_secret = session_update.client_secret
931
+ oauth_session.client_secret_enc = None
932
+
797
933
  if session_update.redirect_uri is not None:
798
934
  oauth_session.redirect_uri = session_update.redirect_uri
799
935
  if session_update.status is not None:
@@ -804,27 +940,7 @@ class MCPManager:
804
940
 
805
941
  oauth_session = await oauth_session.update_async(db_session=session, actor=actor)
806
942
 
807
- return MCPOAuthSession(
808
- id=oauth_session.id,
809
- state=oauth_session.state,
810
- server_url=oauth_session.server_url,
811
- server_name=oauth_session.server_name,
812
- user_id=oauth_session.user_id,
813
- organization_id=oauth_session.organization_id,
814
- authorization_url=oauth_session.authorization_url,
815
- authorization_code=oauth_session.authorization_code,
816
- access_token=oauth_session.access_token,
817
- refresh_token=oauth_session.refresh_token,
818
- token_type=oauth_session.token_type,
819
- expires_at=oauth_session.expires_at,
820
- scope=oauth_session.scope,
821
- client_id=oauth_session.client_id,
822
- client_secret=oauth_session.client_secret,
823
- redirect_uri=oauth_session.redirect_uri,
824
- status=oauth_session.status,
825
- created_at=oauth_session.created_at,
826
- updated_at=oauth_session.updated_at,
827
- )
943
+ return self._oauth_orm_to_pydantic(oauth_session)
828
944
 
829
945
  @enforce_types
830
946
  async def delete_oauth_session(self, session_id: str, actor: PydanticUser) -> None:
@@ -9,12 +9,14 @@ from sqlalchemy.orm import Session
9
9
  from letta.helpers.singleton import singleton
10
10
  from letta.orm.errors import NoResultFound
11
11
  from letta.orm.job import Job as JobModel
12
+ from letta.orm.message import Message as MessageModel
12
13
  from letta.orm.sqlalchemy_base import AccessType
13
14
  from letta.orm.step import Step as StepModel
14
15
  from letta.orm.step_metrics import StepMetrics as StepMetricsModel
15
16
  from letta.otel.tracing import get_trace_id, trace_method
16
17
  from letta.schemas.enums import StepStatus
17
18
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
19
+ from letta.schemas.message import Message as PydanticMessage
18
20
  from letta.schemas.openai.chat_completion_response import UsageStatistics
19
21
  from letta.schemas.step import Step as PydanticStep
20
22
  from letta.schemas.step_metrics import StepMetrics as PydanticStepMetrics
@@ -237,6 +239,30 @@ class StepManager:
237
239
  await session.commit()
238
240
  return step.to_pydantic()
239
241
 
242
+ @enforce_types
243
+ @trace_method
244
+ async def list_step_messages_async(
245
+ self,
246
+ step_id: str,
247
+ actor: PydanticUser,
248
+ before: str | None = None,
249
+ after: str | None = None,
250
+ limit: int = 100,
251
+ ascending: bool = False,
252
+ ) -> List[PydanticMessage]:
253
+ async with db_registry.async_session() as session:
254
+ messages = MessageModel.list(
255
+ db_session=session,
256
+ before=before,
257
+ after=after,
258
+ ascending=ascending,
259
+ limit=limit,
260
+ actor=actor,
261
+ join_model=StepModel,
262
+ join_conditions=[MessageModel.step.id == step_id],
263
+ )
264
+ return [message.to_pydantic() for message in messages]
265
+
240
266
  @enforce_types
241
267
  @trace_method
242
268
  async def update_step_stop_reason(self, actor: PydanticUser, step_id: str, stop_reason: StopReasonType) -> PydanticStep:
@@ -19,7 +19,6 @@ from letta.services.agent_manager import AgentManager
19
19
  from letta.services.message_manager import MessageManager
20
20
  from letta.services.summarizer.enums import SummarizationMode
21
21
  from letta.system import package_summarize_message_no_counts
22
- from letta.templates.template_helper import render_template
23
22
  from letta.utils import safe_create_task
24
23
 
25
24
  logger = get_logger(__name__)
@@ -280,8 +279,7 @@ class Summarizer:
280
279
  formatted_evicted_messages = [f"{i}. {msg}" for (i, msg) in enumerate(formatted_evicted_messages)]
281
280
  formatted_in_context_messages = [f"{i + offset}. {msg}" for (i, msg) in enumerate(formatted_in_context_messages)]
282
281
 
283
- summary_request_text = render_template(
284
- "summary_request_text.j2",
282
+ summary_request_text = build_summary_request_text(
285
283
  retain_count=retain_count,
286
284
  evicted_messages=formatted_evicted_messages,
287
285
  in_context_messages=formatted_in_context_messages,
@@ -304,6 +302,30 @@ def simple_formatter(messages: List[Message], include_system: bool = False) -> s
304
302
  return "\n".join(json.dumps(msg) for msg in parsed_messages)
305
303
 
306
304
 
305
+ def build_summary_request_text(retain_count: int, evicted_messages: List[str], in_context_messages: List[str]) -> str:
306
+ parts: List[str] = []
307
+ if retain_count == 0:
308
+ parts.append(
309
+ "You’re a memory-recall helper for an AI that is about to forget all prior messages. Scan the conversation history and write crisp notes that capture any important facts or insights about the conversation history."
310
+ )
311
+ else:
312
+ parts.append(
313
+ f"You’re a memory-recall helper for an AI that can only keep the last {retain_count} messages. Scan the conversation history, focusing on messages about to drop out of that window, and write crisp notes that capture any important facts or insights about the human so they aren’t lost."
314
+ )
315
+
316
+ if evicted_messages:
317
+ parts.append("\n(Older) Evicted Messages:")
318
+ for item in evicted_messages:
319
+ parts.append(f" {item}")
320
+
321
+ if retain_count > 0 and in_context_messages:
322
+ parts.append("\n(Newer) In-Context Messages:")
323
+ for item in in_context_messages:
324
+ parts.append(f" {item}")
325
+
326
+ return "\n".join(parts) + "\n"
327
+
328
+
307
329
  def simple_message_wrapper(openai_msg: dict) -> Message:
308
330
  """Extremely simple way to map from role/content to Message object w/ throwaway dummy fields"""
309
331
 
@@ -26,6 +26,7 @@ class TelemetryManager:
26
26
  async def create_provider_trace_async(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
27
27
  async with db_registry.async_session() as session:
28
28
  provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
29
+ provider_trace.organization_id = actor.organization_id
29
30
  if provider_trace_create.request_json:
30
31
  request_json_str = json_dumps(provider_trace_create.request_json)
31
32
  provider_trace.request_json = json_loads(request_json_str)
@@ -43,6 +44,7 @@ class TelemetryManager:
43
44
  def create_provider_trace(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
44
45
  with db_registry.session() as session:
45
46
  provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
47
+ provider_trace.organization_id = actor.organization_id
46
48
  if provider_trace_create.request_json:
47
49
  request_json_str = json_dumps(provider_trace_create.request_json)
48
50
  provider_trace.request_json = json_loads(request_json_str)
@@ -51,7 +51,7 @@ class ExternalComposioToolExecutor(ToolExecutor):
51
51
 
52
52
  def _get_entity_id(self, agent_state: AgentState) -> Optional[str]:
53
53
  """Extract the entity ID from environment variables."""
54
- for env_var in agent_state.tool_exec_environment_variables:
54
+ for env_var in agent_state.secrets:
55
55
  if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
56
56
  return env_var.value
57
57
  return None
@@ -36,7 +36,7 @@ class SandboxToolExecutor(ToolExecutor):
36
36
  ) -> ToolExecutionResult:
37
37
  # Store original memory state
38
38
  if agent_state:
39
- orig_memory_str = await agent_state.memory.compile_in_thread_async()
39
+ orig_memory_str = agent_state.memory.compile()
40
40
  else:
41
41
  orig_memory_str = None
42
42
 
@@ -89,7 +89,7 @@ class SandboxToolExecutor(ToolExecutor):
89
89
 
90
90
  # Verify memory integrity
91
91
  if agent_state:
92
- new_memory_str = await agent_state.memory.compile_in_thread_async()
92
+ new_memory_str = agent_state.memory.compile()
93
93
  assert orig_memory_str == new_memory_str, "Memory should not be modified in a sandbox tool"
94
94
 
95
95
  # Update agent memory if needed