From ac599145bb5b7fa853b9231a690a39f7cd077dc4 Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Mon, 3 Nov 2025 03:49:25 +0000 Subject: [PATCH] fix: various fixes for runs (#5907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * add integration tests * passing tests * fix: minor patches * undo --------- Co-authored-by: cpacker Co-authored-by: Letta --- letta/agents/letta_agent_v3.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/letta/agents/letta_agent_v3.py b/letta/agents/letta_agent_v3.py index 5da5f566..3e64228a 100644 --- a/letta/agents/letta_agent_v3.py +++ b/letta/agents/letta_agent_v3.py @@ -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 ]