From 9b8620b4e2a527bd538c7f6f67baa2448f038b25 Mon Sep 17 00:00:00 2001 From: atian8179 Date: Sat, 7 Mar 2026 00:04:59 +0800 Subject: [PATCH] fix: override end_turn stop reason when streaming response contains toolUse Some models (e.g., Sonnet 4.x on Bedrock) return end_turn as the stop reason even when the response contains toolUse content blocks. In streaming mode, the stop_reason from messageStop is used directly without checking for toolUse blocks, causing the event loop to skip tool execution. Add a post-stream check: if stop_reason is end_turn but the assembled message contains toolUse blocks, override to tool_use. This matches the existing non-streaming behavior in event_loop.py. Fixes #1810 --- src/strands/event_loop/streaming.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/strands/event_loop/streaming.py b/src/strands/event_loop/streaming.py index b157f740e..7644a2f56 100644 --- a/src/strands/event_loop/streaming.py +++ b/src/strands/event_loop/streaming.py @@ -419,6 +419,17 @@ async def process_stream( elif "redactContent" in chunk: handle_redact_content(chunk["redactContent"], state) + # Override stop_reason when the model returns "end_turn" but the response + # contains toolUse blocks. Some models (e.g., Sonnet 4.x) can return + # end_turn as stop_reason even when tool calls are present, which prevents + # the event loop from processing those tool calls. + # See: https://github.com/strands-agents/sdk-python/issues/1810 + if stop_reason == "end_turn": + content = state["message"].get("content", []) + if any("toolUse" in item for item in content): + logger.debug("stop_reason override: end_turn -> tool_use (response contains toolUse blocks)") + stop_reason = "tool_use" + yield ModelStopReason(stop_reason=stop_reason, message=state["message"], usage=usage, metrics=metrics)