letta-nightly 0.11.0.dev20250807000848__py3-none-any.whl → 0.11.0.dev20250808055434__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.
Files changed (27) hide show
  1. letta/agent.py +2 -1
  2. letta/agents/letta_agent.py +215 -143
  3. letta/functions/function_sets/base.py +2 -2
  4. letta/functions/function_sets/files.py +22 -9
  5. letta/interfaces/anthropic_streaming_interface.py +291 -265
  6. letta/interfaces/openai_streaming_interface.py +270 -250
  7. letta/llm_api/anthropic.py +3 -10
  8. letta/llm_api/openai_client.py +6 -1
  9. letta/orm/__init__.py +1 -0
  10. letta/orm/step.py +14 -0
  11. letta/orm/step_metrics.py +71 -0
  12. letta/schemas/enums.py +9 -0
  13. letta/schemas/llm_config.py +8 -6
  14. letta/schemas/providers/lmstudio.py +2 -2
  15. letta/schemas/providers/openai.py +1 -1
  16. letta/schemas/step.py +6 -0
  17. letta/schemas/step_metrics.py +23 -0
  18. letta/schemas/tool_rule.py +10 -29
  19. letta/services/step_manager.py +179 -1
  20. letta/services/tool_executor/builtin_tool_executor.py +4 -1
  21. letta/services/tool_executor/core_tool_executor.py +2 -10
  22. letta/services/tool_executor/files_tool_executor.py +89 -40
  23. {letta_nightly-0.11.0.dev20250807000848.dist-info → letta_nightly-0.11.0.dev20250808055434.dist-info}/METADATA +1 -1
  24. {letta_nightly-0.11.0.dev20250807000848.dist-info → letta_nightly-0.11.0.dev20250808055434.dist-info}/RECORD +27 -25
  25. {letta_nightly-0.11.0.dev20250807000848.dist-info → letta_nightly-0.11.0.dev20250808055434.dist-info}/LICENSE +0 -0
  26. {letta_nightly-0.11.0.dev20250807000848.dist-info → letta_nightly-0.11.0.dev20250808055434.dist-info}/WHEEL +0 -0
  27. {letta_nightly-0.11.0.dev20250807000848.dist-info → letta_nightly-0.11.0.dev20250808055434.dist-info}/entry_points.txt +0 -0
letta/agent.py CHANGED
@@ -45,7 +45,7 @@ from letta.otel.tracing import log_event, trace_method
45
45
  from letta.schemas.agent import AgentState, AgentStepResponse, UpdateAgent, get_prompt_template_for_agent_type
46
46
  from letta.schemas.block import BlockUpdate
47
47
  from letta.schemas.embedding_config import EmbeddingConfig
48
- from letta.schemas.enums import MessageRole, ProviderType, ToolType
48
+ from letta.schemas.enums import MessageRole, ProviderType, StepStatus, ToolType
49
49
  from letta.schemas.letta_message_content import ImageContent, TextContent
50
50
  from letta.schemas.memory import ContextWindowOverview, Memory
51
51
  from letta.schemas.message import Message, MessageCreate, ToolReturn
@@ -991,6 +991,7 @@ class Agent(BaseAgent):
991
991
  job_id=job_id,
992
992
  step_id=step_id,
993
993
  project_id=self.agent_state.project_id,
994
+ status=StepStatus.SUCCESS, # Set to SUCCESS since we're logging after successful completion
994
995
  )
995
996
  for message in all_new_messages:
996
997
  message.step_id = step.id
@@ -34,7 +34,7 @@ from letta.otel.context import get_ctx_attributes
34
34
  from letta.otel.metric_registry import MetricRegistry
35
35
  from letta.otel.tracing import log_event, trace_method, tracer
36
36
  from letta.schemas.agent import AgentState, UpdateAgent
37
- from letta.schemas.enums import JobStatus, MessageRole, ProviderType, ToolType
37
+ from letta.schemas.enums import JobStatus, MessageRole, ProviderType, StepStatus, ToolType
38
38
  from letta.schemas.letta_message import MessageType
39
39
  from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
40
40
  from letta.schemas.letta_response import LettaResponse
@@ -241,6 +241,26 @@ class LettaAgent(BaseAgent):
241
241
 
