letta-nightly 0.13.0.dev20251031104146__py3-none-any.whl → 0.13.1.dev20251031234110__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (101) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/simple_llm_stream_adapter.py +1 -0
  3. letta/agents/letta_agent_v2.py +8 -0
  4. letta/agents/letta_agent_v3.py +120 -27
  5. letta/agents/temporal/activities/__init__.py +25 -0
  6. letta/agents/temporal/activities/create_messages.py +26 -0
  7. letta/agents/temporal/activities/create_step.py +57 -0
  8. letta/agents/temporal/activities/example_activity.py +9 -0
  9. letta/agents/temporal/activities/execute_tool.py +130 -0
  10. letta/agents/temporal/activities/llm_request.py +114 -0
  11. letta/agents/temporal/activities/prepare_messages.py +27 -0
  12. letta/agents/temporal/activities/refresh_context.py +160 -0
  13. letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
  14. letta/agents/temporal/activities/update_message_ids.py +25 -0
  15. letta/agents/temporal/activities/update_run.py +43 -0
  16. letta/agents/temporal/constants.py +59 -0
  17. letta/agents/temporal/temporal_agent_workflow.py +704 -0
  18. letta/agents/temporal/types.py +275 -0
  19. letta/constants.py +8 -0
  20. letta/errors.py +4 -0
  21. letta/functions/function_sets/base.py +0 -11
  22. letta/groups/helpers.py +7 -1
  23. letta/groups/sleeptime_multi_agent_v4.py +4 -3
  24. letta/interfaces/anthropic_streaming_interface.py +0 -1
  25. letta/interfaces/openai_streaming_interface.py +103 -100
  26. letta/llm_api/anthropic_client.py +57 -12
  27. letta/llm_api/bedrock_client.py +1 -0
  28. letta/llm_api/deepseek_client.py +3 -2
  29. letta/llm_api/google_vertex_client.py +1 -0
  30. letta/llm_api/groq_client.py +1 -0
  31. letta/llm_api/llm_client_base.py +15 -1
  32. letta/llm_api/openai.py +2 -2
  33. letta/llm_api/openai_client.py +17 -3
  34. letta/llm_api/xai_client.py +1 -0
  35. letta/orm/organization.py +4 -0
  36. letta/orm/sqlalchemy_base.py +7 -0
  37. letta/otel/tracing.py +131 -4
  38. letta/schemas/agent_file.py +10 -10
  39. letta/schemas/block.py +22 -3
  40. letta/schemas/enums.py +21 -0
  41. letta/schemas/environment_variables.py +3 -2
  42. letta/schemas/group.py +3 -3
  43. letta/schemas/letta_response.py +36 -4
  44. letta/schemas/llm_batch_job.py +3 -3
  45. letta/schemas/llm_config.py +27 -3
  46. letta/schemas/mcp.py +3 -2
  47. letta/schemas/mcp_server.py +3 -2
  48. letta/schemas/message.py +167 -49
  49. letta/schemas/organization.py +2 -1
  50. letta/schemas/passage.py +2 -1
  51. letta/schemas/provider_trace.py +2 -1
  52. letta/schemas/providers/openrouter.py +1 -2
  53. letta/schemas/run_metrics.py +2 -1
  54. letta/schemas/sandbox_config.py +3 -1
  55. letta/schemas/step_metrics.py +2 -1
  56. letta/schemas/tool_rule.py +2 -2
  57. letta/schemas/user.py +2 -1
  58. letta/server/rest_api/app.py +5 -1
  59. letta/server/rest_api/routers/v1/__init__.py +4 -0
  60. letta/server/rest_api/routers/v1/agents.py +71 -9
  61. letta/server/rest_api/routers/v1/blocks.py +7 -7
  62. letta/server/rest_api/routers/v1/groups.py +40 -0
  63. letta/server/rest_api/routers/v1/identities.py +2 -2
  64. letta/server/rest_api/routers/v1/internal_agents.py +31 -0
  65. letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
  66. letta/server/rest_api/routers/v1/internal_runs.py +25 -1
  67. letta/server/rest_api/routers/v1/runs.py +2 -22
  68. letta/server/rest_api/routers/v1/tools.py +10 -0
  69. letta/server/server.py +5 -2
  70. letta/services/agent_manager.py +4 -4
  71. letta/services/archive_manager.py +16 -0
  72. letta/services/group_manager.py +44 -0
  73. letta/services/helpers/run_manager_helper.py +2 -2
  74. letta/services/lettuce/lettuce_client.py +148 -0
  75. letta/services/mcp/base_client.py +9 -3
  76. letta/services/run_manager.py +148 -37
  77. letta/services/source_manager.py +91 -3
  78. letta/services/step_manager.py +2 -3
  79. letta/services/streaming_service.py +52 -13
  80. letta/services/summarizer/summarizer.py +28 -2
  81. letta/services/tool_executor/builtin_tool_executor.py +1 -1
  82. letta/services/tool_executor/core_tool_executor.py +2 -117
  83. letta/services/tool_schema_generator.py +2 -2
  84. letta/validators.py +21 -0
  85. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/METADATA +1 -1
  86. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/RECORD +89 -84
  87. letta/agent.py +0 -1758
  88. letta/cli/cli_load.py +0 -16
  89. letta/client/__init__.py +0 -0
  90. letta/client/streaming.py +0 -95
  91. letta/client/utils.py +0 -78
  92. letta/functions/async_composio_toolset.py +0 -109
  93. letta/functions/composio_helpers.py +0 -96
  94. letta/helpers/composio_helpers.py +0 -38
  95. letta/orm/job_messages.py +0 -33
  96. letta/schemas/providers.py +0 -1617
  97. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
  98. letta/services/tool_executor/composio_tool_executor.py +0 -57
  99. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/WHEEL +0 -0
  100. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/entry_points.txt +0 -0
  101. {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import json
2
3
  import logging
3
4
  import re
@@ -19,6 +20,7 @@ from letta.errors import (
19
20
  LLMConnectionError,
20
21
  LLMNotFoundError,
21
22
  LLMPermissionDeniedError,
23
+ LLMProviderOverloaded,
22
24
  LLMRateLimitError,
23
25
  LLMServerError,
24
26
  LLMTimeoutError,
@@ -229,6 +231,7 @@ class AnthropicClient(LLMClientBase):
229
231
  tools: Optional[List[dict]] = None,
230
232
  force_tool_call: Optional[str] = None,
231
233
  requires_subsequent_tool_call: bool = False,
234
+ tool_return_truncation_chars: Optional[int] = None,
232
235
  ) -> dict:
233
236
  # TODO: This needs to get cleaned up. The logic here is pretty confusing.
234
237
  # TODO: I really want to get rid of prefixing, it's a recipe for disaster code maintenance wise
@@ -334,6 +337,7 @@ class AnthropicClient(LLMClientBase):
334
337
  # if react, use native content + strip heartbeats
335
338
  native_content=is_v1,
336
339
  strip_request_heartbeat=is_v1,
340
+ tool_return_truncation_chars=tool_return_truncation_chars,
337
341
  )
