letta-nightly 0.13.0.dev20251031104146__py3-none-any.whl → 0.13.1.dev20251101010313__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.
- letta/__init__.py +1 -1
- letta/adapters/simple_llm_stream_adapter.py +1 -0
- letta/agents/letta_agent_v2.py +8 -0
- letta/agents/letta_agent_v3.py +127 -27
- letta/agents/temporal/activities/__init__.py +25 -0
- letta/agents/temporal/activities/create_messages.py +26 -0
- letta/agents/temporal/activities/create_step.py +57 -0
- letta/agents/temporal/activities/example_activity.py +9 -0
- letta/agents/temporal/activities/execute_tool.py +130 -0
- letta/agents/temporal/activities/llm_request.py +114 -0
- letta/agents/temporal/activities/prepare_messages.py +27 -0
- letta/agents/temporal/activities/refresh_context.py +160 -0
- letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
- letta/agents/temporal/activities/update_message_ids.py +25 -0
- letta/agents/temporal/activities/update_run.py +43 -0
- letta/agents/temporal/constants.py +59 -0
- letta/agents/temporal/temporal_agent_workflow.py +704 -0
- letta/agents/temporal/types.py +275 -0
- letta/constants.py +11 -0
- letta/errors.py +4 -0
- letta/functions/function_sets/base.py +0 -11
- letta/groups/helpers.py +7 -1
- letta/groups/sleeptime_multi_agent_v4.py +4 -3
- letta/interfaces/anthropic_streaming_interface.py +0 -1
- letta/interfaces/openai_streaming_interface.py +103 -100
- letta/llm_api/anthropic_client.py +57 -12
- letta/llm_api/bedrock_client.py +1 -0
- letta/llm_api/deepseek_client.py +3 -2
- letta/llm_api/google_vertex_client.py +5 -4
- letta/llm_api/groq_client.py +1 -0
- letta/llm_api/llm_client_base.py +15 -1
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +17 -3
- letta/llm_api/xai_client.py +1 -0
- letta/orm/agent.py +3 -0
- letta/orm/organization.py +4 -0
- letta/orm/sqlalchemy_base.py +7 -0
- letta/otel/tracing.py +131 -4
- letta/schemas/agent.py +108 -40
- letta/schemas/agent_file.py +10 -10
- letta/schemas/block.py +22 -3
- letta/schemas/enums.py +21 -0
- letta/schemas/environment_variables.py +3 -2
- letta/schemas/group.py +3 -3
- letta/schemas/letta_response.py +36 -4
- letta/schemas/llm_batch_job.py +3 -3
- letta/schemas/llm_config.py +123 -4
- letta/schemas/mcp.py +3 -2
- letta/schemas/mcp_server.py +3 -2
- letta/schemas/message.py +167 -49
- letta/schemas/model.py +265 -0
- letta/schemas/organization.py +2 -1
- letta/schemas/passage.py +2 -1
- letta/schemas/provider_trace.py +2 -1
- letta/schemas/providers/openrouter.py +1 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/step_metrics.py +2 -1
- letta/schemas/tool_rule.py +2 -2
- letta/schemas/user.py +2 -1
- letta/server/rest_api/app.py +5 -1
- letta/server/rest_api/routers/v1/__init__.py +4 -0
- letta/server/rest_api/routers/v1/agents.py +71 -9
- letta/server/rest_api/routers/v1/blocks.py +7 -7
- letta/server/rest_api/routers/v1/groups.py +40 -0
- letta/server/rest_api/routers/v1/identities.py +2 -2
- letta/server/rest_api/routers/v1/internal_agents.py +31 -0
- letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
- letta/server/rest_api/routers/v1/internal_runs.py +25 -1
- letta/server/rest_api/routers/v1/runs.py +2 -22
- letta/server/rest_api/routers/v1/tools.py +12 -1
- letta/server/server.py +20 -4
- letta/services/agent_manager.py +4 -4
- letta/services/archive_manager.py +16 -0
- letta/services/group_manager.py +44 -0
- letta/services/helpers/run_manager_helper.py +2 -2
- letta/services/lettuce/lettuce_client.py +148 -0
- letta/services/mcp/base_client.py +9 -3
- letta/services/run_manager.py +148 -37
- letta/services/source_manager.py +91 -3
- letta/services/step_manager.py +2 -3
- letta/services/streaming_service.py +52 -13
- letta/services/summarizer/summarizer.py +28 -2
- letta/services/tool_executor/builtin_tool_executor.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +2 -117
- letta/services/tool_sandbox/e2b_sandbox.py +4 -1
- letta/services/tool_schema_generator.py +2 -2
- letta/validators.py +21 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/METADATA +1 -1
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/RECORD +93 -87
- letta/agent.py +0 -1758
- letta/cli/cli_load.py +0 -16
- letta/client/__init__.py +0 -0
- letta/client/streaming.py +0 -95
- letta/client/utils.py +0 -78
- letta/functions/async_composio_toolset.py +0 -109
- letta/functions/composio_helpers.py +0 -96
- letta/helpers/composio_helpers.py +0 -38
- letta/orm/job_messages.py +0 -33
- letta/schemas/providers.py +0 -1617
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
- letta/services/tool_executor/composio_tool_executor.py +0 -57
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/WHEEL +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/licenses/LICENSE +0 -0
letta/__init__.py
CHANGED
|
@@ -78,6 +78,7 @@ class SimpleLLMStreamAdapter(LettaLLMStreamAdapter):
|
|
|
78
78
|
use_responses = "input" in request_data and "messages" not in request_data
|
|
79
79
|
# No support for Responses API proxy
|
|
80
80
|
is_proxy = self.llm_config.provider_name == "lmstudio_openai"
|
|
81
|
+
|
|
81
82
|
if use_responses and not is_proxy:
|
|
82
83
|
self.interface = SimpleOpenAIResponsesStreamingInterface(
|
|
83
84
|
is_openai_proxy=False,
|
letta/agents/letta_agent_v2.py
CHANGED
|
@@ -911,6 +911,10 @@ class LettaAgentV2(BaseAgentV2):
|
|
|
911
911
|
)
|
|
912
912
|
messages_to_persist = (initial_messages or []) + tool_call_messages
|
|
913
913
|
|
|
914
|
+
for message in messages_to_persist:
|
|
915
|
+
message.step_id = step_id
|
|
916
|
+
message.run_id = run_id
|
|
917
|
+
|
|
914
918
|
persisted_messages = await self.message_manager.create_many_messages_async(
|
|
915
919
|
messages_to_persist,
|
|
916
920
|
actor=self.actor,
|
|
@@ -1028,6 +1032,10 @@ class LettaAgentV2(BaseAgentV2):
|
|
|
1028
1032
|
)
|
|
1029
1033
|
messages_to_persist = (initial_messages or []) + tool_call_messages
|
|
1030
1034
|
|
|
1035
|
+
for message in messages_to_persist:
|
|
1036
|
+
message.step_id = step_id
|
|
1037
|
+
message.run_id = run_id
|
|
1038
|
+
|
|
1031
1039
|
persisted_messages = await self.message_manager.create_many_messages_async(
|
|
1032
1040
|
messages_to_persist, actor=self.actor, run_id=run_id, project_id=agent_state.project_id, template_id=agent_state.template_id
|
|
1033
1041
|
)
|
letta/agents/letta_agent_v3.py
CHANGED
|
@@ -19,7 +19,7 @@ from letta.agents.helpers import (
|
|
|
19
19
|
merge_and_validate_prefilled_args,
|
|
20
20
|
)
|
|
21
21
|
from letta.agents.letta_agent_v2 import LettaAgentV2
|
|
22
|
-
from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM
|
|
22
|
+
from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM, SUMMARIZATION_TRIGGER_MULTIPLIER
|
|
23
23
|
from letta.errors import ContextWindowExceededError, LLMError
|
|
24
24
|
from letta.helpers import ToolRulesSolver
|
|
25
25
|
from letta.helpers.datetime_helpers import get_utc_time, get_utc_timestamp_ns
|
|
@@ -37,6 +37,7 @@ from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
|
|
|
37
37
|
from letta.schemas.step import StepProgression
|
|
38
38
|
from letta.schemas.step_metrics import StepMetrics
|
|
39
39
|
from letta.schemas.tool_execution_result import ToolExecutionResult
|
|
40
|
+
from letta.schemas.usage import LettaUsageStatistics
|
|
40
41
|
from letta.server.rest_api.utils import (
|
|
41
42
|
create_approval_request_message_from_llm_response,
|
|
42
43
|
create_letta_messages_from_llm_response,
|
|
@@ -67,6 +68,13 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
67
68
|
def _initialize_state(self):
|
|
68
69
|
super()._initialize_state()
|
|
69
70
|
self._require_tool_call = False
|
|
71
|
+
self.last_step_usage = None
|
|
72
|
+
self.response_messages_for_metadata = [] # Separate accumulator for streaming job metadata
|
|
73
|
+
|
|
74
|
+
def _update_global_usage_stats(self, step_usage_stats: LettaUsageStatistics):
|
|
75
|
+
"""Override to track per-step usage for context limit checks"""
|
|
76
|
+
self.last_step_usage = step_usage_stats
|
|
77
|
+
super()._update_global_usage_stats(step_usage_stats)
|
|
70
78
|
|
|
71
79
|
@trace_method
|
|
72
80
|
async def step(
|
|
@@ -115,19 +123,46 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
115
123
|
async for chunk in response:
|
|
116
124
|
response_letta_messages.append(chunk)
|
|
117
125
|
|
|
126
|
+
# Proactive summarization if approaching context limit
|
|
127
|
+
if (
|
|
128
|
+
self.last_step_usage
|
|
129
|
+
and self.last_step_usage.total_tokens > self.agent_state.llm_config.context_window * SUMMARIZATION_TRIGGER_MULTIPLIER
|
|
130
|
+
and not self.agent_state.message_buffer_autoclear
|
|
131
|
+
):
|
|
132
|
+
self.logger.warning(
|
|
133
|
+
f"Step usage ({self.last_step_usage.total_tokens} tokens) approaching "
|
|
134
|
+
f"context limit ({self.agent_state.llm_config.context_window}), triggering summarization."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
in_context_messages = await self.summarize_conversation_history(
|
|
138
|
+
in_context_messages=in_context_messages,
|
|
139
|
+
new_letta_messages=self.response_messages,
|
|
140
|
+
total_tokens=self.last_step_usage.total_tokens,
|
|
141
|
+
force=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Clear to avoid duplication in next iteration
|
|
145
|
+
self.response_messages = []
|
|
146
|
+
|
|
118
147
|
if not self.should_continue:
|
|
119
148
|
break
|
|
120
149
|
|
|
121
150
|
input_messages_to_persist = []
|
|
122
151
|
|
|
123
|
-
# Rebuild context window after stepping
|
|
152
|
+
# Rebuild context window after stepping (safety net)
|
|
124
153
|
if not self.agent_state.message_buffer_autoclear:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
if self.last_step_usage:
|
|
155
|
+
await self.summarize_conversation_history(
|
|
156
|
+
in_context_messages=in_context_messages,
|
|
157
|
+
new_letta_messages=self.response_messages,
|
|
158
|
+
total_tokens=self.last_step_usage.total_tokens,
|
|
159
|
+
force=False,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
self.logger.warning(
|
|
163
|
+
"Post-loop summarization skipped: last_step_usage is None. "
|
|
164
|
+
"No step completed successfully or usage stats were not updated."
|
|
165
|
+
)
|
|
131
166
|
|
|
132
167
|
if self.stop_reason is None:
|
|
133
168
|
self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
|
|
@@ -211,18 +246,45 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
211
246
|
yield f"data: {chunk.model_dump_json()}\n\n"
|
|
212
247
|
first_chunk = False
|
|
213
248
|
|
|
249
|
+
# Proactive summarization if approaching context limit
|
|
250
|
+
if (
|
|
251
|
+
self.last_step_usage
|
|
252
|
+
and self.last_step_usage.total_tokens > self.agent_state.llm_config.context_window * SUMMARIZATION_TRIGGER_MULTIPLIER
|
|
253
|
+
and not self.agent_state.message_buffer_autoclear
|
|
254
|
+
):
|
|
255
|
+
self.logger.warning(
|
|
256
|
+
f"Step usage ({self.last_step_usage.total_tokens} tokens) approaching "
|
|
257
|
+
f"context limit ({self.agent_state.llm_config.context_window}), triggering summarization."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
in_context_messages = await self.summarize_conversation_history(
|
|
261
|
+
in_context_messages=in_context_messages,
|
|
262
|
+
new_letta_messages=self.response_messages,
|
|
263
|
+
total_tokens=self.last_step_usage.total_tokens,
|
|
264
|
+
force=True,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Clear to avoid duplication in next iteration
|
|
268
|
+
self.response_messages = []
|
|
269
|
+
|
|
214
270
|
if not self.should_continue:
|
|
215
271
|
break
|
|
216
272
|
|
|
217
273
|
input_messages_to_persist = []
|
|
218
274
|
|
|
219
275
|
if not self.agent_state.message_buffer_autoclear:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
if self.last_step_usage:
|
|
277
|
+
await self.summarize_conversation_history(
|
|
278
|
+
in_context_messages=in_context_messages,
|
|
279
|
+
new_letta_messages=self.response_messages,
|
|
280
|
+
total_tokens=self.last_step_usage.total_tokens,
|
|
281
|
+
force=False,
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
self.logger.warning(
|
|
285
|
+
"Post-loop summarization skipped: last_step_usage is None. "
|
|
286
|
+
"No step completed successfully or usage stats were not updated."
|
|
287
|
+
)
|
|
226
288
|
|
|
227
289
|
except Exception as e:
|
|
228
290
|
self.logger.warning(f"Error during agent stream: {e}", exc_info=True)
|
|
@@ -231,7 +293,7 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
231
293
|
|
|
232
294
|
if run_id:
|
|
233
295
|
letta_messages = Message.to_letta_messages_from_list(
|
|
234
|
-
self.
|
|
296
|
+
self.response_messages_for_metadata, # Use separate accumulator to preserve all messages
|
|
235
297
|
use_assistant_message=False, # NOTE: set to false
|
|
236
298
|
reverse=False,
|
|
237
299
|
# text_is_assistant_message=(self.agent_state.agent_type == AgentType.react_agent),
|
|
@@ -364,13 +426,15 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
364
426
|
requires_subsequent_tool_call=self._require_tool_call,
|
|
365
427
|
)
|
|
366
428
|
# TODO: Extend to more providers, and also approval tool rules
|
|
367
|
-
# Enable
|
|
429
|
+
# Enable parallel tool use when no tool rules are attached
|
|
368
430
|
try:
|
|
431
|
+
no_tool_rules = (
|
|
432
|
+
not self.agent_state.tool_rules
|
|
433
|
+
or len([t for t in self.agent_state.tool_rules if t.type != "requires_approval"]) == 0
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Anthropic/Bedrock parallel tool use
|
|
369
437
|
if self.agent_state.llm_config.model_endpoint_type in ["anthropic", "bedrock"]:
|
|
370
|
-
no_tool_rules = (
|
|
371
|
-
not self.agent_state.tool_rules
|
|
372
|
-
or len([t for t in self.agent_state.tool_rules if t.type != "requires_approval"]) == 0
|
|
373
|
-
)
|
|
374
438
|
if (
|
|
375
439
|
isinstance(request_data.get("tool_choice"), dict)
|
|
376
440
|
and "disable_parallel_tool_use" in request_data["tool_choice"]
|
|
@@ -381,6 +445,23 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
381
445
|
else:
|
|
382
446
|
# Explicitly disable when tool rules present or llm_config toggled off
|
|
383
447
|
request_data["tool_choice"]["disable_parallel_tool_use"] = True
|
|
448
|
+
|
|
449
|
+
# OpenAI parallel tool use
|
|
450
|
+
elif self.agent_state.llm_config.model_endpoint_type == "openai":
|
|
451
|
+
# For OpenAI, we control parallel tool calling via parallel_tool_calls field
|
|
452
|
+
# Only allow parallel tool calls when no tool rules and enabled in config
|
|
453
|
+
if "parallel_tool_calls" in request_data:
|
|
454
|
+
if no_tool_rules and self.agent_state.llm_config.parallel_tool_calls:
|
|
455
|
+
request_data["parallel_tool_calls"] = True
|
|
456
|
+
else:
|
|
457
|
+
request_data["parallel_tool_calls"] = False
|
|
458
|
+
|
|
459
|
+
# Gemini (Google AI/Vertex) parallel tool use
|
|
460
|
+
elif self.agent_state.llm_config.model_endpoint_type in ["google_ai", "google_vertex"]:
|
|
461
|
+
# Gemini supports parallel tool calling natively through multiple parts in the response
|
|
462
|
+
# We just need to ensure the config flag is set for tracking purposes
|
|
463
|
+
# The actual handling happens in GoogleVertexClient.convert_response_to_chat_completion
|
|
464
|
+
pass # No specific request_data field needed for Gemini
|
|
384
465
|
except Exception:
|
|
385
466
|
# if this fails, we simply don't enable parallel tool use
|
|
386
467
|
pass
|
|
@@ -420,11 +501,11 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
420
501
|
messages = await self.summarize_conversation_history(
|
|
421
502
|
in_context_messages=messages,
|
|
422
503
|
new_letta_messages=self.response_messages,
|
|
423
|
-
llm_config=self.agent_state.llm_config,
|
|
424
504
|
force=True,
|
|
425
505
|
)
|
|
426
506
|
else:
|
|
427
|
-
self.stop_reason = LettaStopReason(stop_reason=StopReasonType.
|
|
507
|
+
self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
|
|
508
|
+
self.logger.error(f"Unknown error occured for run {run_id}: {e}")
|
|
428
509
|
raise e
|
|
429
510
|
|
|
430
511
|
step_progression, step_metrics = self._step_checkpoint_llm_request_finish(
|
|
@@ -434,11 +515,13 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
434
515
|
self._update_global_usage_stats(llm_adapter.usage)
|
|
435
516
|
|
|
436
517
|
# Handle the AI response with the extracted data (supports multiple tool calls)
|
|
437
|
-
# Gather tool calls
|
|
518
|
+
# Gather tool calls - check for multi-call API first, then fall back to single
|
|
438
519
|
if hasattr(llm_adapter, "tool_calls") and llm_adapter.tool_calls:
|
|
439
520
|
tool_calls = llm_adapter.tool_calls
|
|
440
521
|
elif llm_adapter.tool_call is not None:
|
|
441
522
|
tool_calls = [llm_adapter.tool_call]
|
|
523
|
+
else:
|
|
524
|
+
tool_calls = []
|
|
442
525
|
|
|
443
526
|
aggregated_persisted: list[Message] = []
|
|
444
527
|
persisted_messages, self.should_continue, self.stop_reason = await self._handle_ai_response(
|
|
@@ -468,6 +551,7 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
468
551
|
|
|
469
552
|
new_message_idx = len(input_messages_to_persist) if input_messages_to_persist else 0
|
|
470
553
|
self.response_messages.extend(aggregated_persisted[new_message_idx:])
|
|
554
|
+
self.response_messages_for_metadata.extend(aggregated_persisted[new_message_idx:]) # Track for job metadata
|
|
471
555
|
|
|
472
556
|
if llm_adapter.supports_token_streaming():
|
|
473
557
|
# Stream each tool return if tools were executed
|
|
@@ -673,6 +757,8 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
673
757
|
for message in messages_to_persist:
|
|
674
758
|
if message.run_id is None:
|
|
675
759
|
message.run_id = run_id
|
|
760
|
+
if message.step_id is None:
|
|
761
|
+
message.step_id = step_id
|
|
676
762
|
|
|
677
763
|
persisted_messages = await self.message_manager.create_many_messages_async(
|
|
678
764
|
messages_to_persist, actor=self.actor, run_id=run_id, project_id=agent_state.project_id, template_id=agent_state.template_id
|
|
@@ -699,6 +785,8 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
699
785
|
for message in messages_to_persist:
|
|
700
786
|
if message.run_id is None:
|
|
701
787
|
message.run_id = run_id
|
|
788
|
+
if message.step_id is None:
|
|
789
|
+
message.step_id = step_id
|
|
702
790
|
|
|
703
791
|
persisted_messages = await self.message_manager.create_many_messages_async(
|
|
704
792
|
messages_to_persist,
|
|
@@ -909,10 +997,12 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
909
997
|
|
|
910
998
|
messages_to_persist: list[Message] = (initial_messages or []) + parallel_messages
|
|
911
999
|
|
|
912
|
-
# Set run_id on all messages before persisting
|
|
1000
|
+
# Set run_id and step_id on all messages before persisting
|
|
913
1001
|
for message in messages_to_persist:
|
|
914
1002
|
if message.run_id is None:
|
|
915
1003
|
message.run_id = run_id
|
|
1004
|
+
if message.step_id is None:
|
|
1005
|
+
message.step_id = step_id
|
|
916
1006
|
|
|
917
1007
|
# Persist all messages
|
|
918
1008
|
persisted_messages = await self.message_manager.create_many_messages_async(
|
|
@@ -924,15 +1014,25 @@ class LettaAgentV3(LettaAgentV2):
|
|
|
924
1014
|
)
|
|
925
1015
|
|
|
926
1016
|
# 5g. Aggregate continuation decisions
|
|
927
|
-
# For multiple tools: continue if ANY says continue, use last non-None stop_reason
|
|
928
|
-
# For single tool: use its decision directly
|
|
929
1017
|
aggregate_continue = any(persisted_continue_flags) if persisted_continue_flags else False
|
|
930
|
-
aggregate_continue = aggregate_continue or tool_call_denials or tool_returns
|
|
1018
|
+
aggregate_continue = aggregate_continue or tool_call_denials or tool_returns
|
|
1019
|
+
|
|
1020
|
+
# Determine aggregate stop reason
|
|
931
1021
|
aggregate_stop_reason = None
|
|
932
1022
|
for sr in persisted_stop_reasons:
|
|
933
1023
|
if sr is not None:
|
|
934
1024
|
aggregate_stop_reason = sr
|
|
935
1025
|
|
|
1026
|
+
# For parallel tool calls, always continue to allow the agent to process/summarize results
|
|
1027
|
+
# unless a terminal tool was called or we hit max steps
|
|
1028
|
+
if len(exec_specs) > 1:
|
|
1029
|
+
has_terminal = any(sr and sr.stop_reason == StopReasonType.tool_rule.value for sr in persisted_stop_reasons)
|
|
1030
|
+
is_max_steps = any(sr and sr.stop_reason == StopReasonType.max_steps.value for sr in persisted_stop_reasons)
|
|
1031
|
+
|
|
1032
|
+
if not has_terminal and not is_max_steps:
|
|
1033
|
+
# Force continuation for parallel tool execution
|
|
1034
|
+
aggregate_continue = True
|
|
1035
|
+
aggregate_stop_reason = None
|
|
936
1036
|
return persisted_messages, aggregate_continue, aggregate_stop_reason
|
|
937
1037
|
|
|
938
1038
|
@trace_method
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from letta.agents.temporal.activities.create_messages import create_messages
|
|
2
|
+
from letta.agents.temporal.activities.create_step import create_step
|
|
3
|
+
from letta.agents.temporal.activities.example_activity import example_activity
|
|
4
|
+
from letta.agents.temporal.activities.execute_tool import execute_tool
|
|
5
|
+
from letta.agents.temporal.activities.llm_request import llm_request
|
|
6
|
+
from letta.agents.temporal.activities.prepare_messages import prepare_messages
|
|
7
|
+
from letta.agents.temporal.activities.refresh_context import refresh_context_and_system_message
|
|
8
|
+
from letta.agents.temporal.activities.summarize_conversation_history import summarize_conversation_history
|
|
9
|
+
from letta.agents.temporal.activities.update_message_ids import update_message_ids
|
|
10
|
+
from letta.agents.temporal.activities.update_run import update_run
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"prepare_messages",
|
|
14
|
+
"refresh_context_and_system_message",
|
|
15
|
+
"llm_request",
|
|
16
|
+
"summarize_conversation_history",
|
|
17
|
+
"example_activity",
|
|
18
|
+
"execute_tool",
|
|
19
|
+
"create_messages",
|
|
20
|
+
"create_step",
|
|
21
|
+
"prepare_messages",
|
|
22
|
+
"refresh_context_and_system_message",
|
|
23
|
+
"update_message_ids",
|
|
24
|
+
"update_run",
|
|
25
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from temporalio import activity
|
|
2
|
+
|
|
3
|
+
from letta.agents.temporal.types import CreateMessagesParams, CreateMessagesResult
|
|
4
|
+
from letta.services.message_manager import MessageManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@activity.defn(name="create_messages")
|
|
8
|
+
async def create_messages(params: CreateMessagesParams) -> CreateMessagesResult:
|
|
9
|
+
"""
|
|
10
|
+
Persist messages to the database.
|
|
11
|
+
|
|
12
|
+
This activity saves the messages to the database and returns the persisted messages
|
|
13
|
+
with their assigned IDs and timestamps.
|
|
14
|
+
"""
|
|
15
|
+
message_manager = MessageManager()
|
|
16
|
+
|
|
17
|
+
# Persist messages to database
|
|
18
|
+
persisted_messages = await message_manager.create_many_messages_async(
|
|
19
|
+
params.messages,
|
|
20
|
+
actor=params.actor,
|
|
21
|
+
project_id=params.project_id,
|
|
22
|
+
template_id=params.template_id,
|
|
23
|
+
allow_partial=True, # always allow partial to handle retries gracefully
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return CreateMessagesResult(messages=persisted_messages)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from temporalio import activity
|
|
2
|
+
|
|
3
|
+
from letta.agents.temporal.types import CreateStepParams, CreateStepResult
|
|
4
|
+
from letta.helpers.datetime_helpers import get_utc_timestamp_ns
|
|
5
|
+
from letta.schemas.enums import StepStatus
|
|
6
|
+
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
7
|
+
from letta.schemas.step_metrics import StepMetrics
|
|
8
|
+
from letta.services.step_manager import StepManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@activity.defn(name="create_step")
|
|
12
|
+
async def create_step(params: CreateStepParams) -> CreateStepResult:
|
|
13
|
+
"""
|
|
14
|
+
Persist step to the database, update usage statistics, and record metrics.
|
|
15
|
+
|
|
16
|
+
This activity saves the step to the database, updates its usage statistics,
|
|
17
|
+
and records timing metrics.
|
|
18
|
+
"""
|
|
19
|
+
step_manager = StepManager()
|
|
20
|
+
|
|
21
|
+
# Determine status based on stop_reason
|
|
22
|
+
status = StepStatus.ERROR if params.stop_reason == "error" else StepStatus.SUCCESS
|
|
23
|
+
|
|
24
|
+
# Persist step to database
|
|
25
|
+
persisted_step = await step_manager.log_step_async(
|
|
26
|
+
actor=params.actor,
|
|
27
|
+
agent_id=params.agent_state.id,
|
|
28
|
+
provider_name=params.agent_state.llm_config.model_endpoint_type,
|
|
29
|
+
provider_category=params.agent_state.llm_config.provider_category or "base",
|
|
30
|
+
model=params.agent_state.llm_config.model,
|
|
31
|
+
model_endpoint=params.agent_state.llm_config.model_endpoint,
|
|
32
|
+
context_window_limit=params.agent_state.llm_config.context_window,
|
|
33
|
+
usage=params.usage,
|
|
34
|
+
provider_id=None,
|
|
35
|
+
run_id=params.run_id,
|
|
36
|
+
step_id=params.step_id,
|
|
37
|
+
project_id=params.agent_state.project_id,
|
|
38
|
+
status=status,
|
|
39
|
+
allow_partial=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Record step metrics
|
|
43
|
+
await step_manager.record_step_metrics_async(
|
|
44
|
+
actor=params.actor,
|
|
45
|
+
step_id=persisted_step.id,
|
|
46
|
+
llm_request_ns=params.llm_request_ns,
|
|
47
|
+
tool_execution_ns=params.tool_execution_ns,
|
|
48
|
+
step_ns=params.step_ns,
|
|
49
|
+
agent_id=params.agent_state.id,
|
|
50
|
+
run_id=params.run_id,
|
|
51
|
+
project_id=params.agent_state.project_id,
|
|
52
|
+
template_id=params.agent_state.template_id,
|
|
53
|
+
base_template_id=params.agent_state.base_template_id,
|
|
54
|
+
allow_partial=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return CreateStepResult(step=persisted_step)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from temporalio import activity
|
|
5
|
+
|
|
6
|
+
from letta.agents.temporal.activities.llm_request import ApplicationError
|
|
7
|
+
from letta.agents.temporal.types import ExecuteToolParams, ExecuteToolResult
|
|
8
|
+
from letta.helpers.datetime_helpers import get_utc_timestamp_ns
|
|
9
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
|
10
|
+
from letta.services.agent_manager import AgentManager
|
|
11
|
+
from letta.services.block_manager import BlockManager
|
|
12
|
+
from letta.services.message_manager import MessageManager
|
|
13
|
+
from letta.services.passage_manager import PassageManager
|
|
14
|
+
from letta.services.run_manager import RunManager
|
|
15
|
+
from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@activity.defn(name="execute_tool")
|
|
19
|
+
async def execute_tool(params: ExecuteToolParams) -> ExecuteToolResult:
|
|
20
|
+
"""
|
|
21
|
+
Execute the tool using ToolExecutionManager.
|
|
22
|
+
Returns the execution result and timing information.
|
|
23
|
+
"""
|
|
24
|
+
message_manager = MessageManager()
|
|
25
|
+
agent_manager = AgentManager()
|
|
26
|
+
block_manager = BlockManager()
|
|
27
|
+
run_manager = RunManager()
|
|
28
|
+
passage_manager = PassageManager()
|
|
29
|
+
|
|
30
|
+
target_tool = next((x for x in params.agent_state.tools if x.name == params.tool_name), None)
|
|
31
|
+
|
|
32
|
+
if not target_tool:
|
|
33
|
+
return ExecuteToolResult(
|
|
34
|
+
tool_execution_result=ToolExecutionResult(
|
|
35
|
+
func_return=f"Tool {params.tool_name} not found",
|
|
36
|
+
status="error",
|
|
37
|
+
),
|
|
38
|
+
execution_time_ns=0,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
start_time = get_utc_timestamp_ns()
|
|
42
|
+
|
|
43
|
+
# Decrypt environment variable values
|
|
44
|
+
sandbox_env_vars = {var.key: var.get_value_secret().get_plaintext() for var in params.agent_state.secrets}
|
|
45
|
+
tool_execution_manager = ToolExecutionManager(
|
|
46
|
+
agent_state=params.agent_state,
|
|
47
|
+
message_manager=message_manager,
|
|
48
|
+
agent_manager=agent_manager,
|
|
49
|
+
block_manager=block_manager,
|
|
50
|
+
run_manager=run_manager,
|
|
51
|
+
passage_manager=passage_manager,
|
|
52
|
+
sandbox_env_vars=sandbox_env_vars,
|
|
53
|
+
actor=params.actor,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
tool_execution_result = await tool_execution_manager.execute_tool_async(
|
|
57
|
+
function_name=params.tool_name,
|
|
58
|
+
function_args=params.tool_args,
|
|
59
|
+
tool=target_tool,
|
|
60
|
+
step_id=params.step_id,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
end_time = get_utc_timestamp_ns()
|
|
64
|
+
|
|
65
|
+
# Exceptions are not JSON serializable, make sure to deserialize post activity execution
|
|
66
|
+
if isinstance(tool_execution_result.func_return, Exception):
|
|
67
|
+
tool_execution_result.func_return = _serialize_func_return(tool_execution_result.func_return)
|
|
68
|
+
|
|
69
|
+
return ExecuteToolResult(
|
|
70
|
+
tool_execution_result=tool_execution_result,
|
|
71
|
+
execution_time_ns=end_time - start_time, # TODO: actually record this or use native Temporal metrics?
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _serialize_func_return(e: Exception) -> str:
|
|
76
|
+
"""Serialize exception to be JSON serializable string"""
|
|
77
|
+
result = {
|
|
78
|
+
"type": type(e).__name__,
|
|
79
|
+
"module": type(e).__module__,
|
|
80
|
+
"args": list(e.args) if e.args else [],
|
|
81
|
+
"message": str(e),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Preserve custom attributes if present
|
|
85
|
+
if hasattr(e, "__dict__"):
|
|
86
|
+
custom_attrs = {k: v for k, v in e.__dict__.items() if isinstance(v, (str, int, float, bool, list, dict, type(None)))}
|
|
87
|
+
if custom_attrs:
|
|
88
|
+
result["custom_attrs"] = custom_attrs
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def deserialize_func_return(data: dict) -> Exception:
|
|
94
|
+
"""Deserialize back to Exception class"""
|
|
95
|
+
exception_map = {
|
|
96
|
+
"ValueError": ValueError,
|
|
97
|
+
"KeyError": KeyError,
|
|
98
|
+
"TypeError": TypeError,
|
|
99
|
+
"RuntimeError": RuntimeError,
|
|
100
|
+
"ApplicationError": ApplicationError,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
exc_class = exception_map.get(data["type"], Exception)
|
|
104
|
+
|
|
105
|
+
# Create exception with original args
|
|
106
|
+
if data.get("args"):
|
|
107
|
+
exc = exc_class(*data["args"])
|
|
108
|
+
else:
|
|
109
|
+
exc = exc_class(data.get("message", ""))
|
|
110
|
+
|
|
111
|
+
# Restore custom attributes if any
|
|
112
|
+
if data.get("custom_attrs"):
|
|
113
|
+
for key, value in data["custom_attrs"].items():
|
|
114
|
+
setattr(exc, key, value)
|
|
115
|
+
|
|
116
|
+
return exc
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_serialized_exception(data: Any) -> bool:
|
|
120
|
+
"""Check if data is a serialized exception"""
|
|
121
|
+
# Should have been serialized to a string
|
|
122
|
+
if not isinstance(data, str):
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Soft check if data has exception structure
|
|
127
|
+
parsed = json.loads(data)
|
|
128
|
+
return isinstance(parsed, dict) and "type" in parsed and "message" in parsed
|
|
129
|
+
except Exception:
|
|
130
|
+
return False
|