letta-nightly 0.13.0.dev20251030104218__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.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/METADATA +1 -1
  86. {letta_nightly-0.13.0.dev20251030104218.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.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/WHEEL +0 -0
  100. {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/entry_points.txt +0 -0
  101. {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,275 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List, Optional
3
+
4
+ from letta.helpers import ToolRulesSolver
5
+ from letta.schemas.agent import AgentState
6
+ from letta.schemas.enums import RunStatus
7
+ from letta.schemas.letta_message import LettaMessageUnion, MessageType
8
+ from letta.schemas.letta_message_content import (
9
+ OmittedReasoningContent,
10
+ ReasoningContent,
11
+ RedactedReasoningContent,
12
+ TextContent,
13
+ )
14
+ from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
15
+ from letta.schemas.message import Message, MessageCreate
16
+ from letta.schemas.openai.chat_completion_response import ToolCall, UsageStatistics
17
+ from letta.schemas.step import Step
18
+ from letta.schemas.tool_execution_result import ToolExecutionResult
19
+ from letta.schemas.usage import LettaUsageStatistics
20
+ from letta.schemas.user import User
21
+
22
+
23
+ @dataclass
24
+ class WorkflowInputParams:
25
+ agent_state: AgentState
26
+ messages: list[MessageCreate]
27
+ actor: User
28
+ max_steps: int
29
+ run_id: str
30
+ use_assistant_message: bool = True
31
+ include_return_message_types: list[MessageType] | None = None
32
+
33
+
34
+ @dataclass
35
+ class PreparedMessages:
36
+ in_context_messages: List[Message]
37
+ input_messages_to_persist: List[Message]
38
+
39
+
40
+ @dataclass
41
+ class FinalResult:
42
+ messages: List[LettaMessageUnion]
43
+ stop_reason: str
44
+ usage: LettaUsageStatistics
45
+
46
+
47
+ @dataclass
48
+ class InnerStepResult:
49
+ """Result from a single inner_step execution."""
50
+
51
+ stop_reason: StopReasonType
52
+ usage: LettaUsageStatistics
53
+ should_continue: bool
54
+ response_messages: List[Message]
55
+ agent_state: AgentState
56
+
57
+
58
+ # ===== Additional types for activities up to _handle_ai_response =====
59
+
60
+
61
+ @dataclass
62
+ class RefreshContextParams:
63
+ """Input to refresh_context_and_system_message activity.
64
+
65
+ - agent_state: Current agent state (memory, sources, tools, etc.)
66
+ - in_context_messages: Current message history to potentially rebuild/scrub
67
+ - actor: Requesting user (for DB access control)
68
+ """
69
+
70
+ agent_state: AgentState
71
+ in_context_messages: List[Message]
72
+ tool_rules_solver: ToolRulesSolver
73
+ actor: User
74
+
75
+
76
+ @dataclass
77
+ class RefreshContextResult:
78
+ """Output from refresh_context_and_system_message activity.
79
+
80
+ - messages: Updated in-context messages with refreshed system message
81
+ - agent_state: Updated agent state with refreshed memory
82
+ """
83
+
84
+ messages: List[Message]
85
+ agent_state: AgentState
86
+
87
+
88
+ @dataclass
89
+ class LLMRequestParams:
90
+ """Input to llm_request activity.
91
+
92
+ - agent_state: Needed primarily for llm_config (model, endpoint, etc.)
93
+ - messages: Full prompt messages (context + input + responses if any)
94
+ - allowed_tools: Tools JSON schema list after rules + strict mode + runtime overrides
95
+ - force_tool_call: Optional tool name to force call when only one valid tool exists
96
+ - requires_approval_tools: Optional list of tool names that require approval
97
+ - actor: Requesting user (for audit/tenant context)
98
+ - step_id: Current step id for tracing/telemetry correlation
99
+ - use_assistant_message: Whether to use assistant message format for responses
100
+ """
101
+
102
+ agent_state: AgentState
103
+ messages: List[Message]
104
+ allowed_tools: List[dict]
105
+ force_tool_call: Optional[str] = None
106
+ requires_approval_tools: Optional[List[str]] = None
107
+ actor: Optional[User] = None
108
+ step_id: Optional[str] = None
109
+ use_assistant_message: bool = True
110
+
111
+
112
+ @dataclass
113
+ class LLMCallResult:
114
+ """Output from llm_request activity.
115
+
116
+ - tool_call: Parsed tool call from LLM (None for stub/approval paths)
117
+ - reasoning_content: Optional reasoning/assistant content stream collected
118
+ - assistant_message_id: Optional precomputed assistant message id (if adapter sets)
119
+ - usage: Provider usage statistics for this call
120
+ - request_finish_ns: Provider request finish time (ns) for metrics, if available
121
+ - llm_request_start_ns: Start time of LLM request in nanoseconds
122
+ - llm_request_ns: Duration of LLM request in nanoseconds
123
+ """
124
+
125
+ tool_call: Optional[ToolCall]
126
+ reasoning_content: Optional[List[TextContent | ReasoningContent | RedactedReasoningContent | OmittedReasoningContent]]
127
+ assistant_message_id: Optional[str]
128
+ usage: UsageStatistics
129
+ request_finish_ns: Optional[int]
130
+ llm_request_start_ns: Optional[int] = None
131
+ llm_request_ns: Optional[int] = None
132
+
133
+
134
+ @dataclass
135
+ class SummarizeParams:
136
+ """Input to summarize_conversation_history activity.
137
+
138
+ - agent_state: Current agent state (summarizer config, ids)
139
+ - in_context_messages: Current context window
140
+ - new_letta_messages: Newly generated/persisted messages to consider
141
+ - actor: Requesting user
142
+ - force: Whether to force summarization + clear
143
+ """
144
+
145
+ agent_state: AgentState
146
+ in_context_messages: List[Message]
147
+ new_letta_messages: List[Message]
148
+ actor: User
149
+ force: bool = True
150
+
151
+
152
+ # ===== Tool execution and message handling types =====
153
+
154
+
155
+ @dataclass
156
+ class ExecuteToolParams:
157
+ """Input to execute_tool_activity.
158
+
159
+ - tool_name: Name of the tool to execute
160
+ - tool_args: Arguments to pass to the tool
161
+ - agent_state: Current agent state containing tools and configuration
162
+ - actor: Requesting user for access control
163
+ - step_id: Current step ID for tracing
164
+ """
165
+
166
+ tool_name: str
167
+ tool_args: Dict
168
+ agent_state: AgentState
169
+ actor: User
170
+ step_id: Optional[str]
171
+
172
+
173
+ @dataclass
174
+ class ExecuteToolResult:
175
+ """Output from execute_tool_activity."""
176
+
177
+ tool_execution_result: ToolExecutionResult
178
+ execution_time_ns: int
179
+
180
+
181
+ @dataclass
182
+ class CreateStepParams:
183
+ """Input to create_step_activity."""
184
+
185
+ agent_state: AgentState
186
+ messages: List[Message]
187
+ actor: User
188
+ run_id: str
189
+ step_id: Optional[str]
190
+ usage: UsageStatistics
191
+ step_ns: Optional[int] = None
192
+ llm_request_ns: Optional[int] = None
193
+ tool_execution_ns: Optional[int] = None
194
+ stop_reason: Optional[str] = None
195
+
196
+
197
+ @dataclass
198
+ class CreateStepResult:
199
+ """Output from create_step_activity."""
200
+
201
+ step: Step
202
+
203
+
204
+ @dataclass
205
+ class CreateMessagesParams:
206
+ """Input to create_messages_activity.
207
+
208
+ Persists messages to the database.
209
+ """
210
+
211
+ messages: List[Message]
212
+ actor: User
213
+ project_id: Optional[str]
214
+ template_id: Optional[str]
215
+
216
+
217
+ @dataclass
218
+ class CreateMessagesResult:
219
+ """Output from create_messages_activity."""
220
+
221
+ messages: List[Message]
222
+
223
+
224
+ @dataclass
225
+ class PersistMessagesParams:
226
+ """Input to persist_messages_activity.
227
+
228
+ Persists messages to database and optionally updates job messages.
229
+ """
230
+
231
+ messages: List[Message]
232
+ actor: User
233
+ project_id: Optional[str]
234
+ template_id: Optional[str]
235
+ run_id: Optional[str]
236
+
237
+
238
+ @dataclass
239
+ class PersistMessagesResult:
240
+ """Output from persist_messages_activity."""
241
+
242
+
243
+ @dataclass
244
+ class UpdateRunParams:
245
+ """Input to update_run_activity."""
246
+
247
+ run_id: str
248
+ actor: User
249
+ run_status: RunStatus
250
+ stop_reason: LettaStopReason | None
251
+ persisted_messages: List[Message]
252
+ usage: LettaUsageStatistics
253
+ total_duration_ns: int | None
254
+
255
+
256
+ @dataclass
257
+ class UpdateMessageIdsParams:
258
+ """Input to update_message_ids_activity.
259
+
260
+ Updates the agent's message IDs in the database.
261
+ Used for immediate approval persistence to prevent bad state.
262
+ """
263
+
264
+ agent_id: str
265
+ message_ids: List[str]
266
+ actor: User
267
+
268
+
269
+ @dataclass
270
+ class UpdateMessageIdsResult:
271
+ """Output from update_message_ids_activity."""
272
+
273
+ success: bool
274
+ agent_state: AgentState
275
+ persisted_messages: List[Message]
letta/constants.py CHANGED
@@ -51,6 +51,11 @@ DEFAULT_MAX_STEPS = 50
51
51
  MIN_CONTEXT_WINDOW = 4096
52
52
  DEFAULT_CONTEXT_WINDOW = 32000
53
53
 
54
+ # Summarization trigger threshold (multiplier of context_window limit)
55
+ # Summarization triggers when step usage > context_window * SUMMARIZATION_TRIGGER_MULTIPLIER
56
+ # Set to 0.9 (90%) to provide buffer before hitting hard limit
57
+ SUMMARIZATION_TRIGGER_MULTIPLIER = 0.9
58
+
54
59
  # number of concurrent embedding requests to sent
55
60
  EMBEDDING_BATCH_SIZE = 200
56
61
 
@@ -373,6 +378,9 @@ FUNCTION_RETURN_CHAR_LIMIT = 50000 # ~300 words
373
378
  BASE_FUNCTION_RETURN_CHAR_LIMIT = 50000 # same as regular function limit
374
379
  FILE_IS_TRUNCATED_WARNING = "# NOTE: This block is truncated, use functions to view the full content."
375
380
 
381
+ # Tool return truncation limit for LLM context window management
382
+ TOOL_RETURN_TRUNCATION_CHARS = 5000
383
+
376
384
  MAX_PAUSE_HEARTBEATS = 360 # in min
377
385
 
378
386
  MESSAGE_CHATGPT_FUNCTION_MODEL = "gpt-3.5-turbo"
letta/errors.py CHANGED
@@ -202,6 +202,10 @@ class LLMTimeoutError(LLMError):
202
202
  """Error when LLM request times out"""
203
203
 
204
204
 
205
+ class LLMProviderOverloaded(LLMError):
206
+ """Error when LLM provider is overloaded"""
207
+
208
+
205
209
  class BedrockPermissionError(LettaError):
206
210
  """Exception raised for errors in the Bedrock permission process."""
207
211
 
@@ -24,7 +24,6 @@ def memory(
24
24
 
25
25
  Args:
26
26
  command (str): The sub-command to execute. Supported commands:
27
- - "view": List memory blocks or view specific block content
28
27
  - "create": Create a new memory block
29
28
  - "str_replace": Replace text in a memory block
30
29
  - "insert": Insert text at a specific line in a memory block
@@ -39,21 +38,11 @@ def memory(
39
38
  insert_text (Optional[str]): Text to insert (for insert)
40
39
  old_path (Optional[str]): Old path for rename operation
41
40
  new_path (Optional[str]): New path for rename operation
42
- view_range (Optional[int]): Range of lines to view (for view)
43
41
 
44
42
  Returns:
45
43
  Optional[str]: Success message or error description
46
44
 
47
45
  Examples:
48
- # List all memory blocks
49
- memory(agent_state, "view", path="/memories")
50
-
51
- # View specific memory block content
52
- memory(agent_state, "view", path="/memories/user_preferences")
53
-
54
- # View first 10 lines of a memory block
55
- memory(agent_state, "view", path="/memories/user_preferences", view_range=10)
56
-
57
46
  # Replace text in a memory block
58
47
  memory(agent_state, "str_replace", path="/memories/user_preferences", old_str="theme: dark", new_str="theme: light")
59
48
 
letta/groups/helpers.py CHANGED
@@ -6,7 +6,7 @@ from letta.orm.group import Group
6
6
  from letta.orm.user import User
7
7
  from letta.schemas.agent import AgentState
8
8
  from letta.schemas.group import ManagerType
9
- from letta.schemas.letta_message_content import ImageContent, TextContent
9
+ from letta.schemas.letta_message_content import ImageContent, ReasoningContent, TextContent
10
10
  from letta.schemas.message import Message
11
11
  from letta.services.mcp.base_client import AsyncBaseMCPClient
12
12
 
@@ -100,6 +100,12 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str
100
100
  return f"{message.name or 'user'}: {message.content[0].text}"
101
101
  elif message.role == "assistant":
102
102
  messages = []
103
+ if message.content:
104
+ for content in message.content:
105
+ if isinstance(content, TextContent):
106
+ messages.append(f"{assistant_name}: {content.text}")
107
+ elif isinstance(content, ReasoningContent):
108
+ messages.append(f"{assistant_name}: *thinking* {content.reasoning}")
103
109
  if message.tool_calls:
104
110
  if message.tool_calls[0].function.name == "send_message":
105
111
  messages.append(f"{assistant_name}: {json.loads(message.tool_calls[0].function.arguments)['message']}")
@@ -59,8 +59,8 @@ class SleeptimeMultiAgentV4(LettaAgentV3):
59
59
  request_start_timestamp_ns=request_start_timestamp_ns,
60
60
  )
61
61
 
62
- await self.run_sleeptime_agents()
63
- response.usage.run_ids = self.run_ids
62
+ run_ids = await self.run_sleeptime_agents()
63
+ response.usage.run_ids = run_ids
64
64
  return response
65
65
 
66
66
  @trace_method
@@ -94,7 +94,7 @@ class SleeptimeMultiAgentV4(LettaAgentV3):
94
94
  await self.run_sleeptime_agents()
95
95
 
96
96
  @trace_method
97
- async def run_sleeptime_agents(self):
97
+ async def run_sleeptime_agents(self) -> list[str]:
98
98
  # Get response messages
99
99
  last_response_messages = self.response_messages
100
100
 
@@ -122,6 +122,7 @@ class SleeptimeMultiAgentV4(LettaAgentV3):
122
122
  # Individual task failures
123
123
  print(f"Sleeptime agent processing failed: {e!s}")
124
124
  raise e
125
+ return self.run_ids
125
126
 
126
127
  @trace_method
127
128
  async def _issue_background_task(
@@ -23,7 +23,6 @@ from anthropic.types.beta import (
23
23
  BetaThinkingDelta,
24
24
  BetaToolUseBlock,
25
25
  )
26
- from letta_client.types import assistant_message
27
26
 
28
27
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
29
28
  from letta.local_llm.constants import INNER_THOUGHTS_KWARG
@@ -6,6 +6,7 @@ from typing import Optional
6
6
  from openai import AsyncStream
7
7
  from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
8
8
  from openai.types.responses import (
9
+ ParsedResponse,
9
10
  ResponseCompletedEvent,
10
11
  ResponseContentPartAddedEvent,
11
12
  ResponseContentPartDoneEvent,
@@ -524,10 +525,9 @@ class SimpleOpenAIStreamingInterface:
524
525
  self.messages = messages or []
525
526
  self.tools = tools or []
526
527
 
527
- # Buffers to hold accumulating tools
528
- self.tool_call_name = ""
529
- self.tool_call_args = ""
530
- self.tool_call_id = ""
528
+ # Accumulate per-index tool call fragments and preserve order
529
+ self._tool_calls_acc: dict[int, dict[str, str]] = {}
530
+ self._tool_call_start_order: list[int] = []
531
531
 
532
532
  self.content_messages = []
533
533
  self.emitted_hidden_reasoning = False # Track if we've emitted hidden reasoning message
@@ -561,19 +561,27 @@ class SimpleOpenAIStreamingInterface:
561
561
 
562
562
  return merged_messages
563
563
 
564
- def get_tool_call_object(self) -> ToolCall:
565
- """Useful for agent loop"""
566
- if not self.tool_call_name:
567
- raise ValueError("No tool call name available")
568
- if not self.tool_call_args:
569
- raise ValueError("No tool call arguments available")
570
- if not self.tool_call_id:
571
- raise ValueError("No tool call ID available")
564
+ def get_tool_call_objects(self) -> list[ToolCall]:
565
+ """Return finalized tool calls (parallel supported)."""
566
+ if not self._tool_calls_acc:
567
+ return []
568
+ ordered_indices = [i for i in self._tool_call_start_order if i in self._tool_calls_acc]
569
+ result: list[ToolCall] = []
570
+ for idx in ordered_indices:
571
+ ctx = self._tool_calls_acc[idx]
572
+ name = ctx.get("name", "")
573
+ args = ctx.get("arguments", "")
574
+ call_id = ctx.get("id", "")
575
+ if call_id and name:
576
+ result.append(ToolCall(id=call_id, function=FunctionCall(arguments=args or "", name=name)))
577
+ return result
572
578
 
573
- return ToolCall(
574
- id=self.tool_call_id,
575
- function=FunctionCall(arguments=self.tool_call_args, name=self.tool_call_name),
576
- )
579
+ def get_tool_call_object(self) -> ToolCall:
580
+ """Backwards-compatible single tool call accessor (first tool if multiple)."""
581
+ calls = self.get_tool_call_objects()
582
+ if not calls:
583
+ raise ValueError("No tool calls available")
584
+ return calls[0]
577
585
 
578
586
  async def process(
579
587
  self,
@@ -718,70 +726,61 @@ class SimpleOpenAIStreamingInterface:
718
726
  yield reasoning_msg
719
727
 
720
728
  if message_delta.tool_calls is not None and len(message_delta.tool_calls) > 0:
721
- tool_call = message_delta.tool_calls[0]
722
-
723
- # For OpenAI reasoning models, emit a hidden reasoning message before the first tool call
724
- # if not self.emitted_hidden_reasoning and is_openai_reasoning_model(self.model):
725
- # self.emitted_hidden_reasoning = True
726
- # if prev_message_type and prev_message_type != "hidden_reasoning_message":
727
- # message_index += 1
728
- # hidden_message = HiddenReasoningMessage(
729
- # id=self.letta_message_id,
730
- # date=datetime.now(timezone.utc),
731
- # state="omitted",
732
- # hidden_reasoning=None,
733
- # otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
734
- # )
735
- # self.content_messages.append(hidden_message)
736
- # prev_message_type = hidden_message.message_type
737
- # message_index += 1 # Increment for the next message
738
- # yield hidden_message
739
-
740
- if not tool_call.function.name and not tool_call.function.arguments and not tool_call.id:
741
- # No chunks to process, exit
742
- return
743
-
744
- if tool_call.function.name:
745
- self.tool_call_name += tool_call.function.name
746
- if tool_call.function.arguments:
747
- self.tool_call_args += tool_call.function.arguments
748
- if tool_call.id:
749
- self.tool_call_id += tool_call.id
729
+ # Accumulate per-index tool call fragments and emit deltas
730
+ for tool_call in message_delta.tool_calls:
731
+ if (
732
+ not (tool_call.function and (tool_call.function.name or tool_call.function.arguments))
733
+ and not tool_call.id
734
+ and getattr(tool_call, "index", None) is None
735
+ ):
736
+ continue
750
737
 
751
- if self.requires_approval_tools:
752
- tool_call_msg = ApprovalRequestMessage(
753
- id=decrement_message_uuid(self.letta_message_id),
754
- date=datetime.now(timezone.utc),
755
- tool_call=ToolCallDelta(
756
- name=tool_call.function.name,
757
- arguments=tool_call.function.arguments,
758
- tool_call_id=tool_call.id,
759
- ),
760
- # name=name,
761
- otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
762
- run_id=self.run_id,
763
- step_id=self.step_id,
764
- )
765
- else:
766
- if prev_message_type and prev_message_type != "tool_call_message":
767
- message_index += 1
768
- tool_call_delta = ToolCallDelta(
769
- name=tool_call.function.name,
770
- arguments=tool_call.function.arguments,
771
- tool_call_id=tool_call.id,
772
- )
773
- tool_call_msg = ToolCallMessage(
774
- id=self.letta_message_id,
775
- date=datetime.now(timezone.utc),
776
- tool_call=tool_call_delta,
777
- tool_calls=tool_call_delta,
778
- # name=name,
779
- otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
780
- run_id=self.run_id,
781
- step_id=self.step_id,
738
+ idx = getattr(tool_call, "index", None)
739
+ if idx is None:
740
+ idx = 0
741
+
742
+ if idx not in self._tool_call_start_order:
743
+ self._tool_call_start_order.append(idx)
744
+ if idx not in self._tool_calls_acc:
745
+ self._tool_calls_acc[idx] = {"name": "", "arguments": "", "id": ""}
746
+ acc = self._tool_calls_acc[idx]
747
+
748
+ if tool_call.function and tool_call.function.name:
749
+ acc["name"] += tool_call.function.name
750
+ if tool_call.function and tool_call.function.arguments:
751
+ acc["arguments"] += tool_call.function.arguments
752
+ if tool_call.id:
753
+ acc["id"] += tool_call.id
754
+
755
+ delta = ToolCallDelta(
756
+ name=tool_call.function.name if (tool_call.function and tool_call.function.name) else None,
757
+ arguments=tool_call.function.arguments if (tool_call.function and tool_call.function.arguments) else None,
758
+ tool_call_id=tool_call.id if tool_call.id else None,
782
759
  )
783
- prev_message_type = tool_call_msg.message_type
784
- yield tool_call_msg
760
+
761
+ if acc.get("name") and acc["name"] in self.requires_approval_tools:
762
+ tool_call_msg = ApprovalRequestMessage(
763
+ id=decrement_message_uuid(self.letta_message_id),
764
+ date=datetime.now(timezone.utc),
765
+ tool_call=delta,
766
+ otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
767
+ run_id=self.run_id,
768
+ step_id=self.step_id,
769
+ )
770
+ else:
771
+ if prev_message_type and prev_message_type != "tool_call_message":
772
+ message_index += 1
773
+ tool_call_msg = ToolCallMessage(
774
+ id=self.letta_message_id,
775
+ date=datetime.now(timezone.utc),
776
+ tool_call=delta,
777
+ tool_calls=delta,
778
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
779
+ run_id=self.run_id,
780
+ step_id=self.step_id,
781
+ )
782
+ prev_message_type = tool_call_msg.message_type
783
+ yield tool_call_msg
785
784
 
786
785
 
787
786
  class SimpleOpenAIResponsesStreamingInterface:
@@ -813,7 +812,7 @@ class SimpleOpenAIResponsesStreamingInterface:
813
812
  # Premake IDs for database writes
814
813
  self.letta_message_id = Message.generate_id()
815
814
  self.model = model
816
- self.final_response = None
815
+ self.final_response: Optional[ParsedResponse] = None
817
816
 
818
817
  def get_content(self) -> list[TextContent | SummarizedReasoningContent]:
819
818
  """This includes both SummarizedReasoningContent and TextContent"""
@@ -844,31 +843,35 @@ class SimpleOpenAIResponsesStreamingInterface:
844
843
 
845
844
  return content
846
845
 
847
- def get_tool_call_object(self) -> ToolCall:
848
- """Useful for agent loop"""
846
+ def get_tool_call_objects(self) -> list[ToolCall]:
847
+ """Return finalized tool calls (parallel supported) from final response."""
849
848
  if self.final_response is None:
850
- raise ValueError("No final response available")
851
-
852
- tool_calls = []
853
- for response in self.final_response.output:
854
- # TODO make sure this shouldn't be ResponseCustomToolCall?
855
- if isinstance(response, ResponseFunctionToolCall):
856
- tool_calls.append(
857
- ToolCall(
858
- id=response.call_id,
859
- function=FunctionCall(
860
- name=response.name,
861
- arguments=response.arguments,
862
- ),
849
+ return []
850
+
851
+ tool_calls: list[ToolCall] = []
852
+ for item in self.final_response.output:
853
+ if isinstance(item, ResponseFunctionToolCall):
854
+ call_id = item.call_id
855
+ name = item.name
856
+ arguments = item.arguments
857
+ if call_id and name is not None:
858
+ tool_calls.append(
859
+ ToolCall(
860
+ id=call_id,
861
+ function=FunctionCall(
862
+ name=name,
863
+ arguments=arguments,
864
+ ),
865
+ )
863
866
  )
864
- )
865
867
 
866
- if len(tool_calls) == 0:
867
- raise ValueError("No tool calls available")
868
- if len(tool_calls) > 1:
869
- raise ValueError(f"Got {len(tool_calls)} tool calls, expected 1")
868
+ return tool_calls
870
869
 
871
- return tool_calls[0]
870
+ def get_tool_call_object(self) -> ToolCall:
871
+ calls = self.get_tool_call_objects()
872
+ if not calls:
873
+ raise ValueError("No tool calls available")
874
+ return calls[0]
872
875
 
873
876
  async def process(
874
877
  self,