338
342
 
339
343
  # Ensure first message is user
@@ -383,25 +387,53 @@ class AnthropicClient(LLMClientBase):
383
387
  else:
384
388
  anthropic_tools = None
385
389
 
386
- # Detect presence of reasoning blocks anywhere in the final assistant message.
387
- # Interleaved thinking is not guaranteed to be the first content part.
390
+ # Convert final thinking blocks to text to work around token counting endpoint limitation.
391
+ # The token counting endpoint rejects messages where the final content block is thinking,
392
+ # even though the main API supports this with the interleaved-thinking beta.
393
+ # We convert (not strip) to preserve accurate token counts.
394
+ # TODO: Remove this workaround if Anthropic fixes the token counting endpoint.
388
395
  thinking_enabled = False
396
+ messages_for_counting = messages
397
+
389
398
  if messages and len(messages) > 0:
390
- last_assistant_message = next((m for m in reversed(messages) if m.get("role") == "assistant"), None)
391
- if last_assistant_message:
392
- content = last_assistant_message.get("content")
393
- if isinstance(content, list):
394
- for part in content:
395
- if isinstance(part, dict) and part.get("type") in {"thinking", "redacted_thinking"}:
399
+ messages_for_counting = copy.deepcopy(messages)
400
+
401
+ # Scan all assistant messages and convert any final thinking blocks to text
402
+ for message in messages_for_counting:
403
+ if message.get("role") == "assistant":
404
+ content = message.get("content")
405
+
406
+ # Check for thinking in any format
407
+ if isinstance(content, list) and len(content) > 0:
408
+ # Check if message has any thinking blocks (to enable thinking mode)
409
+ has_thinking = any(
410
+ isinstance(part, dict) and part.get("type") in {"thinking", "redacted_thinking"} for part in content
411
+ )
412
+ if has_thinking:
396
413
  thinking_enabled = True
