fix: various fixes for runs (#5907)

* Fix agent loop continuing after cancellation in letta_agent_v3

Bug: When a run is cancelled, _check_run_cancellation() sets
self.should_continue=False and returns early from _step(), but the outer
for loop (line 245) continues to the next iteration, executing subsequent
steps even though cancellation was requested.

Symptom: User hits cancel during step 1, backend marks run as cancelled,
but agent continues executing steps 2, 3, etc.

Root cause: After the 'async for chunk in response' loop completes (line 255),
there was no check of self.should_continue before continuing to the next
iteration of the outer step loop.

Fix: Added 'if not self.should_continue: break' check after the inner loop
to exit the outer step loop when cancellation is detected. This makes v3
consistent with v2 which already had this check (line 306-307).

🐾 Generated with [Letta Code](https://letta.com)

Co-authored-by: Letta <noreply@letta.com>

* add integration tests

* passing tests

* fix: minor patches

* undo

---------

Co-authored-by: cpacker <packercharles@gmail.com>
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Sarah Wooders
2025-11-03 03:49:25 +00:00
committed by Caren Thomas
parent a6077f3927
commit ac599145bb

View File

@@ -283,8 +283,8 @@ class LettaAgentV3(LettaAgentV2):
# Clear to avoid duplication in next iteration
self.response_messages = []
if not self.should_continue:
break
# if not self.should_continue:
# break
input_messages_to_persist = []
@@ -394,18 +394,24 @@ class LettaAgentV3(LettaAgentV2):
# Get tool calls that are pending
backfill_tool_call_id = approval_request.tool_calls[0].id # legacy case
approved_tool_call_ids = {
backfill_tool_call_id if a.tool_call_id.startswith("message-") else a.tool_call_id
for a in approval_response.approvals
if isinstance(a, ApprovalReturn) and a.approve
}
if approval_response.approvals:
approved_tool_call_ids = {
backfill_tool_call_id if a.tool_call_id.startswith("message-") else a.tool_call_id
for a in approval_response.approvals
if isinstance(a, ApprovalReturn) and a.approve
}
else:
approved_tool_call_ids = {}
tool_calls = [tool_call for tool_call in approval_request.tool_calls if tool_call.id in approved_tool_call_ids]
pending_tool_call_message = _maybe_get_pending_tool_call_message(messages)
if pending_tool_call_message:
tool_calls.extend(pending_tool_call_message.tool_calls)
# Get tool calls that were denied
denies = {d.tool_call_id: d for d in approval_response.approvals if isinstance(d, ApprovalReturn) and not d.approve}
if approval_response.approvals:
denies = {d.tool_call_id: d for d in approval_response.approvals if isinstance(d, ApprovalReturn) and not d.approve}
else:
denies = {}
tool_call_denials = [
ToolCallDenial(**t.model_dump(), reason=denies.get(t.id).reason) for t in approval_request.tool_calls if t.id in denies
]