242
242
  step_progression = StepProgression.START
243
243
  should_continue = False
244
+
245
+ # Create step early with PENDING status
246
+ logged_step = await self.step_manager.log_step_async(
247
+ actor=self.actor,
248
+ agent_id=agent_state.id,
249
+ provider_name=agent_state.llm_config.model_endpoint_type,
250
+ provider_category=agent_state.llm_config.provider_category or "base",
251
+ model=agent_state.llm_config.model,
252
+ model_endpoint=agent_state.llm_config.model_endpoint,
253
+ context_window_limit=agent_state.llm_config.context_window,
254
+ usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0),
255
+ provider_id=None,
256
+ job_id=self.current_run_id if self.current_run_id else None,
257
+ step_id=step_id,
258
+ project_id=agent_state.project_id,
259
+ status=StepStatus.PENDING,
260
+ )
261
+ # Only use step_id in messages if step was actually created
262
+ effective_step_id = step_id if logged_step else None
263
+
244
264
  try:
245
265
  request_data, response_data, current_in_context_messages, new_in_context_messages, valid_tool_names = (
246
266
  await self._build_and_request_from_llm(
@@ -295,13 +315,17 @@ class LettaAgent(BaseAgent):
295
315
  tool_rules_solver,
296
316
  response.usage,
297
317
  reasoning_content=reasoning,
298
- step_id=step_id,
318
+ step_id=effective_step_id,
299
319
  initial_messages=initial_messages,
300
320
  agent_step_span=agent_step_span,
301
321
  is_final_step=(i == max_steps - 1),
302
322
  )
303
323
  step_progression = StepProgression.STEP_LOGGED
304
324
 
325
+ # Update step with actual usage now that we have it (if step was created)
326
+ if logged_step:
327
+ await self.step_manager.update_step_success_async(self.actor, step_id, response.usage, stop_reason)
328
+
305
329
  # TODO (cliandy): handle message contexts with larger refactor and dedupe logic
306
330
  new_message_idx = len(initial_messages) if initial_messages else 0
307
331
  self.response_messages.extend(persisted_messages[new_message_idx:])
@@ -321,7 +345,7 @@ class LettaAgent(BaseAgent):
321
345
  provider_trace_create=ProviderTraceCreate(
322
346
  request_json=request_data,
323
347
  response_json=response_data,
324
- step_id=step_id,
348
+ step_id=step_id, # Use original step_id for telemetry
325
349
  organization_id=self.actor.organization_id,
326
350
  ),
327
351
  )
@@ -358,54 +382,57 @@ class LettaAgent(BaseAgent):
358
382
 
359
383
  # Update step if it needs to be updated
360
384
  finally:
361
- if settings.track_stop_reason:
362
- if step_progression == StepProgression.FINISHED and should_continue:
363
- continue
364
-
365
- self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id)
366
- self.logger.info("Running final update. Step Progression: %s", step_progression)
367
- try:
368
- if step_progression == StepProgression.FINISHED and not should_continue:
369
- if stop_reason is None:
370
- stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
385
+ if step_progression == StepProgression.FINISHED and should_continue:
386
+ continue
387
+
388
+ self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id)
389
+ self.logger.info("Running final update. Step Progression: %s", step_progression)
390
+ try:
391
+ if step_progression == StepProgression.FINISHED and not should_continue:
392
+ # Successfully completed - update with final usage and stop reason
393
+ if stop_reason is None:
394
+ stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
395
+ # Note: step already updated with success status after _handle_ai_response
396
+ if logged_step:
371
397
  await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason)
372
- break
398
+ break
373
399
 
