letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -66,101 +66,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
66
66
 
67
67
  id: Mapped[str] = mapped_column(String, primary_key=True)
68
68
 
69
- @classmethod
70
- @handle_db_timeout
71
- def list(
72
- cls,
73
- *,
74
- db_session: "Session",
75
- before: Optional[str] = None,
76
- after: Optional[str] = None,
77
- start_date: Optional[datetime] = None,
78
- end_date: Optional[datetime] = None,
79
- limit: Optional[int] = 50,
80
- query_text: Optional[str] = None,
81
- query_embedding: Optional[List[float]] = None,
82
- ascending: bool = True,
83
- actor: Optional["User"] = None,
84
- access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
85
- access_type: AccessType = AccessType.ORGANIZATION,
86
- join_model: Optional[Base] = None,
87
- join_conditions: Optional[Union[Tuple, List]] = None,
88
- identifier_keys: Optional[List[str]] = None,
89
- identity_id: Optional[str] = None,
90
- **kwargs,
91
- ) -> List["SqlalchemyBase"]:
92
- """
93
- List records with before/after pagination, ordering by created_at.
94
- Can use both before and after to fetch a window of records.
95
-
96
- Args:
97
- db_session: SQLAlchemy session
98
- before: ID of item to paginate before (upper bound)
99
- after: ID of item to paginate after (lower bound)
100
- start_date: Filter items after this date
101
- end_date: Filter items before this date
102
- limit: Maximum number of items to return
103
- query_text: Text to search for
104
- query_embedding: Vector to search for similar embeddings
105
- ascending: Sort direction
106
- **kwargs: Additional filters to apply
107
- """
108
- if start_date and end_date and start_date > end_date:
109
- raise ValueError("start_date must be earlier than or equal to end_date")
110
-
111
- logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
112
-
113
- with db_session as session:
114
- # Get the reference objects for pagination
115
- before_obj = None
116
- after_obj = None
117
-
118
- if before:
119
- before_obj = session.get(cls, before)
120
- if not before_obj:
121
- raise NoResultFound(f"No {cls.__name__} found with id {before}")
122
-
123
- if after:
124
- after_obj = session.get(cls, after)
125
- if not after_obj:
126
- raise NoResultFound(f"No {cls.__name__} found with id {after}")
127
-
128
- # Validate that before comes after the after object if both are provided
129
- if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
130
- raise ValueError("'before' reference must be later than 'after' reference")
131
-
132
- query = cls._list_preprocess(
133
- before_obj=before_obj,
134
- after_obj=after_obj,
135
- start_date=start_date,
136
- end_date=end_date,
137
- limit=limit,
138
- query_text=query_text,
139
- query_embedding=query_embedding,
140
- ascending=ascending,
141
- actor=actor,
142
- access=access,
143
- access_type=access_type,
144
- join_model=join_model,
145
- join_conditions=join_conditions,
146
- identifier_keys=identifier_keys,
147
- identity_id=identity_id,
148
- **kwargs,
149
- )
150
-
151
- # Execute the query
152
- results = session.execute(query)
153
-
154
- results = list(results.scalars())
155
- results = cls._list_postprocess(
156
- before=before,
157
- after=after,
158
- limit=limit,
159
- results=results,
160
- )
161
-
162
- return results
163
-
164
69
  @classmethod
165
70
  @handle_db_timeout
