From de2561db83cb3b6540d33779fcb399b846b7fc91 Mon Sep 17 00:00:00 2001 From: asekka Date: Tue, 27 Jan 2026 12:57:40 +0100 Subject: [PATCH] fix: emit ToolUseStreamEvent on contentBlockStart for thinking-phase tool calls When models with extended thinking execute tool calls during the reasoning phase, the tool use was never displayed to users because ToolUseStreamEvent was only emitted during contentBlockDelta events. Now emit a ToolUseStreamEvent immediately when contentBlockStart contains toolUse data, ensuring all tool executions are visible via the callback handler. Closes #1551 --- src/strands/event_loop/streaming.py | 5 +++ tests/strands/event_loop/test_streaming.py | 50 ++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/strands/event_loop/streaming.py b/src/strands/event_loop/streaming.py index 954633807..5f409e0a6 100644 --- a/src/strands/event_loop/streaming.py +++ b/src/strands/event_loop/streaming.py @@ -400,6 +400,11 @@ async def process_stream( state["message"] = handle_message_start(chunk["messageStart"], state["message"]) elif "contentBlockStart" in chunk: state["current_tool_use"] = handle_content_block_start(chunk["contentBlockStart"]) + if state["current_tool_use"]: + yield ToolUseStreamEvent( + delta={"toolUse": {"input": ""}}, + current_tool_use=state["current_tool_use"], + ) elif "contentBlockDelta" in chunk: state, typed_event = handle_content_block_delta(chunk["contentBlockDelta"], state) yield typed_event diff --git a/tests/strands/event_loop/test_streaming.py b/tests/strands/event_loop/test_streaming.py index b2cc152cb..bf147b68c 100644 --- a/tests/strands/event_loop/test_streaming.py +++ b/tests/strands/event_loop/test_streaming.py @@ -603,6 +603,19 @@ def test_extract_usage_metrics_empty_metadata(): }, }, }, + { + "current_tool_use": { + "input": {"key": "value"}, + "name": "test", + "toolUseId": "123", + }, + "delta": { + "toolUse": { + "input": "", + }, + }, + "type": "tool_use_stream", + }, { "event": { "contentBlockDelta": { @@ -616,9 +629,7 @@ def test_extract_usage_metrics_empty_metadata(): }, { "current_tool_use": { - "input": { - "key": "value", - }, + "input": {"key": "value"}, "name": "test", "toolUseId": "123", }, @@ -1257,6 +1268,39 @@ async def test_stream_messages_none_system_prompt_content(agenerator, alist): assert non_typed_events == [] +@pytest.mark.asyncio +async def test_process_stream_emits_tool_use_event_on_content_block_start(agenerator, alist): + """Test that a ToolUseStreamEvent is emitted on contentBlockStart with toolUse. + + This ensures tool calls are visible to users even when executed during the model's + thinking/reasoning phase before any contentBlockDelta events are received. + See: https://github.com/strands-agents/sdk-python/issues/1551 + """ + response = [ + {"messageStart": {"role": "assistant"}}, + { + "contentBlockStart": {"start": {"toolUse": {"toolUseId": "tool-1", "name": "get_user_info"}}}, + }, + {"contentBlockStop": {}}, + {"messageStop": {"stopReason": "tool_use"}}, + { + "metadata": { + "usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 1}, + "metrics": {"latencyMs": 1}, + } + }, + ] + + stream = strands.event_loop.streaming.process_stream(agenerator(response)) + events = await alist(stream) + + # Find the ToolUseStreamEvent - should be emitted right after contentBlockStart + tool_use_events = [e for e in events if e.get("type") == "tool_use_stream"] + assert len(tool_use_events) == 1 + assert tool_use_events[0]["current_tool_use"]["name"] == "get_user_info" + assert tool_use_events[0]["current_tool_use"]["toolUseId"] == "tool-1" + + @pytest.mark.asyncio async def test_stream_messages_normalizes_messages(agenerator, alist): mock_model = unittest.mock.MagicMock()