397
- break
398
- elif isinstance(content, str) and "<thinking>" in content:
399
- thinking_enabled = True
414
+
415
+ # If final block is thinking, handle it
416
+ last_block = content[-1]
417
+ if isinstance(last_block, dict) and last_block.get("type") in {"thinking", "redacted_thinking"}:
418
+ if len(content) == 1:
419
+ # Thinking-only message: add text at end (don't convert the thinking)
420
+ # API requires first block to be thinking when thinking is enabled
421
+ content.append({"type": "text", "text": "."})
422
+ else:
423
+ # Multiple blocks: convert final thinking to text
424
+ if last_block["type"] == "thinking":
425
+ content[-1] = {"type": "text", "text": last_block.get("thinking", "")}
426
+ elif last_block["type"] == "redacted_thinking":
427
+ content[-1] = {"type": "text", "text": last_block.get("data", "[redacted]")}
428
+
429
+ elif isinstance(content, str) and "<thinking>" in content:
430
+ # Handle XML-style thinking in string content
431
+ thinking_enabled = True
400
432
 
401
433
  try:
402
434
  count_params = {
403
435
  "model": model or "claude-3-7-sonnet-20250219",
404
- "messages": messages or [{"role": "user", "content": "hi"}],
436
+ "messages": messages_for_counting or [{"role": "user", "content": "hi"}],
405
437
  "tools": anthropic_tools or [],
406
438
  }
407
439
 
@@ -444,6 +476,14 @@ class AnthropicClient(LLMClientBase):
444
476
 
445
477
  @trace_method
446
478
  def handle_llm_error(self, e: Exception) -> Exception:
479
+ # make sure to check for overflow errors, regardless of error type
480
+ error_str = str(e).lower()
481
+ if "prompt is too long" in error_str or "exceed context limit" in error_str or "exceeds context" in error_str:
482
+ logger.warning(f"[Anthropic] Context window exceeded: {str(e)}")
483
+ return ContextWindowExceededError(
484
+ message=f"Context window exceeded for Anthropic: {str(e)}",
485
+ )
486
+
447
487
  if isinstance(e, anthropic.APITimeoutError):
448
488
  logger.warning(f"[Anthropic] Request timeout: {e}")
449
489
  return LLMTimeoutError(
@@ -513,6 +553,11 @@ class AnthropicClient(LLMClientBase):
513
553
 
514
554
  if isinstance(e, anthropic.APIStatusError):
515
555
  logger.warning(f"[Anthropic] API status error: {str(e)}")
556
+ if "overloaded" in str(e).lower():
557
+ return LLMProviderOverloaded(
558
+ message=f"Anthropic API is overloaded: {str(e)}",
559
+ code=ErrorCode.INTERNAL_SERVER_ERROR,
560
+ )
516
561
  return LLMServerError(
517
562
  message=f"Anthropic API error: {str(e)}",
518
563
  code=ErrorCode.INTERNAL_SERVER_ERROR,
@@ -71,6 +71,7 @@ class BedrockClient(AnthropicClient):
71
71
  tools: Optional[List[dict]] = None,
72
72
  force_tool_call: Optional[str] = None,
73
73
  requires_subsequent_tool_call: bool = False,
74
+ tool_return_truncation_chars: Optional[int] = None,
74
75
  ) -> dict:
75
76
  data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
76
77
  # remove disallowed fields
@@ -59,7 +59,7 @@ def handle_assistant_message(assistant_message: AssistantMessage) -> AssistantMe
59
59
  return assistant_message
60
60
 
61
61
 
62
- def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List[_Message]:
62
+ def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List["_Message"]:
63
63
  """
64
64
  Deepeek API has the following constraints: messages must be interleaved between user and assistant messages, ending on a user message.
65
65
  Tools are currently unstable for V3 and not supported for R1 in the API: https://api-docs.deepseek.com/guides/function_calling.
@@ -103,7 +103,7 @@ def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List[_Messag
103
103
 
104
104
  def build_deepseek_chat_completions_request(
105
105
  llm_config: LLMConfig,
106
- messages: List[_Message],
106
+ messages: List["_Message"],
107
107
  user_id: Optional[str],
108
108
  functions: Optional[list],
109
109
  function_call: Optional[str],
@@ -340,6 +340,7 @@ class DeepseekClient(OpenAIClient):
340
340
  tools: Optional[List[dict]] = None,
341
341
  force_tool_call: Optional[str] = None,
342
342
  requires_subsequent_tool_call: bool = False,
343
+ tool_return_truncation_chars: Optional[int] = None,
343
344
  ) -> dict:
344
345
  # Override put_inner_thoughts_in_kwargs to False for DeepSeek
345
346
  llm_config.put_inner_thoughts_in_kwargs = False
@@ -291,6 +291,7 @@ class GoogleVertexClient(LLMClientBase):
291
291
  tools: List[dict],
292
292
  force_tool_call: Optional[str] = None,
293
293
  requires_subsequent_tool_call: bool = False,
294
+ tool_return_truncation_chars: Optional[int] = None,
294
295
  ) -> dict:
295
296
  """
296
297
  Constructs a request object in the expected data format for this client.
@@ -30,6 +30,7 @@ class GroqClient(OpenAIClient):
30
30
  tools: Optional[List[dict]] = None,
31
31
  force_tool_call: Optional[str] = None,
32
32
  requires_subsequent_tool_call: bool = False,
33
+ tool_return_truncation_chars: Optional[int] = None,
33
34
  ) -> dict:
34
35
  data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
35
36
 
@@ -47,13 +47,22 @@ class LLMClientBase:
47
47
  force_tool_call: Optional[str] = None,
48
48
  telemetry_manager: Optional["TelemetryManager"] = None,
49
49
  step_id: Optional[str] = None,
50
+ tool_return_truncation_chars: Optional[int] = None,
50
51
  ) -> Union[ChatCompletionResponse, Stream[ChatCompletionChunk]]:
51
52
  """
52
53
  Issues a request to the downstream model endpoint and parses response.
53
54
  If stream=True, returns a Stream[ChatCompletionChunk] that can be iterated over.
54
55
  Otherwise returns a ChatCompletionResponse.
55
56
  """
56
- request_data = self.build_request_data(agent_type, messages, llm_config, tools, force_tool_call)
57
+ request_data = self.build_request_data(
58
+ agent_type,
59
+ messages,
60
+ llm_config,
61
+ tools,
62
+ force_tool_call,
63
+ requires_subsequent_tool_call=False,
64
+ tool_return_truncation_chars=tool_return_truncation_chars,
65
+ )
57
66
 
58
67
  try:
59
68
  log_event(name="llm_request_sent", attributes=request_data)
@@ -128,9 +137,14 @@ class LLMClientBase:
128
137
  tools: List[dict],
129
138
  force_tool_call: Optional[str] = None,
130
139
  requires_subsequent_tool_call: bool = False,
140
+ tool_return_truncation_chars: Optional[int] = None,
131
141
  ) -> dict:
132
142
  """
133
143
  Constructs a request object in the expected data format for this client.
144
+
145
+ Args:
146
+ tool_return_truncation_chars: If set, truncates tool return content to this many characters.
147
+ Used during summarization to avoid context window issues.
134
148
  """
135
149
  raise NotImplementedError
136
150
 
letta/llm_api/openai.py CHANGED
@@ -624,8 +624,8 @@ def prepare_openai_payload(chat_completion_request: ChatCompletionRequest):
624
624
  data = chat_completion_request.model_dump(exclude_none=True)
625
625
 
626
626
  # add check otherwise will cause error: "Invalid value for 'parallel_tool_calls': 'parallel_tool_calls' is only allowed when 'tools' are specified."
627
- if chat_completion_request.tools is not None:
628
- data["parallel_tool_calls"] = False
627
+ if chat_completion_request.tools is not None and chat_completion_request.parallel_tool_calls is not None:
628
+ data["parallel_tool_calls"] = chat_completion_request.parallel_tool_calls
629
629
 
630
630
  # If functions == None, strip from the payload
631
631
  if "functions" in data and data["functions"] is None:
@@ -64,6 +64,14 @@ def is_openai_reasoning_model(model: str) -> bool:
64
64
  return is_reasoning
65
65
 
66
66
 
67
+ def does_not_support_minimal_reasoning(model: str) -> bool:
68
+ """Check if the model does not support minimal reasoning effort.
69
+
70
+ Currently, models that contain codex don't support minimal reasoning.
71
+ """
72
+ return "codex" in model.lower()
73
+
74
+
67
75
  def is_openai_5_model(model: str) -> bool:
68
76
  """Utility function to check if the model is a '5' model"""
69
77
  return model.startswith("gpt-5")
@@ -221,6 +229,7 @@ class OpenAIClient(LLMClientBase):
221
229
  tools: Optional[List[dict]] = None, # Keep as dict for now as per base class
222
230
  force_tool_call: Optional[str] = None,
223
231
  requires_subsequent_tool_call: bool = False,
232
+ tool_return_truncation_chars: Optional[int] = None,
224
233
  ) -> dict:
225
234
  """
226
235
  Constructs a request object in the expected data format for the OpenAI Responses API.
@@ -228,7 +237,9 @@ class OpenAIClient(LLMClientBase):
228
237
  if llm_config.put_inner_thoughts_in_kwargs:
229
238
  raise ValueError("Inner thoughts in kwargs are not supported for the OpenAI Responses API")
230
239
 
231
- openai_messages_list = PydanticMessage.to_openai_responses_dicts_from_list(messages)
240
+ openai_messages_list = PydanticMessage.to_openai_responses_dicts_from_list(
241
+ messages, tool_return_truncation_chars=tool_return_truncation_chars
242
+ )
232
243
  # Add multi-modal support for Responses API by rewriting user messages
233
244
  # into input_text/input_image parts.
234
245
  openai_messages_list = fill_image_content_in_responses_input(openai_messages_list, messages)
@@ -316,7 +327,7 @@ class OpenAIClient(LLMClientBase):
316
327
  tool_choice=tool_choice,
317
328
  max_output_tokens=llm_config.max_tokens,
318
329
  temperature=llm_config.temperature if supports_temperature_param(model) else None,
319
- parallel_tool_calls=False,
330
+ parallel_tool_calls=llm_config.parallel_tool_calls if tools and supports_parallel_tool_calling(model) else False,
320
331
  )
321
332
 
322
333
  # Add verbosity control for GPT-5 models
@@ -341,7 +352,7 @@ class OpenAIClient(LLMClientBase):
341
352
 
342
353
  # Add parallel tool calling
343
354
  if tools and supports_parallel_tool_calling(model):
344
- data.parallel_tool_calls = False
355
+ data.parallel_tool_calls = llm_config.parallel_tool_calls
345
356
 
346
357
  # always set user id for openai requests
347
358
  if self.actor:
@@ -369,6 +380,7 @@ class OpenAIClient(LLMClientBase):
369
380
  tools: Optional[List[dict]] = None, # Keep as dict for now as per base class
370
381
  force_tool_call: Optional[str] = None,
371
382
  requires_subsequent_tool_call: bool = False,
383
+ tool_return_truncation_chars: Optional[int] = None,
372
384
  ) -> dict:
373
385
  """
374
386
  Constructs a request object in the expected data format for the OpenAI API.
@@ -382,6 +394,7 @@ class OpenAIClient(LLMClientBase):
382
394
  tools=tools,
383
395
  force_tool_call=force_tool_call,
384
396
  requires_subsequent_tool_call=requires_subsequent_tool_call,
397
+ tool_return_truncation_chars=tool_return_truncation_chars,
385
398
  )
386
399
 
387
400
  if agent_type == AgentType.letta_v1_agent:
@@ -411,6 +424,7 @@ class OpenAIClient(LLMClientBase):
411
424
  messages,
412
425
  put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
413
426
  use_developer_message=use_developer_message,
427
+ tool_return_truncation_chars=tool_return_truncation_chars,
414
428
  )
415
429
  ]
416
430
 
@@ -30,6 +30,7 @@ class XAIClient(OpenAIClient):
30
30
  tools: Optional[List[dict]] = None,
31
31
  force_tool_call: Optional[str] = None,
32
32
  requires_subsequent_tool_call: bool = False,
33
+ tool_return_truncation_chars: Optional[int] = None,
33
34
  ) -> dict:
34
35
  data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
35
36
 
letta/orm/organization.py CHANGED
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
19
19
  from letta.orm.passage import ArchivalPassage, SourcePassage
20
20
  from letta.orm.passage_tag import PassageTag
21
21
  from letta.orm.provider import Provider
22
+ from letta.orm.provider_trace import ProviderTrace
22
23
  from letta.orm.run import Run
23
24
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
24
25
  from letta.orm.tool import Tool
@@ -70,3 +71,6 @@ class Organization(SqlalchemyBase):
70
71
  )
71
72
  jobs: Mapped[List["Job"]] = relationship("Job", back_populates="organization", cascade="all, delete-orphan")
72
73
  runs: Mapped[List["Run"]] = relationship("Run", back_populates="organization", cascade="all, delete-orphan")
74
+ provider_traces: Mapped[List["ProviderTrace"]] = relationship(
75
+ "ProviderTrace", back_populates="organization", cascade="all, delete-orphan"
76
+ )
@@ -9,6 +9,7 @@ from sqlalchemy import Sequence, String, and_, delete, func, or_, select
9
9
  from sqlalchemy.exc import DBAPIError, IntegrityError, TimeoutError
10
10
  from sqlalchemy.ext.asyncio import AsyncSession
11
11
  from sqlalchemy.orm import Mapped, Session, mapped_column
12
+ from sqlalchemy.orm.exc import StaleDataError
12
13
  from sqlalchemy.orm.interfaces import ORMOption
13
14
 
14
15
  from letta.log import get_logger
@@ -625,6 +626,12 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
625
626
  if not no_refresh:
626
627
  await db_session.refresh(self)
627
628
  return self
629
+ except StaleDataError as e:
630
+ # This can occur when using optimistic locking (version_id_col) and:
631
+ # 1. The row doesn't exist (0 rows matched)
632
+ # 2. The version has changed (concurrent update)
633
+ # We convert this to NoResultFound to return a proper 404 error
634
+ raise NoResultFound(f"{self.__class__.__name__} with id '{self.id}' not found or was updated by another transaction") from e
628
635
  except (DBAPIError, IntegrityError) as e:
629
636
  self._handle_dbapi_error(e)