374
- if step_progression < StepProgression.STEP_LOGGED:
375
- await self.step_manager.log_step_async(
400
+ # Handle error cases
401
+ if step_progression < StepProgression.STEP_LOGGED:
402
+ # Error occurred before step was fully logged
403
+ import traceback
404
+
405
+ if logged_step:
406
+ await self.step_manager.update_step_error_async(
376
407
  actor=self.actor,
377
- agent_id=agent_state.id,
378
- provider_name=agent_state.llm_config.model_endpoint_type,
379
- provider_category=agent_state.llm_config.provider_category or "base",
380
- model=agent_state.llm_config.model,
381
- model_endpoint=agent_state.llm_config.model_endpoint,
382
- context_window_limit=agent_state.llm_config.context_window,
383
- usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0),
384
- provider_id=None,
385
- job_id=self.current_run_id if self.current_run_id else None,
386
- step_id=step_id,
387
- project_id=agent_state.project_id,
408
+ step_id=step_id, # Use original step_id for telemetry
409
+ error_type=type(e).__name__ if "e" in locals() else "Unknown",
410
+ error_message=str(e) if "e" in locals() else "Unknown error",
411
+ error_traceback=traceback.format_exc(),
388
412
  stop_reason=stop_reason,
389
413
  )
390
- if step_progression <= StepProgression.RESPONSE_RECEIVED:
391
- # TODO (cliandy): persist response if we get it back
392
- if settings.track_errored_messages:
393
- for message in initial_messages:
394
- message.is_err = True
395
- message.step_id = step_id
396
- await self.message_manager.create_many_messages_async(initial_messages, actor=self.actor)
397
- elif step_progression <= StepProgression.LOGGED_TRACE:
398
- if stop_reason is None:
399
- self.logger.error("Error in step after logging step")
400
- stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
414
+
415
+ if step_progression <= StepProgression.RESPONSE_RECEIVED:
416
+ # TODO (cliandy): persist response if we get it back
417
+ if settings.track_errored_messages:
418
+ for message in initial_messages:
419
+ message.is_err = True
420
+ message.step_id = effective_step_id
421
+ await self.message_manager.create_many_messages_async(initial_messages, actor=self.actor)
422
+ elif step_progression <= StepProgression.LOGGED_TRACE:
423
+ if stop_reason is None:
424
+ self.logger.error("Error in step after logging step")
425
+ stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
426
+ if logged_step:
401
427
  await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason)
402
- else:
403
- self.logger.error("Invalid StepProgression value")
428
+ else:
429
+ self.logger.error("Invalid StepProgression value")
404
430
 
431
+ if settings.track_stop_reason:
405
432
  await self._log_request(request_start_timestamp_ns, request_span)
406
433
 
407
- except Exception as e:
408
- self.logger.error("Failed to update step: %s", e)
434
+ except Exception as e:
435
+ self.logger.error("Failed to update step: %s", e)
409
436
 
410
437
  if not should_continue:
411
438
  break
@@ -484,6 +511,25 @@ class LettaAgent(BaseAgent):
484
511
  step_progression = StepProgression.START
485
512
  should_continue = False
486
513
 
514
+ # Create step early with PENDING status
515
+ logged_step = await self.step_manager.log_step_async(
516
+ actor=self.actor,
517
+ agent_id=agent_state.id,
518
+ provider_name=agent_state.llm_config.model_endpoint_type,
519
+ provider_category=agent_state.llm_config.provider_category or "base",
520
+ model=agent_state.llm_config.model,
521
+ model_endpoint=agent_state.llm_config.model_endpoint,
522
+ context_window_limit=agent_state.llm_config.context_window,
523
+ usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0),
524
+ provider_id=None,
525
+ job_id=run_id if run_id else self.current_run_id,
526
+ step_id=step_id,
527
+ project_id=agent_state.project_id,
528
+ status=StepStatus.PENDING,
529
+ )
530
+ # Only use step_id in messages if step was actually created
531
+ effective_step_id = step_id if logged_step else None
532
+
487
533
  try:
488
534
  request_data, response_data, current_in_context_messages, new_in_context_messages, valid_tool_names = (
489
535
  await self._build_and_request_from_llm(
@@ -533,7 +579,7 @@ class LettaAgent(BaseAgent):
533
579
  tool_rules_solver,
534
580
  response.usage,
535
581
  reasoning_content=reasoning,
536
- step_id=step_id,
582
+ step_id=effective_step_id,
537
583
  initial_messages=initial_messages,
538
584
  agent_step_span=agent_step_span,
539
585
  is_final_step=(i == max_steps - 1),
@@ -541,6 +587,10 @@ class LettaAgent(BaseAgent):
541
587
  )
542
588
  step_progression = StepProgression.STEP_LOGGED
543
589
 
590
+ # Update step with actual usage now that we have it (if step was created)
591
+ if logged_step:
592
+ await self.step_manager.update_step_success_async(self.actor, step_id, response.usage, stop_reason)
593
+
544
594
  new_message_idx = len(initial_messages) if initial_messages else 0
545
595
  self.response_messages.extend(persisted_messages[new_message_idx:])
546
596
  new_in_context_messages.extend(persisted_messages[new_message_idx:])
@@ -560,7 +610,7 @@ class LettaAgent(BaseAgent):
560
610
  provider_trace_create=ProviderTraceCreate(
561
611
  request_json=request_data,
562
612
  response_json=response_data,
563
- step_id=step_id,
613
+ step_id=step_id, # Use original step_id for telemetry
564
614
  organization_id=self.actor.organization_id,
565
615
  ),
566
616
  )
@@ -584,54 +634,56 @@ class LettaAgent(BaseAgent):
584
634
 
585
635
  # Update step if it needs to be updated
586
636
  finally:
587
- if settings.track_stop_reason:
588
- if step_progression == StepProgression.FINISHED and should_continue:
589
- continue
590
-
591
- self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id)
592
- self.logger.info("Running final update. Step Progression: %s", step_progression)
593
- try:
594
- if step_progression == StepProgression.FINISHED and not should_continue:
595
- if stop_reason is None:
596
- stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
597
- await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason)
598
- break
637
+ if step_progression == StepProgression.FINISHED and should_continue:
638
+ continue
599
639
 
600
- if step_progression < StepProgression.STEP_LOGGED:
601
- await self.step_manager.log_step_async(
640
+ self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id)
641
+ self.logger.info("Running final update. Step Progression: %s", step_progression)
642
+ try:
643
+ if step_progression == StepProgression.FINISHED and not should_continue:
644
+ # Successfully completed - update with final usage and stop reason
645
+ if stop_reason is None:
646
+ stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
647
+ if logged_step:
648
+ await self.step_manager.update_step_success_async(self.actor, step_id, usage, stop_reason)
649
+ break
650
+
651
+ # Handle error cases
652
+ if step_progression < StepProgression.STEP_LOGGED:
653
+ # Error occurred before step was fully logged
654
+ import traceback
655
+
656
+ if logged_step:
657
+ await self.step_manager.update_step_error_async(
602
658
  actor=self.actor,
603
- agent_id=agent_state.id,
604
- provider_name=agent_state.llm_config.model_endpoint_type,
605
- provider_category=agent_state.llm_config.provider_category or "base",
606
- model=agent_state.llm_config.model,
607
- model_endpoint=agent_state.llm_config.model_endpoint,
608
- context_window_limit=agent_state.llm_config.context_window,
609
- usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0),
610
- provider_id=None,
611
- job_id=self.current_run_id if self.current_run_id else None,
612
- step_id=step_id,
613
- project_id=agent_state.project_id,
659
+ step_id=step_id, # Use original step_id for telemetry
660
+ error_type=type(e).__name__ if "e" in locals() else "Unknown",
661
+ error_message=str(e) if "e" in locals() else "Unknown error",
662
+ error_traceback=traceback.format_exc(),
614
663
  stop_reason=stop_reason,
615
664
  )
616
- if step_progression <= StepProgression.RESPONSE_RECEIVED:
617
- # TODO (cliandy): persist response if we get it back
618
- if settings.track_errored_messages:
619
- for message in initial_messages:
620
- message.is_err = True
621
- message.step_id = step_id
622
- await self.message_manager.create_many_messages_async(initial_messages, actor=self.actor)
623
- elif step_progression <= StepProgression.LOGGED_TRACE:
624
- if stop_reason is None:
625
- self.logger.error("Error in step after logging step")
626
- stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
665
+
666
+ if step_progression <= StepProgression.RESPONSE_RECEIVED:
667
+ # TODO (cliandy): persist response if we get it back
668
+ if settings.track_errored_messages:
669
+ for message in initial_messages:
670
+ message.is_err = True
671
+ message.step_id = effective_step_id
672
+ await self.message_manager.create_many_messages_async(initial_messages, actor=self.actor)
673
+ elif step_progression <= StepProgression.LOGGED_TRACE:
674
+ if stop_reason is None:
675
+ self.logger.error("Error in step after logging step")
676
+ stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
677
+ if logged_step:
627
678
  await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason)
