From 257df32d4fc68f57f4d646f3ed8fdd4b8b9fbe57 Mon Sep 17 00:00:00 2001 From: Ratansairohith Date: Sat, 4 Apr 2026 09:11:21 -0700 Subject: [PATCH 1/2] fix(streaming): log warning when tool input JSON is malformed Malformed tool input JSON was silently replaced with an empty dict, with no logging. The tool then ran with empty arguments and the model had no indication its JSON was invalid. Added a logger.warning() call before the fallback so operators can detect and diagnose this in production. Fixes #2051 --- src/strands/event_loop/streaming.py | 5 +++++ tests/strands/event_loop/test_streaming.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/strands/event_loop/streaming.py b/src/strands/event_loop/streaming.py index 0a11611354..f5e0e8b023 100644 --- a/src/strands/event_loop/streaming.py +++ b/src/strands/event_loop/streaming.py @@ -279,6 +279,11 @@ def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]: try: current_tool_use["input"] = json.loads(current_tool_use["input"]) except ValueError: + logger.warning( + "Failed to parse tool input JSON for '%s': %s", + current_tool_use.get("name", "unknown"), + current_tool_use["input"][:200] if isinstance(current_tool_use.get("input"), str) else "", + ) current_tool_use["input"] = {} tool_use_id = current_tool_use["toolUseId"] diff --git a/tests/strands/event_loop/test_streaming.py b/tests/strands/event_loop/test_streaming.py index 93f8d95f82..ba693485d3 100644 --- a/tests/strands/event_loop/test_streaming.py +++ b/tests/strands/event_loop/test_streaming.py @@ -366,6 +366,25 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, event_type, s "redactedContent": b"", }, ), + # Tool Use - Malformed input JSON + ( + { + "content": [], + "current_tool_use": {"toolUseId": "123", "name": "test", "input": "{invalid json}"}, + "text": "", + "reasoningText": "", + "citationsContent": [], + "redactedContent": b"", + }, + { + "content": [{"toolUse": {"toolUseId": "123", "name": "test", "input": {}}}], + "current_tool_use": {}, + "text": "", + "reasoningText": "", + "citationsContent": [], + "redactedContent": b"", + }, + ), # Text ( { From 009fba5a11fd99d5003bd3824f4d24d8eb5c4a4a Mon Sep 17 00:00:00 2001 From: Ratansairohith Date: Wed, 29 Apr 2026 11:40:26 -0700 Subject: [PATCH 2/2] fix(streaming): apply structured logging format and add warning test - Switch warning to project's field= | message convention per AGENTS.md - Add dedicated test that asserts logger.warning is emitted with the expected tool_name and raw_input on malformed JSON Address auto-review feedback on #2054. --- src/strands/event_loop/streaming.py | 2 +- tests/strands/event_loop/test_streaming.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/strands/event_loop/streaming.py b/src/strands/event_loop/streaming.py index f5e0e8b023..146e899f60 100644 --- a/src/strands/event_loop/streaming.py +++ b/src/strands/event_loop/streaming.py @@ -280,7 +280,7 @@ def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]: current_tool_use["input"] = json.loads(current_tool_use["input"]) except ValueError: logger.warning( - "Failed to parse tool input JSON for '%s': %s", + "tool_name=<%s>, raw_input=<%s> | failed to parse tool input json, defaulting to empty dict", current_tool_use.get("name", "unknown"), current_tool_use["input"][:200] if isinstance(current_tool_use.get("input"), str) else "", ) diff --git a/tests/strands/event_loop/test_streaming.py b/tests/strands/event_loop/test_streaming.py index ba693485d3..57ef8a2e57 100644 --- a/tests/strands/event_loop/test_streaming.py +++ b/tests/strands/event_loop/test_streaming.py @@ -546,6 +546,25 @@ def test_handle_content_block_stop(state, exp_updated_state): assert tru_updated_state == exp_updated_state +@unittest.mock.patch("strands.event_loop.streaming.logger") +def test_handle_content_block_stop_logs_warning_on_malformed_json(mock_logger): + state = { + "content": [], + "current_tool_use": {"toolUseId": "123", "name": "test_tool", "input": "{invalid json}"}, + "text": "", + "reasoningText": "", + "citationsContent": [], + "redactedContent": b"", + } + + strands.event_loop.streaming.handle_content_block_stop(state) + + mock_logger.warning.assert_called_once() + call_args = mock_logger.warning.call_args + assert "test_tool" in str(call_args) + assert "{invalid json}" in str(call_args) + + def test_handle_message_stop(): event: MessageStopEvent = {"stopReason": "end_turn"}