630
637
 
letta/otel/tracing.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import inspect
3
+ import itertools
3
4
  import re
4
5
  import time
5
6
  import traceback
@@ -227,11 +228,137 @@ def trace_method(func):
227
228
  if args and hasattr(args[0], "__class__"):
228
229
  param_items = param_items[1:]
229
230
 
231
+ # Parameters to skip entirely (known to be large)
232
+ SKIP_PARAMS = {
233
+ "agent_state",
234
+ "messages",
235
+ "in_context_messages",
236
+ "message_sequence",
237
+ "content",
238
+ "tool_returns",
239
+ "memory",
240
+ "sources",
241
+ "context",
242
+ "resource_id",
243
+ "source_code",
244
+ "request_data",
245
+ "system",
246
+ }
247
+
248
+ # Max size for parameter value strings (1KB)
249
+ MAX_PARAM_SIZE = 1024
250
+ # Max total size for all parameters (100KB)
251
+ MAX_TOTAL_SIZE = 1024 * 100
252
+ total_size = 0
253
+
230
254
  for name, value in param_items:
231
- # Convert value to string to avoid serialization issues
232
- span.set_attribute(f"parameter.{name}", str(value))
233
- except:
234
- pass
255
+ try:
256
+ # Check if we've exceeded total size limit
257
+ if total_size > MAX_TOTAL_SIZE:
258
+ span.set_attribute("parameters.truncated", True)
259
+ span.set_attribute("parameters.truncated_reason", f"Total size exceeded {MAX_TOTAL_SIZE} bytes")
260
+ break
261
+
262
+ # Skip parameters known to be large
263
+ if name in SKIP_PARAMS:
264
+ # Try to extract ID for observability
265
+ type_name = type(value).__name__
266
+ id_info = ""
267
+
268
+ try:
269
+ # Handle lists/iterables (e.g., messages)
270
+ if hasattr(value, "__iter__") and not isinstance(value, (str, bytes, dict)):
271
+ ids = []
272
+ count = 0
273
+ # Use itertools.islice to avoid converting entire iterable
274
+ for item in itertools.islice(value, 5):
275
+ count += 1
276
+ if hasattr(item, "id"):
277
+ ids.append(str(item.id))
278
+
279
+ # Try to get total count if it's a sized iterable
280
+ total_count = None
281
+ if hasattr(value, "__len__"):
282
+ try:
283
+ total_count = len(value)
284
+ except (TypeError, AttributeError):
285
+ pass
286
+
287
+ if ids:
288
+ suffix = ""
289
+ if total_count is not None and total_count > 5:
290
+ suffix = f"... ({total_count} total)"
291
+ elif count == 5:
292
+ suffix = "..."
293
+ id_info = f", ids=[{','.join(ids)}{suffix}]"
294
+ # Handle single objects with id attribute
295
+ elif hasattr(value, "id"):
296
+ id_info = f", id={value.id}"
297
+ except (TypeError, AttributeError, ValueError):
298
+ pass
299
+
300
+ param_value = f"<{type_name} (excluded{id_info})>"
301
+ span.set_attribute(f"parameter.{name}", param_value)
302
+ total_size += len(param_value)
303
+ continue
304
+
305
+ # Try repr first with length limit, fallback to str if needed
306
+ str_value = None
307
+
308
+ # For simple types, use str directly
309
+ if isinstance(value, (str, int, float, bool, type(None))):
310
+ str_value = str(value)
311
+ else:
312
+ # For complex objects, try to get a truncated representation
313
+ try:
314
+ # Test if str() works (some objects have broken __str__)
315
+ try:
316
+ test_str = str(value)
317
+ # If str() works and is reasonable, use repr
318
+ str_value = repr(value)
319
+ except Exception:
320
+ # If str() fails, mark as serialization failed
321
+ raise ValueError("str() failed")
322
+
323
+ # If repr is already too long, try to be smarter
324
+ if len(str_value) > MAX_PARAM_SIZE * 2:
325
+ # For collections, show just the type and size
326
+ if hasattr(value, "__len__"):
327
+ try:
328
+ str_value = f"<{type(value).__name__} with {len(value)} items>"
329
+ except (TypeError, AttributeError):
330
+ str_value = f"<{type(value).__name__}>"
331
+ else:
332
+ str_value = f"<{type(value).__name__}>"
333
+ except (RecursionError, MemoryError, ValueError):
334
+ # Handle cases where repr or str causes issues
335
+ str_value = f"<serialization failed: {type(value).__name__}>"
336
+ except Exception as e:
337
+ # Fallback for any other issues
338
+ str_value = f"<serialization failed: {type(e).__name__}>"
339
+
340
+ # Apply size limit
341
+ original_size = len(str_value)
342
+ if original_size > MAX_PARAM_SIZE:
343
+ str_value = str_value[:MAX_PARAM_SIZE] + f"... (truncated, original size: {original_size} chars)"
344
+
345
+ span.set_attribute(f"parameter.{name}", str_value)
346
+ total_size += len(str_value)
347
+
348
+ except (TypeError, ValueError, AttributeError, RecursionError, MemoryError) as e:
349
+ try:
350
+ error_msg = f"<serialization failed: {type(e).__name__}>"
351
+ span.set_attribute(f"parameter.{name}", error_msg)
352
+ total_size += len(error_msg)
353
+ except Exception:
354
+ # If even the fallback fails, skip this parameter
355
+ pass
356
+
357
+ except (TypeError, ValueError, AttributeError) as e:
358
+ logger.debug(f"Failed to add parameters to span: {type(e).__name__}: {e}")
359
+ except Exception as e:
360
+ # Catch-all for any other unexpected exceptions
361
+ logger.debug(f"Unexpected error adding parameters to span: {type(e).__name__}: {e}")
235
362
 