628
- else:
629
- self.logger.error("Invalid StepProgression value")
679
+ else:
680
+ self.logger.error("Invalid StepProgression value")
630
681
 
682
+ if settings.track_stop_reason:
631
683
  await self._log_request(request_start_timestamp_ns, request_span)
632
684
 
633
- except Exception as e:
634
- self.logger.error("Failed to update step: %s", e)
685
+ except Exception as e:
686
+ self.logger.error("Failed to update step: %s", e)
635
687
 
636
688
  if not should_continue:
637
689
  break
@@ -717,6 +769,26 @@ class LettaAgent(BaseAgent):
717
769
 
718
770
  step_progression = StepProgression.START
719
771
  should_continue = False
772
+
773
+ # Create step early with PENDING status
774
+ logged_step = await self.step_manager.log_step_async(
775
+ actor=self.actor,
776
+ agent_id=agent_state.id,
777
+ provider_name=agent_state.llm_config.model_endpoint_type,
778
+ provider_category=agent_state.llm_config.provider_category or "base",
779
+ model=agent_state.llm_config.model,
780
+ model_endpoint=agent_state.llm_config.model_endpoint,
781
+ context_window_limit=agent_state.llm_config.context_window,
782
+ usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0),
783
+ provider_id=None,
784
+ job_id=self.current_run_id if self.current_run_id else None,
785
+ step_id=step_id,
786
+ project_id=agent_state.project_id,
787
+ status=StepStatus.PENDING,
788
+ )
789
+ # Only use step_id in messages if step was actually created
790
+ effective_step_id = step_id if logged_step else None
791
+
720
792
  try:
721
793
  (
722
794
  request_data,
@@ -827,13 +899,26 @@ class LettaAgent(BaseAgent):
827
899
  ),
828
900
  reasoning_content=reasoning_content,
829
901
  pre_computed_assistant_message_id=interface.letta_message_id,
830
- step_id=step_id,
902
+ step_id=effective_step_id,
831
903
  initial_messages=initial_messages,
832
904
  agent_step_span=agent_step_span,
833
905
  is_final_step=(i == max_steps - 1),
834
906
  )
835
907
  step_progression = StepProgression.STEP_LOGGED
836
908
 
909
+ # Update step with actual usage now that we have it (if step was created)
910
+ if logged_step:
911
+ await self.step_manager.update_step_success_async(
912
+ self.actor,
913
+ step_id,
914
+ UsageStatistics(
915
+ completion_tokens=usage.completion_tokens,
916
+ prompt_tokens=usage.prompt_tokens,
917
+ total_tokens=usage.total_tokens,
918
+ ),
919
+ stop_reason,
920
+ )
921
+
837
922
  new_message_idx = len(initial_messages) if initial_messages else 0
838
923
  self.response_messages.extend(persisted_messages[new_message_idx:])
839
924
  new_in_context_messages.extend(persisted_messages[new_message_idx:])
@@ -872,7 +957,7 @@ class LettaAgent(BaseAgent):
872
957
  "output_tokens": usage.completion_tokens,
873
958
  },
874
959
  },
875
- step_id=step_id,
960
+ step_id=step_id, # Use original step_id for telemetry
876
961
  organization_id=self.actor.organization_id,
877
962
  ),
878
963
  )
@@ -907,54 +992,57 @@ class LettaAgent(BaseAgent):
907
992
 
908
993
  # Update step if it needs to be updated
909
994
  finally:
910
- if settings.track_stop_reason:
911
- if step_progression == StepProgression.FINISHED and should_continue:
912
- continue
913
-
914
- self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id)
915
- self.logger.info("Running final update. Step Progression: %s", step_progression)
916
- try:
917
- if step_progression == StepProgression.FINISHED and not should_continue:
918
- if stop_reason is None:
919
- stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
995
+ if step_progression == StepProgression.FINISHED and should_continue:
996
+ continue
997
+
998
+ self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id)
999
+ self.logger.info("Running final update. Step Progression: %s", step_progression)
1000
+ try:
1001
+ if step_progression == StepProgression.FINISHED and not should_continue:
1002
+ # Successfully completed - update with final usage and stop reason
1003
+ if stop_reason is None:
1004
+ stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value)
1005
+ # Note: step already updated with success status after _handle_ai_response
1006
+ if logged_step:
920
1007
  await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason)
