letta-nightly 0.11.7.dev20251008104128__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.
letta/__init__.py CHANGED
@@ -5,7 +5,7 @@ try:
5
5
  __version__ = version("letta")
6
6
  except PackageNotFoundError:
7
7
  # Fallback for development installations
8
- __version__ = "0.11.7"
8
+ __version__ = "0.12.0"
9
9
 
10
10
  if os.environ.get("LETTA_VERSION"):
11
11
  __version__ = os.environ["LETTA_VERSION"]
@@ -595,9 +595,30 @@ class LettaAgentV3(LettaAgentV2):
595
595
  # -1. no tool call, no content
596
596
  if tool_call is None and (content is None or len(content) == 0):
597
597
  # Edge case is when there's also no content - basically, the LLM "no-op'd"
598
- # In this case, we actually do not want to persist the no-op message
599
- continue_stepping, heartbeat_reason, stop_reason = False, None, LettaStopReason(stop_reason=StopReasonType.end_turn.value)
600
- messages_to_persist = initial_messages or []
598
+ # If RequiredBeforeExitToolRule exists and not all required tools have been called,
599
+ # inject a rule-violation heartbeat to keep looping and inform the model.
600
+ uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools]))
601
+ if uncalled:
602
+ # TODO: we may need to change this to not have a "heartbeat" prefix for v3?
603
+ heartbeat_reason = (
604
+ f"{NON_USER_MSG_PREFIX}ToolRuleViolated: You must call {', '.join(uncalled)} at least once to exit the loop."
605
+ )
606
+ from letta.server.rest_api.utils import create_heartbeat_system_message
607
+
608
+ heartbeat_msg = create_heartbeat_system_message(
609
+ agent_id=agent_state.id,
610
+ model=agent_state.llm_config.model,
611
+ function_call_success=True,
612
+ timezone=agent_state.timezone,
613
+ heartbeat_reason=heartbeat_reason,
614
+ run_id=run_id,
615
+ )
616
+ messages_to_persist = (initial_messages or []) + [heartbeat_msg]
617
+ continue_stepping, stop_reason = True, None
618
+ else:
619
+ # In this case, we actually do not want to persist the no-op message
620
+ continue_stepping, heartbeat_reason, stop_reason = False, None, LettaStopReason(stop_reason=StopReasonType.end_turn.value)
621
+ messages_to_persist = initial_messages or []
601
622
 
602
623
  # 0. If there's no tool call, we can early exit
603
624
  elif tool_call is None:
@@ -627,7 +648,8 @@ class LettaAgentV3(LettaAgentV2):
627
648
  run_id=run_id,
628
649
  is_approval_response=is_approval or is_denial,
629
650
  force_set_request_heartbeat=False,
630
- add_heartbeat_on_continue=False,
651
+ # If we're continuing due to a required-before-exit rule, include a heartbeat to guide the model
652
+ add_heartbeat_on_continue=bool(heartbeat_reason),
631
653
  )
632
654
  messages_to_persist = (initial_messages or []) + assistant_message
633
655
 
@@ -843,7 +865,13 @@ class LettaAgentV3(LettaAgentV2):
843
865
  stop_reason: LettaStopReason | None = None
844
866
 
845
867
  if tool_call_name is None:
846
- # No tool call? End loop
868
+ # No tool call if there are required-before-exit tools uncalled, keep stepping
869
+ # and provide explicit feedback to the model; otherwise end the loop.
870
+ uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools]))
871
+ if uncalled and not is_final_step:
872
+ reason = f"{NON_USER_MSG_PREFIX}ToolRuleViolated: You must call {', '.join(uncalled)} at least once to exit the loop."
873
+ return True, reason, None
874
+ # No required tools remaining → end turn
847
875
  return False, None, LettaStopReason(stop_reason=StopReasonType.end_turn.value)
848
876
  else:
849
877
  if tool_rule_violated:
@@ -0,0 +1,161 @@
1
+ """
2
+ Database URI utilities for consistent database connection handling across the application.
3
+
4
+ This module provides utilities for parsing and converting database URIs to ensure
5
+ consistent behavior between the main application, alembic migrations, and other
6
+ database-related components.
7
+ """
8
+
9
+ from typing import Optional
10
+ from urllib.parse import urlparse, urlunparse
11
+
12
+
13
+ def parse_database_uri(uri: str) -> dict[str, Optional[str]]:
14
+ """
15
+ Parse a database URI into its components.
16
+
17
+ Args:
18
+ uri: Database URI (e.g., postgresql://user:pass@host:port/db)
19
+
20
+ Returns:
21
+ Dictionary with parsed components: scheme, driver, user, password, host, port, database
22
+ """
23
+ parsed = urlparse(uri)
24
+
25
+ # Extract driver from scheme (e.g., postgresql+asyncpg -> asyncpg)
26
+ scheme_parts = parsed.scheme.split("+")
27
+ base_scheme = scheme_parts[0] if scheme_parts else ""
28
+ driver = scheme_parts[1] if len(scheme_parts) > 1 else None
29
+
30
+ return {
31
+ "scheme": base_scheme,
32
+ "driver": driver,
33
+ "user": parsed.username,
34
+ "password": parsed.password,
35
+ "host": parsed.hostname,
36
+ "port": str(parsed.port) if parsed.port else None,
37
+ "database": parsed.path.lstrip("/") if parsed.path else None,
38
+ "query": parsed.query,
39
+ "fragment": parsed.fragment,
40
+ }
41
+
42
+
43
+ def build_database_uri(
44
+ scheme: str = "postgresql",
45
+ driver: Optional[str] = None,
46
+ user: Optional[str] = None,
47
+ password: Optional[str] = None,
48
+ host: Optional[str] = None,
49
+ port: Optional[str] = None,
50
+ database: Optional[str] = None,
51
+ query: Optional[str] = None,
52
+ fragment: Optional[str] = None,
53
+ ) -> str:
54
+ """
55
+ Build a database URI from components.
56
+
57
+ Args:
58
+ scheme: Base scheme (e.g., "postgresql")
59
+ driver: Driver name (e.g., "asyncpg", "pg8000")
60
+ user: Username
61
+ password: Password
62
+ host: Hostname
63
+ port: Port number
64
+ database: Database name
65
+ query: Query string
66
+ fragment: Fragment
67
+
68
+ Returns:
69
+ Complete database URI
70
+ """
71
+ # Combine scheme and driver
72
+ full_scheme = f"{scheme}+{driver}" if driver else scheme
73
+
74
+ # Build netloc (user:password@host:port)
75
+ netloc_parts = []
76
+ if user:
77
+ if password:
78
+ netloc_parts.append(f"{user}:{password}")
79
+ else:
80
+ netloc_parts.append(user)
81
+
82
+ if host:
83
+ if port:
84
+ netloc_parts.append(f"{host}:{port}")
85
+ else:
86
+ netloc_parts.append(host)
87
+
88
+ netloc = "@".join(netloc_parts) if netloc_parts else ""
89
+
90
+ # Build path
91
+ path = f"/{database}" if database else ""
92
+
93
+ # Build the URI
94
+ return urlunparse((full_scheme, netloc, path, "", query or "", fragment or ""))
95
+
96
+
97
+ def convert_to_async_uri(uri: str) -> str:
98
+ """
99
+ Convert a database URI to use the asyncpg driver for async operations.
100
+
101
+ Args:
102
+ uri: Original database URI
103
+
104
+ Returns:
105
+ URI with asyncpg driver and ssl parameter adjustments
106
+ """
107
+ components = parse_database_uri(uri)
108
+
109
+ # Convert to asyncpg driver
110
+ components["driver"] = "asyncpg"
111
+
112
+ # Build the new URI
113
+ new_uri = build_database_uri(**components)
114
+
115
+ # Replace sslmode= with ssl= for asyncpg compatibility
116
+ new_uri = new_uri.replace("sslmode=", "ssl=")
117
+
118
+ return new_uri
119
+
120
+
121
+ def convert_to_sync_uri(uri: str) -> str:
122
+ """
123
+ Convert a database URI to use the pg8000 driver for sync operations (alembic).
124
+
125
+ Args:
126
+ uri: Original database URI
127
+
128
+ Returns:
129
+ URI with pg8000 driver and sslmode parameter adjustments
130
+ """
131
+ components = parse_database_uri(uri)
132
+
133
+ # Convert to pg8000 driver
134
+ components["driver"] = "pg8000"
135
+
136
+ # Build the new URI
137
+ new_uri = build_database_uri(**components)
138
+
139
+ # Replace ssl= with sslmode= for pg8000 compatibility
140
+ new_uri = new_uri.replace("ssl=", "sslmode=")
141
+
142
+ return new_uri
143
+
144
+
145
+ def get_database_uri_for_context(uri: str, context: str = "async") -> str:
146
+ """
147
+ Get the appropriate database URI for a specific context.
148
+
149
+ Args:
150
+ uri: Original database URI
151
+ context: Context type ("async" for asyncpg, "sync" for pg8000, "alembic" for pg8000)
152
+
153
+ Returns:
154
+ URI formatted for the specified context
155
+ """
156
+ if context in ["async"]:
157
+ return convert_to_async_uri(uri)
158
+ elif context in ["sync", "alembic"]:
159
+ return convert_to_sync_uri(uri)
160
+ else:
161
+ raise ValueError(f"Unknown context: {context}. Must be 'async', 'sync', or 'alembic'")
@@ -325,6 +325,7 @@ class AnthropicClient(LLMClientBase):
325
325
  data["system"] = self._add_cache_control_to_system_message(system_content)