236
363
  @wraps(func)
237
364
  async def async_wrapper(*args, **kwargs):
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
7
7
  from letta.helpers.datetime_helpers import get_utc_time
8
8
  from letta.schemas.agent import AgentState, CreateAgent
9
9
  from letta.schemas.block import Block, CreateBlock
10
- from letta.schemas.enums import MessageRole
10
+ from letta.schemas.enums import MessageRole, PrimitiveType
11
11
  from letta.schemas.file import FileAgent, FileAgentBase, FileMetadata, FileMetadataBase
12
12
  from letta.schemas.group import Group, GroupCreate
13
13
  from letta.schemas.letta_message import ApprovalReturn
@@ -42,7 +42,7 @@ class ImportResult:
42
42
  class MessageSchema(MessageCreate):
43
43
  """Message with human-readable ID for agent file"""
44
44
 
45
- __id_prefix__ = "message"
45
+ __id_prefix__ = PrimitiveType.MESSAGE.value
46
46
  id: str = Field(..., description="Human-readable identifier for this message in the file")
47
47
 
48
48
  # Override the role field to accept all message roles, not just user/system/assistant
@@ -96,7 +96,7 @@ class MessageSchema(MessageCreate):
96
96
  class FileAgentSchema(FileAgentBase):
97
97
  """File-Agent relationship with human-readable ID for agent file"""
98
98
 
99
- __id_prefix__ = "file_agent"
99
+ __id_prefix__ = PrimitiveType.FILE_AGENT.value
100
100
  id: str = Field(..., description="Human-readable identifier for this file-agent relationship in the file")
101
101
 
102
102
  @classmethod
@@ -120,7 +120,7 @@ class FileAgentSchema(FileAgentBase):
120
120
  class AgentSchema(CreateAgent):
121
121
  """Agent with human-readable ID for agent file"""
122
122
 
123
- __id_prefix__ = "agent"
123
+ __id_prefix__ = PrimitiveType.AGENT.value
124
124
  id: str = Field(..., description="Human-readable identifier for this agent in the file")