921
- break
1008
+ break
1009
+
1010
+ # Handle error cases
1011
+ if step_progression < StepProgression.STEP_LOGGED:
1012
+ # Error occurred before step was fully logged
1013
+ import traceback
922
1014
 
923
- if step_progression < StepProgression.STEP_LOGGED:
924
- await self.step_manager.log_step_async(
1015
+ if logged_step:
1016
+ await self.step_manager.update_step_error_async(
925
1017
  actor=self.actor,
926
- agent_id=agent_state.id,
927
- provider_name=agent_state.llm_config.model_endpoint_type,
928
- provider_category=agent_state.llm_config.provider_category or "base",
929
- model=agent_state.llm_config.model,
930
- model_endpoint=agent_state.llm_config.model_endpoint,
931
- context_window_limit=agent_state.llm_config.context_window,
932
- usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0),
933
- provider_id=None,
934
- job_id=self.current_run_id if self.current_run_id else None,
935
- step_id=step_id,
936
- project_id=agent_state.project_id,
1018
+ step_id=step_id, # Use original step_id for telemetry
1019
+ error_type=type(e).__name__ if "e" in locals() else "Unknown",
1020
+ error_message=str(e) if "e" in locals() else "Unknown error",
1021
+ error_traceback=traceback.format_exc(),
937
1022
  stop_reason=stop_reason,
938
1023
  )
939
- if step_progression <= StepProgression.STREAM_RECEIVED:
940
- if first_chunk and settings.track_errored_messages:
941
- for message in initial_messages:
942
- message.is_err = True
943
- message.step_id = step_id
944
- await self.message_manager.create_many_messages_async(initial_messages, actor=self.actor)
945
- elif step_progression <= StepProgression.LOGGED_TRACE:
946
- if stop_reason is None:
947
- self.logger.error("Error in step after logging step")
948
- stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
1024
+
1025
+ if step_progression <= StepProgression.STREAM_RECEIVED:
1026
+ if first_chunk and settings.track_errored_messages:
1027
+ for message in initial_messages:
1028
+ message.is_err = True
1029
+ message.step_id = effective_step_id
1030
+ await self.message_manager.create_many_messages_async(initial_messages, actor=self.actor)
1031
+ elif step_progression <= StepProgression.LOGGED_TRACE:
1032
+ if stop_reason is None:
1033
+ self.logger.error("Error in step after logging step")
1034
+ stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value)
1035
+ if logged_step:
949
1036
  await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason)
950
- else:
951
- self.logger.error("Invalid StepProgression value")
1037
+ else:
1038
+ self.logger.error("Invalid StepProgression value")
952
1039
 
953
- # Do tracking for failure cases. Can consolidate with success conditions later.
1040
+ # Do tracking for failure cases. Can consolidate with success conditions later.
1041
+ if settings.track_stop_reason:
954
1042
  await self._log_request(request_start_timestamp_ns, request_span)
955
1043
 
956
- except Exception as e:
957
- self.logger.error("Failed to update step: %s", e)
1044
+ except Exception as e:
1045
+ self.logger.error("Failed to update step: %s", e)
958
1046
 
959
1047
  if not should_continue:
960
1048
  break
@@ -1315,23 +1403,7 @@ class LettaAgent(BaseAgent):
1315
1403
  is_final_step=is_final_step,
1316
1404
  )
1317
1405
 