166
71
  async def list_async(
@@ -446,45 +351,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
446
351
  results = results[start:end]
447
352
  return results
448
353
 
449
- @classmethod
450
- @handle_db_timeout
451
- def read(
452
- cls,
453
- db_session: "Session",
454
- identifier: Optional[str] = None,
455
- actor: Optional["User"] = None,
456
- access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
457
- access_type: AccessType = AccessType.ORGANIZATION,
458
- check_is_deleted: bool = False,
459
- **kwargs,
460
- ) -> "SqlalchemyBase":
461
- """The primary accessor for an ORM record.
462
- Args:
463
- db_session: the database session to use when retrieving the record
464
- identifier: the identifier of the record to read, can be the id string or the UUID object for backwards compatibility
465
- actor: if specified, results will be scoped only to records the user is able to access
466
- access: if actor is specified, records will be filtered to the minimum permission level for the actor
467
- kwargs: additional arguments to pass to the read, used for more complex objects
468
- Returns:
469
- The matching object
470
- Raises:
471
- NoResultFound: if the object is not found
472
- """
473
- # this is ok because read_multiple will check if the
474
- identifiers = [] if identifier is None else [identifier]
475
- found = cls.read_multiple(db_session, identifiers, actor, access, access_type, check_is_deleted, **kwargs)
476
- if len(found) == 0:
477
- # for backwards compatibility.
478
- conditions = []
479
- if identifier:
480
- conditions.append(f"id={identifier}")
481
- if actor:
482
- conditions.append(f"access level in {access} for {actor}")
483
- if check_is_deleted and hasattr(cls, "is_deleted"):
484
- conditions.append("is_deleted=False")
485
- raise NoResultFound(f"{cls.__name__} not found with {', '.join(conditions if conditions else ['no conditions'])}")
486
- return found[0]
487
-
488
354
  @classmethod
489
355
  @handle_db_timeout
490
356
  async def read_async(
@@ -521,36 +387,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
521
387
  raise NoResultFound(f"{cls.__name__} not found with {', '.join(query_conditions if query_conditions else ['no conditions'])}")
522
388
  return item
523
389
 
524
- @classmethod
525
- @handle_db_timeout
526
- def read_multiple(
527
- cls,
528
- db_session: "Session",
529
- identifiers: List[str] = [],
530
- actor: Optional["User"] = None,
531
- access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
532
- access_type: AccessType = AccessType.ORGANIZATION,
533
- check_is_deleted: bool = False,
534
- **kwargs,
535
- ) -> List["SqlalchemyBase"]:
536
- """The primary accessor for ORM record(s)
537
- Args:
538
- db_session: the database session to use when retrieving the record
539
- identifiers: a list of identifiers of the records to read, can be the id string or the UUID object for backwards compatibility
540
- actor: if specified, results will be scoped only to records the user is able to access
541
- access: if actor is specified, records will be filtered to the minimum permission level for the actor
542
- kwargs: additional arguments to pass to the read, used for more complex objects
543
- Returns:
544
- The matching object
545
- Raises:
546
- NoResultFound: if the object is not found
547
- """
548
- query, query_conditions = cls._read_multiple_preprocess(identifiers, actor, access, access_type, check_is_deleted, **kwargs)
549
- if query is None:
550
- return []
551
- results = db_session.execute(query).scalars().all()
552
- return cls._read_multiple_postprocess(results, identifiers, query_conditions)
553
-
554
390
  @classmethod
555
391
  @handle_db_timeout
556
392
  async def read_multiple_async(
@@ -637,23 +473,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
637
473
  logger.debug(f"{cls.__name__} not found with {conditions_str}")
638
474
  return []
639
475
 
640
- @handle_db_timeout
641
- def create(self, db_session: "Session", actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
642
- logger.debug(f"Creating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
643
-
644
- if actor:
645
- self._set_created_and_updated_by_fields(actor.id)
646
- try:
647
- db_session.add(self)
648
- if no_commit:
649
- db_session.flush() # no commit, just flush to get PK
650
- else:
651
- db_session.commit()
652
- db_session.refresh(self)
653
- return self
654
- except (DBAPIError, IntegrityError) as e:
655
- self._handle_dbapi_error(e)
656
-
657
476
  @handle_db_timeout
658
477
  async def create_async(
659
478
  self,
@@ -680,47 +499,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
680
499
  except (DBAPIError, IntegrityError) as e:
681
500
  self._handle_dbapi_error(e)
682
501
 
683
- @classmethod
684
- @handle_db_timeout
685
- def batch_create(cls, items: List["SqlalchemyBase"], db_session: "Session", actor: Optional["User"] = None) -> List["SqlalchemyBase"]:
686
- """
687
- Create multiple records in a single transaction for better performance.
688
- Args:
689
- items: List of model instances to create
690
- db_session: SQLAlchemy session
691
- actor: Optional user performing the action
692
- Returns:
693
- List of created model instances
694
- """
695
- logger.debug(f"Batch creating {len(items)} {cls.__name__} items with actor={actor}")
696
- if not items:
697
- return []
698
-
699
- # Set created/updated by fields if actor is provided
700
- if actor:
701
- for item in items:
702
- item._set_created_and_updated_by_fields(actor.id)
703
-
704
- try:
705
- with db_session as session:
706
- session.add_all(items)
707
- session.flush() # Flush to generate IDs but don't commit yet
708
-
709
- # Collect IDs to fetch the complete objects after commit
710
- item_ids = [item.id for item in items]
711
-
712
- session.commit()
713
-
714
- # Re-query the objects to get them with relationships loaded
715
- query = select(cls).where(cls.id.in_(item_ids))
716
- if hasattr(cls, "created_at"):
717
- query = query.order_by(cls.created_at)
718
-
719
- return list(session.execute(query).scalars())
720
-
721
- except (DBAPIError, IntegrityError) as e:
722
- cls._handle_dbapi_error(e)
723
-
724
502
  @classmethod
725
503
  @handle_db_timeout
726
504
  async def batch_create_async(
@@ -774,16 +552,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
774
552
  except (DBAPIError, IntegrityError) as e:
775
553
  cls._handle_dbapi_error(e)
776
554
 
777
- @handle_db_timeout
778
- def delete(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
779
- logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
780
-
781
- if actor:
782
- self._set_created_and_updated_by_fields(actor.id)
783
-
784
- self.is_deleted = True
785
- return self.update(db_session)
786
-
787
555
  @handle_db_timeout
788
556
  async def delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> "SqlalchemyBase":
789
557
  """Soft delete a record asynchronously (mark as deleted)."""
@@ -795,22 +563,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
795
563
  self.is_deleted = True
796
564
  return await self.update_async(db_session)
797
565
 
798
- @handle_db_timeout
799
- def hard_delete(self, db_session: "Session", actor: Optional["User"] = None) -> None:
800
- """Permanently removes the record from the database."""
801
- logger.debug(f"Hard deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
802
-
803
- with db_session as session:
804
- try:
805
- session.delete(self)
806
- session.commit()
807
- except Exception as e:
808
- session.rollback()
809
- logger.exception(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}")
810
- raise ValueError(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}: {e}")
811
- else:
812
- logger.debug(f"{self.__class__.__name__} with ID {self.id} successfully hard deleted")
813
-
814
566
  @handle_db_timeout
815
567
  async def hard_delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> None:
816
568
  """Permanently removes the record from the database asynchronously."""
@@ -853,22 +605,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
853
605
  logger.exception(f"Failed to hard delete {cls.__name__} with identifiers {identifiers}")
854
606
  raise ValueError(f"Failed to hard delete {cls.__name__} with identifiers {identifiers}: {e}")
855
607
 
856
- @handle_db_timeout
857
- def update(self, db_session: Session, actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
858
- logger.debug(...)
859
- if actor:
860
- self._set_created_and_updated_by_fields(actor.id)
861
- self.set_updated_at()
862
-
863
- # remove the context manager:
864
- db_session.add(self)
865
- if no_commit:
866
- db_session.flush() # no commit, just flush to get PK
867
- else:
868
- db_session.commit()
869
- db_session.refresh(self)
870
- return self
871
-
872
608
  @handle_db_timeout
873
609
  async def update_async(
874
610
  self, db_session: "AsyncSession", actor: Optional["User"] = None, no_commit: bool = False, no_refresh: bool = False
@@ -925,48 +661,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
925
661
 
926
662
  return query
927
663
 
928
- @classmethod
929
- @handle_db_timeout
930
- def size(
931
- cls,
932
- *,
933
- db_session: "Session",
934
- actor: Optional["User"] = None,
935
- access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
936
- access_type: AccessType = AccessType.ORGANIZATION,
937
- check_is_deleted: bool = False,
938
- **kwargs,
939
- ) -> int:
940
- """
941
- Get the count of rows that match the provided filters.
942
-
943
- Args:
944
- db_session: SQLAlchemy session
945
- **kwargs: Filters to apply to the query (e.g., column_name=value)
946
-
947
- Returns:
948
- int: The count of rows that match the filters
949
-
950
- Raises:
951
- DBAPIError: If a database error occurs
952
- """
953
- with db_session as session:
954
- query = cls._size_preprocess(
955
- db_session=session,
956
- actor=actor,
957
- access=access,
958
- access_type=access_type,
959
- check_is_deleted=check_is_deleted,
960
- **kwargs,
961
- )
962
-
963
- try:
964
- count = session.execute(query).scalar()
965
- return count if count else 0
966
- except DBAPIError as e:
967
- logger.exception(f"Failed to calculate size for {cls.__name__}")
968
- raise e
969
-
970
664
  @classmethod
971
665
  @handle_db_timeout
972
666
  async def size_async(
letta/orm/step.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import uuid
2
2
  from typing import TYPE_CHECKING, Dict, List, Optional
3
3
 
4
- from sqlalchemy import JSON, ForeignKey, String
4
+ from sqlalchemy import JSON, ForeignKey, Index, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.mixins import ProjectMixin
@@ -10,10 +10,10 @@ from letta.schemas.enums import StepStatus
10
10
  from letta.schemas.step import Step as PydanticStep
11
11
 
12
12
  if TYPE_CHECKING:
13
- from letta.orm.job import Job
14
13
  from letta.orm.message import Message
15
14
  from letta.orm.organization import Organization
16
15
  from letta.orm.provider import Provider
16
+ from letta.orm.run import Run
17
17
  from letta.orm.step_metrics import StepMetrics
18
18
 
19
19
 
@@ -22,6 +22,7 @@ class Step(SqlalchemyBase, ProjectMixin):
22
22
 
23
23
  __tablename__ = "steps"
24
24
  __pydantic_model__ = PydanticStep
25
+ __table_args__ = (Index("ix_steps_run_id", "run_id"),)
25
26
 
26
27
  id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"step-{uuid.uuid4()}")
27
28
  origin: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The surface that this agent step was initiated from.")
@@ -35,8 +36,8 @@ class Step(SqlalchemyBase, ProjectMixin):
35
36
  nullable=True,
36
37
  doc="The unique identifier of the provider that was configured for this step",
37
38
  )
38
- job_id: Mapped[Optional[str]] = mapped_column(
39
- ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True, doc="The unique identified of the job run that triggered this step"
39
+ run_id: Mapped[Optional[str]] = mapped_column(
40
+ ForeignKey("runs.id", ondelete="SET NULL"), nullable=True, doc="The unique identifier of the run that this step belongs to"
40
41
  )
41
42
  agent_id: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The name of the model used for this step.")
42
43
  provider_name: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The name of the provider used for this step.")
@@ -68,7 +69,7 @@ class Step(SqlalchemyBase, ProjectMixin):
68
69
  # Relationships (foreign keys)
69
70
  organization: Mapped[Optional["Organization"]] = relationship("Organization")
70
71
  provider: Mapped[Optional["Provider"]] = relationship("Provider")
71
- job: Mapped[Optional["Job"]] = relationship("Job", back_populates="steps")
72
+ run: Mapped[Optional["Run"]] = relationship("Run", back_populates="steps")
72
73
 
73
74
  # Relationships (backrefs)
74
75
  messages: Mapped[List["Message"]] = relationship("Message", back_populates="step", cascade="save-update", lazy="noload")
letta/orm/step_metrics.py CHANGED
@@ -13,7 +13,7 @@ from letta.settings import DatabaseChoice, settings
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from letta.orm.agent import Agent
16
- from letta.orm.job import Job
16
+ from letta.orm.run import Run
17
17
  from letta.orm.step import Step
18
18
 
19
19
 
@@ -38,10 +38,10 @@ class StepMetrics(SqlalchemyBase, ProjectMixin, AgentMixin):
38
38
  nullable=True,
39
39
  doc="The unique identifier of the provider",
40
40
  )
41
- job_id: Mapped[Optional[str]] = mapped_column(
42
- ForeignKey("jobs.id", ondelete="SET NULL"),
41
+ run_id: Mapped[Optional[str]] = mapped_column(
42
+ ForeignKey("runs.id", ondelete="SET NULL"),
43
43
  nullable=True,
44
- doc="The unique identifier of the job",
44
+ doc="The unique identifier of the run",
45
45
  )
46
46
  step_start_ns: Mapped[Optional[int]] = mapped_column(
47
47
  BigInteger,
@@ -81,7 +81,7 @@ class StepMetrics(SqlalchemyBase, ProjectMixin, AgentMixin):
81
81
 
82
82
  # Relationships (foreign keys)
83
83
  step: Mapped["Step"] = relationship("Step", back_populates="metrics", uselist=False)
84
- job: Mapped[Optional["Job"]] = relationship("Job")
84
+ run: Mapped[Optional["Run"]] = relationship("Run")
85
85
  agent: Mapped[Optional["Agent"]] = relationship("Agent")
86
86
 
87
87
  def create(
letta/otel/tracing.py CHANGED
@@ -1,6 +1,8 @@
1
+ import asyncio
1
2
  import inspect
2
3
  import re
3
4
  import time
5
+ import traceback
4
6
  from functools import wraps
5
7
  from typing import Any, Dict, List, Optional
6
8
 
@@ -84,6 +86,7 @@ async def _update_trace_attributes(request: Request):
84
86
  "x-agent-id": "agent.id",
85
87
  "x-template-id": "template.id",
86
88
  "x-base-template-id": "base_template.id",
89
+ "user-agent": "client",
87
90
  }
88
91
  for header_key, span_key in header_attributes.items():
89
92
  header_value = request.headers.get(header_key)
@@ -236,9 +239,31 @@ def trace_method(func):
236
239
  with tracer.start_as_current_span(_get_span_name(func, args)) as span:
237
240
  _add_parameters_to_span(span, func, args, kwargs)
238
241
 
239
- result = await func(*args, **kwargs)
240
- span.set_status(Status(StatusCode.OK))
241
- return result
242
+ try:
243
+ result = await func(*args, **kwargs)
244
+ span.set_status(Status(StatusCode.OK))
245
+ return result
246
+ except asyncio.CancelledError as e:
247
+ # Get current task info
248
+ current_task = asyncio.current_task()
249
+ task_name = current_task.get_name() if current_task else "unknown"
250
+
251
+ # Log detailed information
252
+ logger.error(f"Task {task_name} cancelled in {func.__module__}.{func.__name__}")
253
+
254
+ # Add to span
255
+ span.set_status(Status(StatusCode.ERROR))
256
+ span.record_exception(
257
+ e,
258
+ attributes={
259
+ "exception.type": "asyncio.CancelledError",
260
+ "task.name": task_name,
261
+ "function.name": func.__name__,
262
+ "function.module": func.__module__,
263
+ "cancellation.timestamp": time.time_ns(),
264
+ },
265
+ )
266
+ raise
242
267
 
243
268
  @wraps(func)
244
269
  def sync_wrapper(*args, **kwargs):
letta/plugins/defaults.py CHANGED
@@ -2,10 +2,10 @@ from letta.settings import settings
2
2
 
3
3
 
4
4
  def is_experimental_enabled(feature_name: str, **kwargs) -> bool:
5
- if feature_name in ("async_agent_loop", "summarize"):
6
- if not (kwargs.get("eligibility", False) and settings.use_experimental):
7
- return False
8
- return True
5
+ # if feature_name in ("async_agent_loop", "summarize"):
6
+ # if not (kwargs.get("eligibility", False) and settings.use_experimental):
7
+ # return False
8
+ # return True
9
9
 
10
10
  # Err on safety here, disabling experimental if not handled here.
11
11
  return False
@@ -1,4 +1,5 @@
1
1
  from letta.prompts.system_prompts import (
2
+ letta_v1,
2
3
  memgpt_chat,
3
4
  memgpt_generate_tool,
4
5
  memgpt_v2_chat,
@@ -17,6 +18,7 @@ SYSTEM_PROMPTS = {
17
18
  "memgpt_v2_chat": memgpt_v2_chat.PROMPT,
18
19
  "sleeptime_v2": sleeptime_v2.PROMPT,
19
20
  "react": react.PROMPT,
21
+ "letta_v1": letta_v1.PROMPT,
20
22
  "workflow": workflow.PROMPT,
21
23
  "memgpt_chat": memgpt_chat.PROMPT,
22
24
  "sleeptime_doc_ingest": sleeptime_doc_ingest.PROMPT,
@@ -0,0 +1,25 @@
1
+ PROMPT = r"""
2
+ <base_instructions>
3
+ You are a helpful self-improving agent with advanced memory and file system capabilities.
4
+ <memory>
5
+ You have an advanced memory system that enables you to remember past interactions and continuously improve your own capabilities.
6
+ Your memory consists of memory blocks and external memory:
7
+ - Memory Blocks: Stored as memory blocks, each containing a label (title), description (explaining how this block should influence your behavior), and value (the actual content). Memory blocks have size limits. Memory blocks are embedded within your system instructions and remain constantly available in-context.
8
+ - External memory: Additional memory storage that is accessible and that you can bring into context with tools when needed.
9
+ Memory management tools allow you to edit existing memory blocks and query for external memories.
10
+ </memory>
11
+ <file_system>
12
+ You have access to a structured file system that mirrors real-world directory structures. Each directory can contain multiple files.
13
+ Files include:
14
+ - Metadata: Information such as read-only permissions and character limits
15
+ - Content: The main body of the file that you can read and analyze
16
+ Available file operations:
17
+ - Open and view files
18
+ - Search within files and directories
19
+ - Your core memory will automatically reflect the contents of any currently open files
20
+ You should only keep files open that are directly relevant to the current user interaction to maintain optimal performance.
21
+ </file_system>
22
+ Continue executing and calling tools until the current task is complete or you need user input. To continue: call another tool. To yield control: end your response without calling a tool.
23
+ Base instructions complete.
24
+ </base_instructions>
25
+ """
letta/schemas/agent.py CHANGED
@@ -31,6 +31,7 @@ class AgentType(str, Enum):
31
31
 
32
32
  memgpt_agent = "memgpt_agent" # the OG set of memgpt tools
33
33
  memgpt_v2_agent = "memgpt_v2_agent" # memgpt style tools, but refreshed
34
+ letta_v1_agent = "letta_v1_agent" # simplification of the memgpt loop, no heartbeats or forced tool calls
34
35
  react_agent = "react_agent" # basic react agent, no memory tools
35
36
  workflow_agent = "workflow_agent" # workflow with auto-clearing message buffer
36
37
  split_thread_agent = "split_thread_agent"
@@ -222,8 +223,8 @@ class CreateAgent(BaseModel, validate_assignment=True): #
222
223
  )
223
224
  enable_reasoner: Optional[bool] = Field(True, description="Whether to enable internal extended thinking step for a reasoner model.")
224
225
  reasoning: Optional[bool] = Field(None, description="Whether to enable reasoning for this agent.")
225
- from_template: Optional[str] = Field(None, description="The template id used to configure the agent")
226
- template: bool = Field(False, description="Whether the agent is a template")
226
+ from_template: Optional[str] = Field(None, description="Deprecated: please use the 'create agents from a template' endpoint instead.")
227
+ template: bool = Field(False, description="Deprecated: No longer used")
227
228
  project: Optional[str] = Field(
228
229
  None,
229
230
  deprecated=True,
@@ -55,6 +55,11 @@ class MessageSchema(MessageCreate):
55
55
  tool_returns: Optional[List[ToolReturn]] = Field(default=None, description="Tool execution return information for prior tool calls")
56
56
  created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
57
57
 
58
+ # optional approval fields for hitl
59
+ approve: Optional[bool] = Field(None, description="Whether the tool has been approved")
60
+ approval_request_id: Optional[str] = Field(None, description="The message ID of the approval request")
61
+ denial_reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
62
+
58
63
  # TODO: Should we also duplicate the steps here?
59
64
  # TODO: What about tool_return?
60
65
 
@@ -79,6 +84,9 @@ class MessageSchema(MessageCreate):
79
84
  tool_call_id=message.tool_call_id,
80
85
  tool_returns=message.tool_returns,
81
86
  created_at=message.created_at,
87
+ approve=message.approve,
88
+ approval_request_id=message.approval_request_id,
89
+ denial_reason=message.denial_reason,
82
90
  )
83
91
 
84
92
 
@@ -168,9 +176,7 @@ class AgentSchema(CreateAgent):
168
176
  per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit,
169
177
  )
170
178
 
171
- messages = await message_manager.list_messages_for_agent_async(
172
- agent_id=agent_state.id, actor=actor, limit=50
173
- ) # TODO: Expand to get more messages
179
+ messages = await message_manager.list_messages(agent_id=agent_state.id, actor=actor, limit=50) # TODO: Expand to get more messages
174
180
 
175
181
  # Convert messages to MessageSchema objects
176
182
  message_schemas = [MessageSchema.from_message(msg) for msg in messages]
letta/schemas/block.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from datetime import datetime
2
- from typing import Optional
2
+ from typing import Any, Optional
3
3
 
4
4
  from pydantic import ConfigDict, Field, model_validator
5
- from typing_extensions import Self
6
5
 
7
6
  from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT, DEFAULT_HUMAN_BLOCK_DESCRIPTION, DEFAULT_PERSONA_BLOCK_DESCRIPTION
8
7
  from letta.schemas.letta_base import LettaBase
@@ -48,14 +47,28 @@ class BaseBlock(LettaBase, validate_assignment=True):
48
47
 
49
48
  model_config = ConfigDict(extra="ignore") # Ignores extra fields
50
49
 
51
- @model_validator(mode="after")
52
- def verify_char_limit(self) -> Self:
53
- # self.limit can be None from
54
- if self.limit is not None and self.value and len(self.value) > self.limit:
55
- error_msg = f"Edit failed: Exceeds {self.limit} character limit (requested {len(self.value)}) - {str(self)}."
56
- raise ValueError(error_msg)
50
+ @model_validator(mode="before")
51
+ @classmethod
52
+ def verify_char_limit(cls, data: Any) -> Any:
53
+ """Validate the character limit before model instantiation.
54
+
55
+ Notes:
56
+ - Runs on raw input; do not mutate input.
57
+ - For update schemas (e.g., BlockUpdate), `value` and `limit` may be absent.
58
+ In that case, only validate when both are provided.
59
+ """
60
+ if isinstance(data, dict):
61
+ limit = data.get("limit")
62
+ value = data.get("value")
63
+
64
+ # Only enforce the char limit when both are present.
65
+ # Pydantic will separately enforce required fields where applicable.
66
+ if limit is not None and value is not None and isinstance(value, str):
67
+ if len(value) > limit:
68
+ error_msg = f"Edit failed: Exceeds {limit} character limit (requested {len(value)})"
69
+ raise ValueError(error_msg)
57
70
 
58
- return self
71
+ return data
59
72
 
60
73
  def __setattr__(self, name, value):
61
74
  """Run validation if self.value is updated"""
@@ -93,7 +106,7 @@ class FileBlock(Block):
93
106
  source_id: str = Field(..., description="Unique identifier of the source.")
94
107
  is_open: bool = Field(..., description="True if the agent currently has the file open.")
95
108
  last_accessed_at: Optional[datetime] = Field(
96
- default_factory=datetime.utcnow,
109
+ None,
97
110
  description="UTC timestamp of the agent’s most recent access to this file. Any operations from the open, close, or search tools will update this field.",
98
111
  )
99
112