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
letta/__init__.py CHANGED
@@ -5,7 +5,7 @@ try:
5
5
  __version__ = version("letta")
6
6
  except PackageNotFoundError:
7
7
  # Fallback for development installations
8
- __version__ = "0.13.0"
8
+ __version__ = "0.11.7"
9
9
 
10
10
  if os.environ.get("LETTA_VERSION"):
11
11
  __version__ = os.environ["LETTA_VERSION"]
@@ -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,
@@ -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
  )
@@ -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
- await self.summarize_conversation_history(
126
- in_context_messages=in_context_messages,
127
- new_letta_messages=self.response_messages,
128
- total_tokens=self.usage.total_tokens,
129
- force=False,
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
- await self.summarize_conversation_history(
221
- in_context_messages=in_context_messages,
222
- new_letta_messages=self.response_messages,
223
- total_tokens=self.usage.total_tokens,
224
- force=False,
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.response_messages,
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 Anthropic parallel tool use when no tool rules are attached
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,16 @@ 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
384
458
  except Exception:
385
459
  # if this fails, we simply don't enable parallel tool use
386
460
  pass
@@ -420,11 +494,11 @@ class LettaAgentV3(LettaAgentV2):
420
494
  messages = await self.summarize_conversation_history(
421
495
  in_context_messages=messages,
422
496
  new_letta_messages=self.response_messages,
423
- llm_config=self.agent_state.llm_config,
424
497
  force=True,
425
498
  )
426
499
  else:
427
- self.stop_reason = LettaStopReason(stop_reason=StopReasonType.llm_api_error.value)
500
+ self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
501
+ self.logger.error(f"Unknown error occured for run {run_id}: {e}")
428
502
  raise e
429
503
 
430
504
  step_progression, step_metrics = self._step_checkpoint_llm_request_finish(
@@ -434,11 +508,13 @@ class LettaAgentV3(LettaAgentV2):
434
508
  self._update_global_usage_stats(llm_adapter.usage)
435
509
 
436
510
  # Handle the AI response with the extracted data (supports multiple tool calls)
437
- # Gather tool calls. Approval paths specify a single tool call.
511
+ # Gather tool calls - check for multi-call API first, then fall back to single
438
512
  if hasattr(llm_adapter, "tool_calls") and llm_adapter.tool_calls:
439
513
  tool_calls = llm_adapter.tool_calls
440
514
  elif llm_adapter.tool_call is not None:
441
515
  tool_calls = [llm_adapter.tool_call]
516
+ else:
517
+ tool_calls = []
442
518
 
443
519
  aggregated_persisted: list[Message] = []
444
520
  persisted_messages, self.should_continue, self.stop_reason = await self._handle_ai_response(
@@ -468,6 +544,7 @@ class LettaAgentV3(LettaAgentV2):
468
544
 
469
545
  new_message_idx = len(input_messages_to_persist) if input_messages_to_persist else 0
470
546
  self.response_messages.extend(aggregated_persisted[new_message_idx:])
547
+ self.response_messages_for_metadata.extend(aggregated_persisted[new_message_idx:]) # Track for job metadata
471
548
 
472
549
  if llm_adapter.supports_token_streaming():
473
550
  # Stream each tool return if tools were executed
@@ -673,6 +750,8 @@ class LettaAgentV3(LettaAgentV2):
673
750
  for message in messages_to_persist:
674
751
  if message.run_id is None:
675
752
  message.run_id = run_id
753
+ if message.step_id is None:
754
+ message.step_id = step_id
676
755
 
677
756
  persisted_messages = await self.message_manager.create_many_messages_async(
678
757
  messages_to_persist, actor=self.actor, run_id=run_id, project_id=agent_state.project_id, template_id=agent_state.template_id
@@ -699,6 +778,8 @@ class LettaAgentV3(LettaAgentV2):
699
778
  for message in messages_to_persist:
700
779
  if message.run_id is None:
701
780
  message.run_id = run_id
781
+ if message.step_id is None:
782
+ message.step_id = step_id
702
783
 
703
784
  persisted_messages = await self.message_manager.create_many_messages_async(
704
785
  messages_to_persist,
@@ -909,10 +990,12 @@ class LettaAgentV3(LettaAgentV2):
909
990
 
910
991
  messages_to_persist: list[Message] = (initial_messages or []) + parallel_messages
911
992
 
912
- # Set run_id on all messages before persisting
993
+ # Set run_id and step_id on all messages before persisting
913
994
  for message in messages_to_persist:
914
995
  if message.run_id is None:
915
996
  message.run_id = run_id
997
+ if message.step_id is None:
998
+ message.step_id = step_id
916
999
 
917
1000
  # Persist all messages
918
1001
  persisted_messages = await self.message_manager.create_many_messages_async(
@@ -924,15 +1007,25 @@ class LettaAgentV3(LettaAgentV2):
924
1007
  )
925
1008
 
926
1009
  # 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
1010
  aggregate_continue = any(persisted_continue_flags) if persisted_continue_flags else False
930
- aggregate_continue = aggregate_continue or tool_call_denials or tool_returns # continue if any tool call was denied or returned
1011
+ aggregate_continue = aggregate_continue or tool_call_denials or tool_returns
1012
+
1013
+ # Determine aggregate stop reason
931
1014
  aggregate_stop_reason = None
932
1015
  for sr in persisted_stop_reasons:
933
1016
  if sr is not None:
934
1017
  aggregate_stop_reason = sr
935
1018
 
1019
+ # For parallel tool calls, always continue to allow the agent to process/summarize results
1020
+ # unless a terminal tool was called or we hit max steps
1021
+ if len(exec_specs) > 1:
1022
+ has_terminal = any(sr and sr.stop_reason == StopReasonType.tool_rule.value for sr in persisted_stop_reasons)
1023
+ is_max_steps = any(sr and sr.stop_reason == StopReasonType.max_steps.value for sr in persisted_stop_reasons)
1024
+
1025
+ if not has_terminal and not is_max_steps:
1026
+ # Force continuation for parallel tool execution
1027
+ aggregate_continue = True
1028
+ aggregate_stop_reason = None
936
1029
  return persisted_messages, aggregate_continue, aggregate_stop_reason
937
1030
 
938
1031
  @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,9 @@
1
+ from temporalio import activity
2
+
3
+ from letta.agents.temporal.types import PreparedMessages
4
+
5
+
6
+ @activity.defn(name="example_activity")
7
+ async def example_activity(input_: PreparedMessages) -> str:
8
+ # Process the result from the previous activity
9
+ pass
@@ -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