1318
- # 5. Persist step + messages and propagate to jobs
1319
- logged_step = await self.step_manager.log_step_async(
1320
- actor=self.actor,
1321
- agent_id=agent_state.id,
1322
- provider_name=agent_state.llm_config.model_endpoint_type,
1323
- provider_category=agent_state.llm_config.provider_category or "base",
1324
- model=agent_state.llm_config.model,
1325
- model_endpoint=agent_state.llm_config.model_endpoint,
1326
- context_window_limit=agent_state.llm_config.context_window,
1327
- usage=usage,
1328
- provider_id=None,
1329
- job_id=run_id if run_id else self.current_run_id,
1330
- step_id=step_id,
1331
- project_id=agent_state.project_id,
1332
- stop_reason=stop_reason,
1333
- )
1334
-
1406
+ # 5. Create messages (step was already created at the beginning)
1335
1407
  tool_call_messages = create_letta_messages_from_llm_response(
1336
1408
  agent_id=agent_state.id,
1337
1409
  model=agent_state.llm_config.model,
@@ -1347,7 +1419,7 @@ class LettaAgent(BaseAgent):
1347
1419
  heartbeat_reason=heartbeat_reason,
1348
1420
  reasoning_content=reasoning_content,
1349
1421
  pre_computed_assistant_message_id=pre_computed_assistant_message_id,
1350
- step_id=logged_step.id if logged_step else None,
1422
+ step_id=step_id,
1351
1423
  )
1352
1424
 
1353
1425
  persisted_messages = await self.message_manager.create_many_messages_async(
@@ -191,14 +191,14 @@ SNIPPET_LINES: int = 4
191
191
 
192
192
 
193
193
  # Based off of: https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/edit.py?ref=musings.yasyf.com#L154
194
- def memory_replace(agent_state: "AgentState", label: str, old_str: str, new_str: Optional[str] = None) -> str: # type: ignore
194
+ def memory_replace(agent_state: "AgentState", label: str, old_str: str, new_str: str) -> str: # type: ignore
195
195
  """
196
196
  The memory_replace command allows you to replace a specific string in a memory block with a new string. This is used for making precise edits.
197
197
 
198
198
  Args:
199
199
  label (str): Section of the memory to be edited, identified by its label.
200
200
  old_str (str): The text to replace (must match exactly, including whitespace and indentation).
201
- new_str (Optional[str]): The new text to insert in place of the old text. Omit this argument to delete the old_str.
201
+ new_str (str): The new text to insert in place of the old text. Do not include line number prefixes.
202
202
 
203
203
  Returns:
204
204
  str: The success message
@@ -43,24 +43,37 @@ async def grep_files(
43
43
  agent_state: "AgentState",
44
44
  pattern: str,
45
45
  include: Optional[str] = None,
46
- context_lines: Optional[int] = 3,
46
+ context_lines: Optional[int] = 1,
47
+ offset: Optional[int] = None,
47
48
  ) -> str:
48
49
  """
49
50
  Searches file contents for pattern matches with surrounding context.
50
51
 
51
- Ideal for:
52
- - Finding specific code elements (variables, functions, keywords)
53
- - Locating error messages or specific text across multiple files
54
- - Examining code in context to understand usage patterns
52
+ Results are paginated - shows 20 matches per call. The response includes:
53
+ - A summary of total matches and which files contain them
54
+ - The current page of matches (20 at a time)
55
+ - Instructions for viewing more matches using the offset parameter
56
+
57
+ Example usage:
58
+ First call: grep_files(pattern="TODO")
59
+ Next call: grep_files(pattern="TODO", offset=20) # Shows matches 21-40
60
+
61
+ Returns search results containing:
62
+ - Summary with total match count and file distribution
63
+ - List of files with match counts per file
64
+ - Current page of matches (up to 20)
65
+ - Navigation hint for next page if more matches exist
55
66
 
56
67
  Args:
57
68
  pattern (str): Keyword or regex pattern to search within file contents.
58
69
  include (Optional[str]): Optional keyword or regex pattern to filter filenames to include in the search.
59
70
  context_lines (Optional[int]): Number of lines of context to show before and after each match.
60
- Equivalent to `-C` in grep_files. Defaults to 3.
61
-
62
- Returns:
63
- str: Matching lines with optional surrounding context or a summary output.
71
+ Equivalent to `-C` in grep_files. Defaults to 1.
72
+ offset (Optional[int]): Number of matches to skip before showing results. Used for pagination.
73
+ For example, offset=20 shows matches starting from the 21st match.
74
+ Use offset=0 (or omit) for first page, offset=20 for second page,
75
+ offset=40 for third page, etc. The tool will tell you the exact
76
+ offset to use for the next page.
64
77
  """
65
78
  raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
66
79