125
125
  in_context_message_ids: List[str] = Field(
126
126
  default_factory=list, description="List of message IDs that are currently in the agent's context"
@@ -198,7 +198,7 @@ class AgentSchema(CreateAgent):
198
198
  class GroupSchema(GroupCreate):
199
199
  """Group with human-readable ID for agent file"""
200
200
 
201
- __id_prefix__ = "group"
201
+ __id_prefix__ = PrimitiveType.GROUP.value
202
202
  id: str = Field(..., description="Human-readable identifier for this group in the file")
203
203
 
204
204
  @classmethod
@@ -220,7 +220,7 @@ class GroupSchema(GroupCreate):
220
220
  class BlockSchema(CreateBlock):
221
221
  """Block with human-readable ID for agent file"""
222
222
 
223
- __id_prefix__ = "block"
223
+ __id_prefix__ = PrimitiveType.BLOCK.value
224
224
  id: str = Field(..., description="Human-readable identifier for this block in the file")
225
225
 
226
226
  @classmethod
@@ -246,7 +246,7 @@ class BlockSchema(CreateBlock):
246
246
  class FileSchema(FileMetadataBase):
247
247
  """File with human-readable ID for agent file"""
248
248
 
249
- __id_prefix__ = "file"
249
+ __id_prefix__ = PrimitiveType.FILE.value
250
250
  id: str = Field(..., description="Human-readable identifier for this file in the file")
251
251
 
252
252
  @classmethod
@@ -276,7 +276,7 @@ class FileSchema(FileMetadataBase):
276
276
  class SourceSchema(SourceCreate):
277
277
  """Source with human-readable ID for agent file"""
278
278
 
279
- __id_prefix__ = "source"
279
+ __id_prefix__ = PrimitiveType.SOURCE.value
280
280
  id: str = Field(..., description="Human-readable identifier for this source in the file")
281
281
 
282
282
  @classmethod
@@ -299,7 +299,7 @@ class SourceSchema(SourceCreate):
299
299
  class ToolSchema(Tool):
300
300
  """Tool with human-readable ID for agent file"""
301
301
 
302
- __id_prefix__ = "tool"
302
+ __id_prefix__ = PrimitiveType.TOOL.value
303
303
  id: str = Field(..., description="Human-readable identifier for this tool in the file")
304
304
 
305
305
  @classmethod
@@ -311,7 +311,7 @@ class ToolSchema(Tool):
311
311
  class MCPServerSchema(BaseModel):
312
312
  """MCP server schema for agent files with remapped ID."""
313
313
 
314
- __id_prefix__ = "mcp_server"
314
+ __id_prefix__ = PrimitiveType.MCP_SERVER.value
315
315
 
316
316
  id: str = Field(..., description="Human-readable MCP server ID")
317
317
  server_type: str
letta/schemas/block.py CHANGED
@@ -21,9 +21,9 @@ class BaseBlock(LettaBase, validate_assignment=True):
21
21
 
22
22
  project_id: Optional[str] = Field(None, description="The associated project id.")
23
23
  # template data (optional)
24
- template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
24
+ template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
25
25
  is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
26
- template_id: Optional[str] = Field(None, description="The id of the template.", alias="name")
26
+ template_id: Optional[str] = Field(None, description="The id of the template.")
27
27
  base_template_id: Optional[str] = Field(None, description="The base template id of the block.")
28
28
  deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
29
29
  entity_id: Optional[str] = Field(None, description="The id of the entity within the template.")
@@ -102,6 +102,25 @@ class Block(BaseBlock):
102
102
  last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.")
103
103
 
104
104
 
105
+ class BlockResponse(Block):
106
+ id: str = Field(
107
+ ...,
108
+ description="The id of the block.",
109
+ )
110
+ template_name: Optional[str] = Field(
111
+ None, description="(Deprecated) The name of the block template (if it is a template).", deprecated=True
112
+ )
113
+ template_id: Optional[str] = Field(None, description="(Deprecated) The id of the template.", deprecated=True)
114
+ base_template_id: Optional[str] = Field(None, description="(Deprecated) The base template id of the block.", deprecated=True)
115
+ deployment_id: Optional[str] = Field(None, description="(Deprecated) The id of the deployment.", deprecated=True)
116
+ entity_id: Optional[str] = Field(None, description="(Deprecated) The id of the entity within the template.", deprecated=True)
117
+ preserve_on_migration: Optional[bool] = Field(
118
+ False, description="(Deprecated) Preserve the block on template migration.", deprecated=True
119
+ )
120
+ read_only: bool = Field(False, description="(Deprecated) Whether the agent has read-only access to the block.", deprecated=True)
121
+ hidden: Optional[bool] = Field(None, description="(Deprecated) If set to True, the block will be hidden.", deprecated=True)
122
+
123
+
105
124
  class FileBlock(Block):
106
125
  file_id: str = Field(..., description="Unique identifier of the file.")
107
126
  source_id: str = Field(..., description="Unique identifier of the source.")
@@ -149,7 +168,7 @@ class CreateBlock(BaseBlock):
149
168
  project_id: Optional[str] = Field(None, description="The associated project id.")
150
169
  # block templates
151
170
  is_template: bool = False
152
- template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
171
+ template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
153
172
 
154
173
  @model_validator(mode="before")
155
174
  @classmethod
letta/schemas/enums.py CHANGED
@@ -26,6 +26,27 @@ class PrimitiveType(str, Enum):
26
26
  STEP = "step"
27
27
  IDENTITY = "identity"
28
28
 
29
+ # Infrastructure types
30
+ MCP_SERVER = "mcp_server"
31
+ MCP_OAUTH = "mcp-oauth"
32
+ FILE_AGENT = "file_agent"
33
+
34
+ # Configuration types
35
+ SANDBOX_ENV = "sandbox-env"
36
+ AGENT_ENV = "agent-env"
37
+
38
+ # Core entity types
39
+ USER = "user"
40
+ ORGANIZATION = "org"
41
+ TOOL_RULE = "tool_rule"
42
+
43
+ # Batch processing types
44
+ BATCH_ITEM = "batch_item"
45
+ BATCH_REQUEST = "batch_req"
46
+
47
+ # Telemetry types
48
+ PROVIDER_TRACE = "provider_trace"
49
+
29
50
 
30
51
  class ProviderType(str, Enum):
31
52
  anthropic = "anthropic"