326
326
  data["messages"] = PydanticMessage.to_anthropic_dicts_from_list(
327
327
  messages=messages[1:],
328
+ current_model=llm_config.model,
328
329
  inner_thoughts_xml_tag=inner_thoughts_xml_tag,
329
330
  put_inner_thoughts_in_kwargs=put_kwargs,
330
331
  # if react, use native content + strip heartbeats
@@ -311,6 +311,7 @@ class GoogleVertexClient(LLMClientBase):
311
311
  contents = self.add_dummy_model_messages(
312
312
  PydanticMessage.to_google_dicts_from_list(
313
313
  messages,
314
+ current_model=llm_config.model,
314
315
  put_inner_thoughts_in_kwargs=False if agent_type == AgentType.letta_v1_agent else True,
315
316
  native_content=True if agent_type == AgentType.letta_v1_agent else False,
316
317
  ),
letta/orm/__init__.py CHANGED
@@ -27,6 +27,7 @@ from letta.orm.prompt import Prompt
27
27
  from letta.orm.provider import Provider
28
28
  from letta.orm.provider_trace import ProviderTrace
29
29
  from letta.orm.run import Run
30
+ from letta.orm.run_metrics import RunMetrics
30
31
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
31
32
  from letta.orm.source import Source
32
33
  from letta.orm.sources_agents import SourcesAgents
@@ -0,0 +1,82 @@
1
+ from datetime import datetime, timezone
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from sqlalchemy import BigInteger, ForeignKey, Integer, String
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
7
+
8
+ from letta.orm.mixins import AgentMixin, OrganizationMixin, ProjectMixin, TemplateMixin
9
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
10
+ from letta.schemas.run_metrics import RunMetrics as PydanticRunMetrics
11
+ from letta.schemas.user import User
12
+ from letta.settings import DatabaseChoice, settings
13
+
14
+ if TYPE_CHECKING:
15
+ from letta.orm.agent import Agent
16
+ from letta.orm.run import Run
17
+ from letta.orm.step import Step
18
+
19
+
20
+ class RunMetrics(SqlalchemyBase, ProjectMixin, AgentMixin, OrganizationMixin, TemplateMixin):
21
+ """Tracks performance metrics for agent steps."""
22
+
23
+ __tablename__ = "run_metrics"
24
+ __pydantic_model__ = PydanticRunMetrics
25
+
26
+ id: Mapped[str] = mapped_column(
27
+ ForeignKey("runs.id", ondelete="CASCADE"),
28
+ primary_key=True,
29
+ doc="The unique identifier of the run this metric belongs to (also serves as PK)",
30
+ )
31
+ run_start_ns: Mapped[Optional[int]] = mapped_column(
32
+ BigInteger,
33
+ nullable=True,
34
+ doc="The timestamp of the start of the run in nanoseconds",
35
+ )
36
+ run_ns: Mapped[Optional[int]] = mapped_column(
37
+ BigInteger,
38
+ nullable=True,
39
+ doc="Total time for the run in nanoseconds",
40
+ )
41
+ num_steps: Mapped[Optional[int]] = mapped_column(
42
+ Integer,
43
+ nullable=True,
44
+ doc="The number of steps in the run",
45
+ )
46
+ run: Mapped[Optional["Run"]] = relationship("Run", foreign_keys=[id])
47
+ agent: Mapped[Optional["Agent"]] = relationship("Agent")
48
+
49
+ def create(
50
+ self,
51
+ db_session: Session,
52
+ actor: Optional[User] = None,
53
+ no_commit: bool = False,
54
+ ) -> "RunMetrics":
55
+ """Override create to handle SQLite timestamp issues"""
56
+ # For SQLite, explicitly set timestamps as server_default may not work
57
+ if settings.database_engine == DatabaseChoice.SQLITE:
58
+ now = datetime.now(timezone.utc)
59
+ if not self.created_at:
60
+ self.created_at = now
61
+ if not self.updated_at:
62
+ self.updated_at = now
63
+
64
+ return super().create(db_session, actor=actor, no_commit=no_commit)
65
+
66
+ async def create_async(
67
+ self,
68
+ db_session: AsyncSession,
69
+ actor: Optional[User] = None,
70
+ no_commit: bool = False,
71
+ no_refresh: bool = False,
72
+ ) -> "RunMetrics":
73
+ """Override create_async to handle SQLite timestamp issues"""
74
+ # For SQLite, explicitly set timestamps as server_default may not work
75
+ if settings.database_engine == DatabaseChoice.SQLITE:
76
+ now = datetime.now(timezone.utc)
77
+ if not self.created_at:
78
+ self.created_at = now
79
+ if not self.updated_at:
80
+ self.updated_at = now
81
+
82
+ return await super().create_async(db_session, actor=actor, no_commit=no_commit, no_refresh=no_refresh)
letta/schemas/message.py CHANGED
@@ -965,7 +965,13 @@ class Message(BaseMessage):
965
965
  }
966
966
 
967
967
  elif self.role == "assistant" or self.role == "approval":
968
- assert self.tool_calls is not None or text_content is not None, vars(self)
968
+ try:
969
+ assert self.tool_calls is not None or text_content is not None, vars(self)
970
+ except AssertionError as e:
971
+ # relax check if this message only contains reasoning content
972
+ if self.content is not None and len(self.content) > 0 and isinstance(self.content[0], ReasoningContent):
973
+ return None
974
+ raise e
969
975
 
970
976
  # if native content, then put it directly inside the content
971
977
  if native_content:
@@ -1040,6 +1046,7 @@ class Message(BaseMessage):
1040
1046
  put_inner_thoughts_in_kwargs: bool = False,
1041
1047
  use_developer_message: bool = False,
1042
1048
  ) -> List[dict]:
1049
+ messages = Message.filter_messages_for_llm_api(messages)
1043
1050
  result = [
1044
1051
  m.to_openai_dict(
1045
1052
  max_tool_id_length=max_tool_id_length,
@@ -1149,6 +1156,7 @@ class Message(BaseMessage):
1149
1156
  messages: List[Message],
1150
1157
  max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
1151
1158
  ) -> List[dict]:
1159
+ messages = Message.filter_messages_for_llm_api(messages)
1152
1160
  result = []
1153
1161
  for message in messages:
1154
1162
  result.extend(message.to_openai_responses_dicts(max_tool_id_length=max_tool_id_length))
@@ -1156,6 +1164,7 @@ class Message(BaseMessage):
1156
1164
 
1157
1165
  def to_anthropic_dict(
1158
1166
  self,
1167
+ current_model: str,
1159
1168
  inner_thoughts_xml_tag="thinking",
1160
1169
  put_inner_thoughts_in_kwargs: bool = False,
1161
1170
  # if true, then treat the content field as AssistantMessage
@@ -1242,20 +1251,22 @@ class Message(BaseMessage):
1242
1251
  for content_part in self.content:
1243
1252
  # TextContent, ImageContent, ToolCallContent, ToolReturnContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent
1244
1253
  if isinstance(content_part, ReasoningContent):
1245
- content.append(
1246
- {
1247
- "type": "thinking",
1248
- "thinking": content_part.reasoning,
1249
- "signature": content_part.signature,
1250
- }
1251
- )
1254
+ if current_model == self.model:
1255
+ content.append(
1256
+ {
1257
+ "type": "thinking",
1258
+ "thinking": content_part.reasoning,
1259
+ "signature": content_part.signature,
1260
+ }
1261
+ )
1252
1262
  elif isinstance(content_part, RedactedReasoningContent):
1253
- content.append(
1254
- {
1255
- "type": "redacted_thinking",
1256
- "data": content_part.data,
1257
- }
1258
- )
1263
+ if current_model == self.model:
1264
+ content.append(
1265
+ {
1266
+ "type": "redacted_thinking",
1267
+ "data": content_part.data,
1268
+ }
1269
+ )
1259
1270
  elif isinstance(content_part, TextContent):
1260
1271
  content.append(
1261
1272
  {
@@ -1272,20 +1283,22 @@ class Message(BaseMessage):
1272
1283
  if self.content is not None and len(self.content) >= 1:
1273
1284
  for content_part in self.content:
1274
1285
  if isinstance(content_part, ReasoningContent):
1275
- content.append(
1276
- {
1277
- "type": "thinking",
1278
- "thinking": content_part.reasoning,
1279
- "signature": content_part.signature,
1280
- }
1281
- )
1286
+ if current_model == self.model:
1287
+ content.append(
1288
+ {
1289
+ "type": "thinking",
1290
+ "thinking": content_part.reasoning,
1291
+ "signature": content_part.signature,
1292
+ }
1293
+ )
1282
1294
  if isinstance(content_part, RedactedReasoningContent):
1283
- content.append(
1284
- {
1285
- "type": "redacted_thinking",
1286
- "data": content_part.data,
1287
- }
1288
- )
1295
+ if current_model == self.model:
1296
+ content.append(
1297
+ {
1298
+ "type": "redacted_thinking",
1299
+ "data": content_part.data,
1300
+ }
1301
+ )
1289
1302
  if isinstance(content_part, TextContent):
1290
1303
  content.append(
1291
1304
  {
@@ -1349,14 +1362,17 @@ class Message(BaseMessage):
1349
1362
  @staticmethod
1350
1363
  def to_anthropic_dicts_from_list(
1351
1364
  messages: List[Message],
1365
+ current_model: str,
1352
1366
  inner_thoughts_xml_tag: str = "thinking",
1353
1367
  put_inner_thoughts_in_kwargs: bool = False,
1354
1368
  # if true, then treat the content field as AssistantMessage
1355
1369
  native_content: bool = False,
1356
1370
  strip_request_heartbeat: bool = False,
1357
1371
  ) -> List[dict]:
1372
+ messages = Message.filter_messages_for_llm_api(messages)
1358
1373
  result = [
1359
1374
  m.to_anthropic_dict(
1375
+ current_model=current_model,
1360
1376
  inner_thoughts_xml_tag=inner_thoughts_xml_tag,
1361
1377
  put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
1362
1378
  native_content=native_content,
@@ -1369,6 +1385,7 @@ class Message(BaseMessage):
1369
1385
 
1370
1386
  def to_google_dict(
1371
1387
  self,
1388
+ current_model: str,
1372
1389
  put_inner_thoughts_in_kwargs: bool = True,
1373
1390
  # if true, then treat the content field as AssistantMessage
1374
1391
  native_content: bool = False,
@@ -1484,11 +1501,12 @@ class Message(BaseMessage):
1484
1501
  for content in self.content:
1485
1502
  if isinstance(content, TextContent):
1486
1503
  native_part = {"text": content.text}
1487
- if content.signature:
1504
+ if content.signature and current_model == self.model:
1488
1505
  native_part["thought_signature"] = content.signature
1489
1506
  native_google_content_parts.append(native_part)
1490
1507
  elif isinstance(content, ReasoningContent):
1491
- native_google_content_parts.append({"text": content.reasoning, "thought": True})
1508
+ if current_model == self.model:
1509
+ native_google_content_parts.append({"text": content.reasoning, "thought": True})
1492
1510
  elif isinstance(content, ToolCallContent):
1493
1511
  native_part = {
1494
1512
  "function_call": {
@@ -1496,7 +1514,7 @@ class Message(BaseMessage):
1496
1514
  "args": content.input,
1497
1515
  },
1498
1516
  }
1499
- if content.signature:
1517
+ if content.signature and current_model == self.model:
1500
1518
  native_part["thought_signature"] = content.signature
1501
1519
  native_google_content_parts.append(native_part)
1502
1520
  else:
@@ -1554,11 +1572,14 @@ class Message(BaseMessage):
1554
1572
  @staticmethod
1555
1573
  def to_google_dicts_from_list(
1556
1574
  messages: List[Message],
1575
+ current_model: str,
1557
1576
  put_inner_thoughts_in_kwargs: bool = True,
1558
1577
  native_content: bool = False,
1559
1578
  ):
1579
+ messages = Message.filter_messages_for_llm_api(messages)
1560
1580
  result = [
1561
1581
  m.to_google_dict(
1582
+ current_model=current_model,
1562
1583
  put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
1563
1584
  native_content=native_content,
1564
1585
  )
@@ -1567,6 +1588,45 @@ class Message(BaseMessage):
1567
1588
  result = [m for m in result if m is not None]
1568
1589
  return result
1569
1590
 
1591
+ def is_approval_request(self) -> bool:
1592
+ return self.role == "approval" and self.tool_calls is not None and len(self.tool_calls) > 0
1593
+
1594
+ def is_approval_response(self) -> bool:
1595
+ return self.role == "approval" and self.tool_calls is None and self.approve is not None
1596
+
1597
+ def is_summarization_message(self) -> bool:
1598
+ return (
1599
+ self.role == "user"
1600
+ and self.content is not None
1601
+ and len(self.content) == 1
1602
+ and isinstance(self.content[0], TextContent)
1603
+ and "system_alert" in self.content[0].text
1604
+ )
1605
+
1606
+ @staticmethod
1607
+ def filter_messages_for_llm_api(
1608
+ messages: List[Message],
1609
+ ) -> List[Message]:
1610
+ messages = [m for m in messages if m is not None]
1611
+ if len(messages) == 0:
1612
+ return []
1613
+ # Add special handling for legacy bug where summarization triggers in the middle of hitl
1614
+ messages_to_filter = []
1615
+ for i in range(len(messages) - 1):
1616
+ first_message_is_approval = messages[i].is_approval_request()
1617
+ second_message_is_summary = messages[i + 1].is_summarization_message()
1618
+ third_message_is_optional_approval = i + 2 >= len(messages) or messages[i + 2].is_approval_response()
1619
+ if first_message_is_approval and second_message_is_summary and third_message_is_optional_approval:
1620
+ messages_to_filter.append(messages[i])
1621
+ for idx in reversed(messages_to_filter): # reverse to avoid index shift
1622
+ messages.remove(idx)
1623
+
1624
+ # Filter last message if it is a lone approval request without a response - this only occurs for token counting
1625
+ if messages[-1].role == "approval" and messages[-1].tool_calls is not None and len(messages[-1].tool_calls) > 0:
1626
+ messages.remove(messages[-1])
1627
+
1628
+ return messages
1629
+
1570
1630
  @staticmethod
1571
1631
  def generate_otid_from_id(message_id: str, index: int) -> str:
1572
1632
  """
@@ -0,0 +1,21 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import Field
4
+
5
+ from letta.schemas.letta_base import LettaBase
6
+
7
+
8
+ class RunMetricsBase(LettaBase):
9
+ __id_prefix__ = "run"
10
+
11
+
12
+ class RunMetrics(RunMetricsBase):
13
+ id: str = Field(..., description="The id of the run this metric belongs to (matches runs.id).")
14
+ organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
15
+ agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
16
+ project_id: Optional[str] = Field(None, description="The project that the run belongs to (cloud only).")
17
+ run_start_ns: Optional[int] = Field(None, description="The timestamp of the start of the run in nanoseconds.")
18
+ run_ns: Optional[int] = Field(None, description="Total time for the run in nanoseconds.")
19
+ num_steps: Optional[int] = Field(None, description="The number of steps in the run.")
20
+ template_id: Optional[str] = Field(None, description="The template ID that the run belongs to (cloud only).")
21
+ base_template_id: Optional[str] = Field(None, description="The base template ID that the run belongs to (cloud only).")
letta/server/db.py CHANGED
@@ -10,18 +10,11 @@ from sqlalchemy.ext.asyncio import (
10
10
  create_async_engine,
11
11
  )
12
12
 
13
+ from letta.database_utils import get_database_uri_for_context
13
14
  from letta.settings import settings
14
15
 
15
- # Convert PostgreSQL URI to async format
16
- pg_uri = settings.letta_pg_uri
17
- if pg_uri.startswith("postgresql://"):
18
- async_pg_uri = pg_uri.replace("postgresql://", "postgresql+asyncpg://")
19
- else:
20
- # Handle other URI formats (e.g., postgresql+pg8000://)
21
- async_pg_uri = f"postgresql+asyncpg://{pg_uri.split('://', 1)[1]}" if "://" in pg_uri else pg_uri
22
-
23
- # Replace sslmode with ssl for asyncpg
24
- async_pg_uri = async_pg_uri.replace("sslmode=", "ssl=")
16
+ # Convert PostgreSQL URI to async format using common utility
17
+ async_pg_uri = get_database_uri_for_context(settings.letta_pg_uri, "async")
25
18
 
26
19
  # Build engine configuration based on settings
27
20
  engine_args = {
@@ -13,6 +13,7 @@ from letta.schemas.letta_request import RetrieveStreamRequest
13
13
  from letta.schemas.letta_stop_reason import StopReasonType
14
14
  from letta.schemas.openai.chat_completion_response import UsageStatistics
15
15
  from letta.schemas.run import Run
16
+ from letta.schemas.run_metrics import RunMetrics
16
17
  from letta.schemas.step import Step
17
18
  from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
18
19
  from letta.server.rest_api.redis_stream_manager import redis_sse_stream_generator
@@ -224,6 +225,23 @@ async def retrieve_run_usage(
224
225
  raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
225
226
 
226
227
 
228
+ @router.get("/{run_id}/metrics", response_model=RunMetrics, operation_id="retrieve_metrics_for_run")
229
+ async def retrieve_metrics_for_run(
230
+ run_id: str,
231
+ headers: HeaderParams = Depends(get_headers),
232
+ server: "SyncServer" = Depends(get_letta_server),
233
+ ):
234
+ """
235
+ Get run metrics by run ID.
236
+ """
237
+ try:
238
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
239
+ runs_manager = RunManager()
240
+ return await runs_manager.get_run_metrics_async(run_id=run_id, actor=actor)
241
+ except NoResultFound:
242
+ raise HTTPException(status_code=404, detail="Run metrics not found")
243
+
244
+
227
245
  @router.get(
228
246
  "/{run_id}/steps",
229
247
  response_model=List[Step],
@@ -247,18 +265,14 @@ async def list_run_steps(
247
265
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
248
266
  runs_manager = RunManager()
249
267
 
250
- try:
251
- steps = await runs_manager.get_run_steps(
252
- run_id=run_id,
253
- actor=actor,
254
- limit=limit,
255
- before=before,
256
- after=after,
257
- ascending=(order == "asc"),
258
- )
259
- return steps
260
- except NoResultFound as e:
261
- raise HTTPException(status_code=404, detail=str(e))
268
+ return await runs_manager.get_run_steps(
269
+ run_id=run_id,
270
+ actor=actor,
271
+ limit=limit,
272
+ before=before,
273
+ after=after,
274
+ ascending=(order == "asc"),
275
+ )
262
276
 
263
277
 
264
278
  @router.delete("/{run_id}", response_model=Run, operation_id="delete_run")
@@ -272,12 +286,7 @@ async def delete_run(
272
286
  """
273
287
  actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
274
288
  runs_manager = RunManager()
275
-
276
- try:
277
- run = await runs_manager.delete_run_by_id(run_id=run_id, actor=actor)
278
- return run
279
- except NoResultFound:
280
- raise HTTPException(status_code=404, detail="Run not found")
289
+ return await runs_manager.delete_run_by_id(run_id=run_id, actor=actor)
281
290
 
282
291
 
283
292
  @router.post(
@@ -74,7 +74,7 @@ class AnthropicTokenCounter(TokenCounter):
74
74
  return await self.client.count_tokens(model=self.model, tools=tools)
75
75
 
76
76
  def convert_messages(self, messages: List[Any]) -> List[Dict[str, Any]]:
77
- return Message.to_anthropic_dicts_from_list(messages)
77
+ return Message.to_anthropic_dicts_from_list(messages, current_model=self.model)
78
78
 
79
79
 
80
80
  class TiktokenCounter(TokenCounter):
@@ -2,14 +2,10 @@ from datetime import datetime
2
2
  from typing import Optional
3
3
 
4
4
  from sqlalchemy import asc, desc, nulls_last, select
5
- from letta.settings import DatabaseChoice, settings
6
5
 
7
6
  from letta.orm.run import Run as RunModel
8
- from letta.settings import DatabaseChoice, settings
9
- from sqlalchemy import asc, desc
10
- from typing import Optional
11
-
12
7
  from letta.services.helpers.agent_manager_helper import _cursor_filter
8
+ from letta.settings import DatabaseChoice, settings
13
9
 
14
10
 
15
11
  async def _apply_pagination_async(
@@ -29,17 +25,11 @@ async def _apply_pagination_async(
29
25
  sort_nulls_last = False
30
26
 
31
27
  if after:
32
- result = (
33
- await session.execute(
34
- select(sort_column, RunModel.id).where(RunModel.id == after)
35
- )
36
- ).first()
28
+ result = (await session.execute(select(sort_column, RunModel.id).where(RunModel.id == after))).first()
37
29
  if result:
38
30
  after_sort_value, after_id = result
39
31
  # SQLite does not support as granular timestamping, so we need to round the timestamp
40
- if settings.database_engine is DatabaseChoice.SQLITE and isinstance(
41
- after_sort_value, datetime
42
- ):
32
+ if settings.database_engine is DatabaseChoice.SQLITE and isinstance(after_sort_value, datetime):
43
33
  after_sort_value = after_sort_value.strftime("%Y-%m-%d %H:%M:%S")
44
34
  query = query.where(
45
35
  _cursor_filter(
@@ -53,17 +43,11 @@ async def _apply_pagination_async(
53
43
  )
54
44
 
55
45
  if before:
56
- result = (
57
- await session.execute(
58
- select(sort_column, RunModel.id).where(RunModel.id == before)
59
- )
60
- ).first()
46
+ result = (await session.execute(select(sort_column, RunModel.id).where(RunModel.id == before))).first()
61
47
  if result:
62
48
  before_sort_value, before_id = result
63
49
  # SQLite does not support as granular timestamping, so we need to round the timestamp
64
- if settings.database_engine is DatabaseChoice.SQLITE and isinstance(
65
- before_sort_value, datetime
66
- ):
50
+ if settings.database_engine is DatabaseChoice.SQLITE and isinstance(before_sort_value, datetime):
67
51
  before_sort_value = before_sort_value.strftime("%Y-%m-%d %H:%M:%S")
68
52
  query = query.where(
69
53
  _cursor_filter(
@@ -8,9 +8,11 @@ from sqlalchemy.orm import Session
8
8
 
9
9
  from letta.helpers.datetime_helpers import get_utc_time
10
10
  from letta.log import get_logger
11
+ from letta.orm.agent import Agent as AgentModel
11
12
  from letta.orm.errors import NoResultFound
12
13
  from letta.orm.message import Message as MessageModel
13
14
  from letta.orm.run import Run as RunModel
15
+ from letta.orm.run_metrics import RunMetrics as RunMetricsModel
14
16
  from letta.orm.sqlalchemy_base import AccessType
15
17
  from letta.orm.step import Step as StepModel
16
18
  from letta.otel.tracing import log_event, trace_method
@@ -21,6 +23,7 @@ from letta.schemas.letta_response import LettaResponse
21
23
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
22
24
  from letta.schemas.message import Message as PydanticMessage
23
25
  from letta.schemas.run import Run as PydanticRun, RunUpdate
26
+ from letta.schemas.run_metrics import RunMetrics as PydanticRunMetrics
24
27
  from letta.schemas.step import Step as PydanticStep
25
28
  from letta.schemas.usage import LettaUsageStatistics
26
29
  from letta.schemas.user import User as PydanticUser
@@ -62,6 +65,23 @@ class RunManager:
62
65
  run = RunModel(**run_data)
63
66
  run.organization_id = organization_id
64
67
  run = await run.create_async(session, actor=actor, no_commit=True, no_refresh=True)
68
+
69
+ # Create run metrics with start timestamp
70
+ import time
71
+
72
+ # Get the project_id from the agent
73
+ agent = await session.get(AgentModel, agent_id)
74
+ project_id = agent.project_id if agent else None
75
+
76
+ metrics = RunMetricsModel(
77
+ id=run.id,
78
+ organization_id=organization_id,
79
+ agent_id=agent_id,
80
+ project_id=project_id,
81
+ run_start_ns=int(time.time() * 1e9), # Current time in nanoseconds
82
+ num_steps=0, # Initialize to 0
83
+ )
84
+ await metrics.create_async(session)
65
85
  await session.commit()
66
86
 
67
87
  return run.to_pydantic()
@@ -178,6 +198,21 @@ class RunManager:
178
198
  await run.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
179
199
  final_metadata = run.metadata_
180
200
  pydantic_run = run.to_pydantic()
201
+
202
+ await session.commit()
203
+
204
+ # update run metrics table
205
+ num_steps = len(await self.step_manager.list_steps_async(run_id=run_id, actor=actor))
206
+ async with db_registry.async_session() as session:
207
+ metrics = await RunMetricsModel.read_async(db_session=session, identifier=run_id, actor=actor)
208
+ # Calculate runtime if run is completing
209
+ if is_terminal_update and metrics.run_start_ns:
210
+ import time
211
+
212
+ current_ns = int(time.time() * 1e9)
213
+ metrics.run_ns = current_ns - metrics.run_start_ns
214
+ metrics.num_steps = num_steps
215
+ await metrics.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
181
216
  await session.commit()
182
217
 
183
218
  # Dispatch callback outside of database session if needed
@@ -299,3 +334,31 @@ class RunManager:
299
334
  raise NoResultFound(f"Run with id {run_id} not found")
300
335
  pydantic_run = run.to_pydantic()
301
336
  return pydantic_run.request_config
337
+
338
+ @enforce_types
339
+ async def get_run_metrics_async(self, run_id: str, actor: PydanticUser) -> PydanticRunMetrics:
340
+ """Get metrics for a run."""
341
+ async with db_registry.async_session() as session:
342
+ metrics = await RunMetricsModel.read_async(db_session=session, identifier=run_id, actor=actor)
343
+ return metrics.to_pydantic()
344
+
345
+ @enforce_types
346
+ async def get_run_steps(
347
+ self,
348
+ run_id: str,
349
+ actor: PydanticUser,
350
+ limit: Optional[int] = 100,
351
+ before: Optional[str] = None,
352
+ after: Optional[str] = None,
353
+ ascending: bool = False,
354
+ ) -> List[PydanticStep]:
355
+ """Get steps for a run."""
356
+ async with db_registry.async_session() as session:
357
+ run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor, access_type=AccessType.ORGANIZATION)
358
+ if not run:
359
+ raise NoResultFound(f"Run with id {run_id} not found")
360
+
361
+ steps = await self.step_manager.list_steps_async(
362
+ actor=actor, run_id=run_id, limit=limit, before=before, after=after, order="asc" if ascending else "desc"
363
+ )
364
+ return steps
letta/system.py CHANGED
@@ -248,7 +248,11 @@ def unpack_message(packed_message: str) -> str:
248
248
  warnings.warn(f"Was unable to find 'message' field in packed message object: '{packed_message}'")
249
249
  return packed_message
250
250
  else:
251
- message_type = message_json["type"]
251
+ try:
252
+ message_type = message_json["type"]
253
+ except:
254
+ return packed_message
255
+
252
256
  if message_type != "user_message":
253
257
  warnings.warn(f"Expected type to be 'user_message', but was '{message_type}', so not unpacking: '{packed_message}'")
254
258
  return packed_message
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: letta-nightly
3
- Version: 0.11.7.dev20251008104128
3
+ Version: 0.12.0.dev20251009104148
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  Author-email: Letta Team <contact@letta.com>
6
6
  License: Apache License
@@ -1,7 +1,8 @@
1
- letta/__init__.py,sha256=ZwTfnKwttBN_OlSwb4a3-r8foHZiqx9VfpbsM3BT7xA,1620
1
+ letta/__init__.py,sha256=YF1Hcr7H49Jrjsp2dkyB-FWzee4pnuGLJnLBERmmsi0,1620
2
2
  letta/agent.py,sha256=xP13DDysFq-h3Ny7DiTN5ZC2txRripMNvPFt8dfFDbE,89498
3
3
  letta/config.py,sha256=JFGY4TWW0Wm5fTbZamOwWqk5G8Nn-TXyhgByGoAqy2c,12375
4
4
  letta/constants.py,sha256=Yxp2DGlVnesnevIQbXqfYadxvVuYYk_6-lsF7YmHixs,16255
5
+ letta/database_utils.py,sha256=D0lIbOkUQDBd2cJL1bmepyrsD5Rl29ZZLMkwnB7z8o0,4492
5
6
  letta/embeddings.py,sha256=d2o1nOVTaofBk6j-WwsE0_ugvxa1nIOcceqGuJ4w_pc,2045
6
7
  letta/errors.py,sha256=cpZQlm1VaGypu1SWj1-5bqfbW2kwKwdfcSculec5J5M,11534
7
8
  letta/interface.py,sha256=kYOTv1xhNWcoL9qP7cs8hGp9gKBimcBmqIhCUeoil18,13026
@@ -12,7 +13,7 @@ letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  letta/settings.py,sha256=CbSbYJ21q8MSrWJBie0YxXC2ijmPLNr7mojU4DPZuH4,15810
13
14
  letta/streaming_interface.py,sha256=rPMfwUcjqITWk2tVqFQm1hmP99tU2IOHg9gU2dgPSo8,16400
14
15
  letta/streaming_utils.py,sha256=ZRFGFpQqn9ujCEbgZdLM7yTjiuNNvqQ47sNhV8ix-yQ,16553
15
- letta/system.py,sha256=kHF7n3Viq7gV5UIUEXixod2gWa2jroUgztpEzMC1Sew,8925
16
+ letta/system.py,sha256=NClLhwB9Dw20L6IDPpfcnmxXqYQlA5E92xzJ0hGPLo0,8993
16
17
  letta/utils.py,sha256=c108IxvNVf0NeuQRrr2NK-asqjpzOI79YKSHYyhEqrQ,43541
17
18
  letta/adapters/letta_llm_adapter.py,sha256=Uo9l2lUI-n9mymWlK8lTResdWx0mc-F_ttmmKRJKzxo,3527
18
19
  letta/adapters/letta_llm_request_adapter.py,sha256=ApEaMOkvtH5l09NvVgm0r2qaqTnGSqwVqAFA7n5N1HA,4757
@@ -30,7 +31,7 @@ letta/agents/helpers.py,sha256=Hz8dKczPxMmDh-ILiUAEHgT55hMgIHO1yV8Vo0PORcw,16837
30
31
  letta/agents/letta_agent.py,sha256=4SSx6aORD28B1IogEoWeCTQr4jBaniX9jJolD4pssaQ,98156
31
32
  letta/agents/letta_agent_batch.py,sha256=NizNeIHvFtG4XpZCIplOSF8GohwHvNEkAUvsFBSdtSs,27950
32
33
  letta/agents/letta_agent_v2.py,sha256=MBoXVMIPixozb3xts2Bl8BCbExMIv3vtGbKZK1RitT4,60101
33
- letta/agents/letta_agent_v3.py,sha256=2yWG8cDkxrFJR57GeU3ReLGqim27L0OP0XcRtQVW-VE,46161
34
+ letta/agents/letta_agent_v3.py,sha256=i8idcEKVcZxJk67T_g0zLFBcrlHfygcuN9V_xk4Sj0c,48081
34
35
  letta/agents/voice_agent.py,sha256=DP7g7TPFhaIAxmpur9yyJVI8Sb_MnoWfpHm7Q-t7mJI,23177
35
36
  letta/agents/voice_sleeptime_agent.py,sha256=_JzCbWBOKrmo1cTaqZFTrQudpJEapwAyrXYtAHUILGo,8675
36
37
  letta/cli/cli.py,sha256=tKtghlX36Rp0_HbkMosvlAapL07JXhA0vKLGTNKnxSQ,1615
@@ -98,13 +99,13 @@ letta/jobs/llm_batch_job_polling.py,sha256=HUCTa1lTOiLAB_8m95RUfeNJa4lxlF8paGdCV
98
99
  letta/jobs/scheduler.py,sha256=Ub5VTCA8P5C9Y-0mPK2YIPJSEzKbSd2l5Sp0sOWctD8,8697
99
100
  letta/jobs/types.py,sha256=K8GKEnqEgAT6Kq4F2hUrBC4ZAFM9OkfOjVMStzxKuXQ,742
100
101
  letta/llm_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
- letta/llm_api/anthropic_client.py,sha256=YhLfiiRVVr3JoZtz8SnXOPuC62za4En2ThqmrXPM_zo,42160
102
+ letta/llm_api/anthropic_client.py,sha256=xZc2tvqz0_P3oNc0Wd2iWpK8qopQJkOSGnOFyo7v7hU,42204
102
103
  letta/llm_api/azure_client.py,sha256=BeChGsH4brrSgZBbCf8UE5RkW-3ZughpKnsBY2VYxwI,3841
103
104
  letta/llm_api/bedrock_client.py,sha256=xB01zdk1bzzf0ExkPcWcuxuLb1GXaJglvgRMYP2zai4,3572
104
105
  letta/llm_api/deepseek_client.py,sha256=YYja4-LWODE8fAJPSIVGxA7q0hb6-QmWRxWhlboTp64,17144
105
106
  letta/llm_api/google_ai_client.py,sha256=JweTUHZXvK6kcZBGXA7XEU53KP4vM7_zdD7AorCtsdI,8166
106
107
  letta/llm_api/google_constants.py,sha256=eOjOv-FImyJ4b4QGIaod-mEROMtrBFz0yhuYHqOEkwY,797
107
- letta/llm_api/google_vertex_client.py,sha256=Ws6qBlYIBdw009epkBiIRWVW47aNHAgAabwPDL6f1L0,39461
108
+ letta/llm_api/google_vertex_client.py,sha256=__j0OQDjzW1auyzvXKJoWLg3oPDLQgLL79A26K6GT84,39509
108
109
  letta/llm_api/groq_client.py,sha256=C34aWj2fv5A9AG_BKLdz-pXcvPQ6zf38f28EXkTtCjY,3361
109
110
  letta/llm_api/helpers.py,sha256=GXV_SuaU7uSCDj6bxDcCCF7CUjuZQCVWd5qZ3OsHVNk,17587
110
111
  letta/llm_api/llm_api_tools.py,sha256=EZLya93AbRblpnzpz5c1jEUI8n-zIbH7wbBH5LnLd6Q,12674
@@ -157,7 +158,7 @@ letta/local_llm/webui/legacy_settings.py,sha256=BLmd3TSx5StnY3ibjwaxYATPt_Lvq-o1
157
158
  letta/local_llm/webui/settings.py,sha256=gmLHfiOl1u4JmlAZU2d2O8YKF9lafdakyjwR_ftVPh8,552
158
159
  letta/openai_backcompat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
159
160
  letta/openai_backcompat/openai_object.py,sha256=GSzeCTwLpLD2fH4X8wVqzwdmoTjKK2I4PnriBY453lc,13505
160
- letta/orm/__init__.py,sha256=-P_frbPnMa19v8Zb8qvKycU3K72iIRrVzrAo2yPn2V0,1662
161
+ letta/orm/__init__.py,sha256=ZlnnsQu3PSzpHyGmXsBnHZCfRiiszUkND3G69Ma6fr0,1707
161
162
  letta/orm/agent.py,sha256=HS8hbS3uJoH8uezxKQ1vNr0YoXyTKBKhPsQdz4SrVK4,17747
162
163
  letta/orm/agents_tags.py,sha256=-rWR8DoEiHM4yc9vAHgHuvjIwgMXMWzKnTKFlBBu3TQ,1076
163
164
  letta/orm/archive.py,sha256=82engq9ZfNrp1yu8QS5df4vsIwQC33CADZA-05doNko,3920
@@ -191,6 +192,7 @@ letta/orm/prompt.py,sha256=NpFPTm3jD8Aewxhlnq8s4eIzANJ3bAEtbq6UmFqyc3U,489
191
192
  letta/orm/provider.py,sha256=dBvqXcQ7vMEkbsCQ82NezUB4RTCT3KiyZlkC3Z9ZTPg,1955
192
193
  letta/orm/provider_trace.py,sha256=CJMGz-rLqagJ-yXh9SJRbiGr5nAYdxY524hmiTgDFx4,1153
193
194
  letta/orm/run.py,sha256=eQgqfq-F9W9hHRbnfSVOhrW936J0xiTb5bvaVviTxvM,3820
195
+ letta/orm/run_metrics.py,sha256=WiWiJgx-eaQxn6ldUkv7aloUUKdB2l8lzTommoDOhCg,3094
194
196
  letta/orm/sandbox_config.py,sha256=FxFhKlbfGwE3d0quNbKXLkVcKSWh0HPmQVShdvktCoo,4497
195
197
  letta/orm/source.py,sha256=tRxqjWM8n1LBhlDTIiKhJ82-tpWErTeiDXMFz1rwY4g,1715
196
198
  letta/orm/sources_agents.py,sha256=Fvo8ujDNeeqleiiqXk-0VUPiQDawLAN-d28RxdvWnbs,554
@@ -270,7 +272,7 @@ letta/schemas/llm_config.py,sha256=dzEiIvm1l5xlYF0Q1It-18l9HKdwMd4Yr09A3gWhhNI,1
270
272
  letta/schemas/llm_config_overrides.py,sha256=E6qJuVA8TwAAy3VjGitJ5jSQo5PbN-6VPcZOF5qhP9A,1815
271
273
  letta/schemas/mcp.py,sha256=Wiu3FL5qupaHFaMqKFp-w1Ev6ShQ5dPfAtKIMGmRiF8,15527
272
274
  letta/schemas/memory.py,sha256=g2cPd0CF_3atzVkQA8ioIm52oZbsr6Ng-w31qGgNJ_g,20206
273
- letta/schemas/message.py,sha256=Ey2Kq3ipdPUwTIixYoWxpko9nF-22lhigqZ_3kinKFY,71874
275
+ letta/schemas/message.py,sha256=kXTfYnbFm7uzj2TIy-R3ixrSJGnR6xasdt1wgYJJOvw,74961
274
276
  letta/schemas/npm_requirement.py,sha256=HkvBF7KjHUH-MG-RAEYJHO2MLRS2rxFUcmbpbZVznLk,457
275
277
  letta/schemas/organization.py,sha256=TXrHN4IBQnX-mWvRuCOH57XZSLYCVOY0wWm2_UzDQIA,1279
276
278
  letta/schemas/passage.py,sha256=_bO19zOIQtQ3F3VqDSgIJqh15V0IIrJ_KdlbCt6-4D0,3940
@@ -280,6 +282,7 @@ letta/schemas/provider_trace.py,sha256=L-L5gEt9X1wMiI5w1fQ79qvJ1g1Kvo2TobeQC1p9a
280
282
  letta/schemas/providers.py,sha256=oHDHP3wUVPe3S2RfefUcsgE2DFCnkMFXx75FE7h0Pzw,69393
281
283
  letta/schemas/response_format.py,sha256=b2onyfSDCxnNkSHd4NsfJg_4ni5qBIK_F6zeJoMvjq0,2131
282
284
  letta/schemas/run.py,sha256=wjpDNsgme51WVr5EJ12Px6qhtOYbw-PMpJP32lche14,3630
285
+ letta/schemas/run_metrics.py,sha256=xbeObgpP-0guO3RRvEfEK5YNV6G7ol4whx3_ds88mIk,1147
283
286
  letta/schemas/sandbox_config.py,sha256=iw3-QS7PNy649tdynTJUxBbaruprykYAuGO6q28w-gU,5974
284
287
  letta/schemas/secret.py,sha256=1vq33Z-Oe2zSeVnGuj6j04YoO0Y9LxX1bmkprg6C1NA,15659
285
288
  letta/schemas/source.py,sha256=Uxsm8-XA3vuIt5Ihu_l2Aau7quuqmyIDg7qIryklUqY,3565
@@ -328,7 +331,7 @@ letta/serialize_schemas/marshmallow_tool.py,sha256=Cq4JQeLBrHdeoi8YhT84rXMXBCtiA
328
331
  letta/serialize_schemas/pydantic_agent_schema.py,sha256=CqGqSFzArYE2CzFsIU8LXVmH1A1jYFQpFy7Sj62n_4A,3171
329
332
  letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
330
333
  letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
331
- letta/server/db.py,sha256=lRQpacwE0sAjZgYqlui7BvCWYQYs3O58g2J67r92_Ks,3257
334
+ letta/server/db.py,sha256=bh294tLdOQxOrO9jICDrXa_UXTAYFByu_47tPXlKrjs,3006
332
335
  letta/server/generate_openapi_schema.sh,sha256=14Q6r0fbNNVVdf4X3Z-H0ZtyrVw5zYLzL5Doom3kM9k,351
333
336
  letta/server/server.py,sha256=nEvHxECQJ6I6EC2GQRNzHZpxg5HgFkhCXzqFwB-U9E4,78499
334
337
  letta/server/startup.sh,sha256=z-Fea-7LiuS_aG1tJqS8JAsDQaamwC_kuDhv9D3PPPY,2698
@@ -367,7 +370,7 @@ letta/server/rest_api/routers/v1/llms.py,sha256=sv5VWqB0-iSRi6LyzqsM1fLmOFm9UhM9
367
370
  letta/server/rest_api/routers/v1/messages.py,sha256=MgfHaTuPGyO1RjtjDuu4fC5dkmXEannUc9XGEK_aFLg,8559
368
371
  letta/server/rest_api/routers/v1/organizations.py,sha256=Un7qRo-69m9bC_TYyMnIRNLXf3fHHbNh1aVghnzzips,2932
369
372
  letta/server/rest_api/routers/v1/providers.py,sha256=_gKcCbEN2tW7c0BO7_cMFEYdhhsxDlKaopqXlZXMU1s,5996
370
- letta/server/rest_api/routers/v1/runs.py,sha256=EABftoK5CcA3aD9NmkLIiuuEAp2QLMt5kKo0_xjyzWw,14551
373
+ letta/server/rest_api/routers/v1/runs.py,sha256=qdCnp90Mdycu_itmPk78U_gYVbboBcadsCCAAdl4Oe8,14957
371
374
  letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=1x1QOOv-7jB9qf_AiYXH7ITyYMYicAgA4a5Qc8OHwB0,8702
372
375
  letta/server/rest_api/routers/v1/sources.py,sha256=8fkCbprC4PZlcf5HnBYj2-8PjWFIkL0TWZBlp95N7nE,22319
373
376
  letta/server/rest_api/routers/v1/steps.py,sha256=OIExfKSwilCmtrVHhF80h8g3yqhf5ww533FIw7N8noI,8251
@@ -404,7 +407,7 @@ letta/services/organization_manager.py,sha256=fHDTtZVCLOeH3_NPCRlDH-a3i2iPYAjSs-
404
407
  letta/services/passage_manager.py,sha256=TsdO9VN7oh4xV_oPvi9eyFOIrVA1fT7qGB8taYnf0b8,38419
405
408
  letta/services/per_agent_lock_manager.py,sha256=cMaW8r-qhucQbiK27jVqz8wzhlr2yuRNXbdkaMO4lnk,627
406
409
  letta/services/provider_manager.py,sha256=qSCoN2Qy5SQcsZZOThAE2rAfDjT7zr32omenGSHBYAs,7877
407
- letta/services/run_manager.py,sha256=tI1_CjN40gmJxUyCs26i0ToIZciaUTZ0Tj_g9Pz0ZLA,13557
410
+ letta/services/run_manager.py,sha256=fWTTEKG0tVEDcKUrR0sEj9ozgZ7ziPFV7LvZO2UdXgk,16292
408
411
  letta/services/sandbox_config_manager.py,sha256=dBDb6fuL7K-Ds6eL3TmcyCDQ7UKlF_nbJ2bixpmxm98,14956
409
412
  letta/services/source_manager.py,sha256=IteOcn9ydoO7KARoPh-JuuYwO4jWcsBoTsrkGWvDk9c,18864
410
413
  letta/services/step_manager.py,sha256=V9LlX5dOMuTVFkoncrFnzcgtn0sokhsOcUNSahiCZS0,22666
@@ -414,7 +417,7 @@ letta/services/tool_schema_generator.py,sha256=PA-hOhLi_w9VNbPpC3kt1YWybc2uQfkX5
414
417
  letta/services/user_manager.py,sha256=0LgTED-EPUCmQkXm0LPMBV6ONbOFtsXJnHxjoG5YjRU,6159
415
418
  letta/services/context_window_calculator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
416
419
  letta/services/context_window_calculator/context_window_calculator.py,sha256=Ux1UU_l0Nk08HGUMUUrjbtnm2thVRC9bHE7poo8gb8o,8241
417
- letta/services/context_window_calculator/token_counter.py,sha256=Am3ofVdi0_RiJz56b2uqc1Xz_1WglHkiyRwWrXvYrx8,4815
420
+ letta/services/context_window_calculator/token_counter.py,sha256=cdSoF8muJmBezE9gf8kL-LG6QOeP5mlaPAp2xr2Upbg,4841
418
421
  letta/services/file_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
419
422
  letta/services/file_processor/file_processor.py,sha256=VkQDtb79Xlj4bWLbGGG53ZPwaDf55w7t50WhlwNYvfI,15923
420
423
  letta/services/file_processor/file_types.py,sha256=kHe_EWK2k8y_5glVkCkkdZy1BLALyBvtlpZ9V-kiqHE,13070
@@ -432,7 +435,7 @@ letta/services/file_processor/parser/base_parser.py,sha256=WfnXP6fL-xQz4eIHEWa6-
432
435
  letta/services/file_processor/parser/markitdown_parser.py,sha256=BpCM82ocDKbNTKhb2Zu3ffYUXR5fqudiUiwxmmUePg4,3715
433
436
  letta/services/file_processor/parser/mistral_parser.py,sha256=NmCdVdpAB5f-VjILJp85pz2rSjlghKEg7qKTFzZLhP8,2384
434
437
  letta/services/helpers/agent_manager_helper.py,sha256=FGOcaFkuOnTadaHFQ2H3wi3w5MVPNMgT32E99mSrKkg,51126
435
- letta/services/helpers/run_manager_helper.py,sha256=q73DyDodrBmoC6tutfGOwpC6mqvTi46dFA2RcL-xuaA,2877
438
+ letta/services/helpers/run_manager_helper.py,sha256=3hzxJ2gA1QEVSP-IhNf6NyTxoXogI8bqPGgqTTNpyRk,2599
436
439
  letta/services/helpers/tool_execution_helper.py,sha256=45L7woJ98jK5MQAnhE_4NZdCeyOOzC4328FTQPM7iTA,9159
437
440
  letta/services/helpers/tool_parser_helper.py,sha256=gJ-XwvqIgVPlnPVbseHL0YPfTUtk6svqC43-U4VcM5k,4467
438
441
  letta/services/lettuce/__init__.py,sha256=UU0Jb58TV72EW204f8U2rru_Q9HlpaxqhECDF3t5N98,151
@@ -473,8 +476,8 @@ letta/templates/sandbox_code_file.py.j2,sha256=eXga5J_04Z8-pGdwfOCDjcRnMceIqcF5i
473
476
  letta/templates/sandbox_code_file_async.py.j2,sha256=lb7nh_P2W9VZHzU_9TxSCEMUod7SDziPXgvT75xVds0,2748
474
477
  letta/templates/summary_request_text.j2,sha256=ZttQwXonW2lk4pJLYzLK0pmo4EO4EtUUIXjgXKiizuc,842
475
478
  letta/types/__init__.py,sha256=hokKjCVFGEfR7SLMrtZsRsBfsC7yTIbgKPLdGg4K1eY,147
476
- letta_nightly-0.11.7.dev20251008104128.dist-info/METADATA,sha256=ojYZkA1rT1M3girk9unsdfPOx9uoGUr6ZzNKfGoJkcI,24468
477
- letta_nightly-0.11.7.dev20251008104128.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
478
- letta_nightly-0.11.7.dev20251008104128.dist-info/entry_points.txt,sha256=m-94Paj-kxiR6Ktu0us0_2qfhn29DzF2oVzqBE6cu8w,41
479
- letta_nightly-0.11.7.dev20251008104128.dist-info/licenses/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
480
- letta_nightly-0.11.7.dev20251008104128.dist-info/RECORD,,
479
+ letta_nightly-0.12.0.dev20251009104148.dist-info/METADATA,sha256=-xts5F15qFLK5pjWrucujMu7g3rWG_qDh3uG9DQuzjE,24468
480
+ letta_nightly-0.12.0.dev20251009104148.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
481
+ letta_nightly-0.12.0.dev20251009104148.dist-info/entry_points.txt,sha256=m-94Paj-kxiR6Ktu0us0_2qfhn29DzF2oVzqBE6cu8w,41
482
+ letta_nightly-0.12.0.dev20251009104148.dist-info/licenses/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
483
+ letta_nightly-0.12.0.dev20251009104148.dist-info/RECORD,,