From 205e3ea11f83824115d4df9c2acececfb0995fb6 Mon Sep 17 00:00:00 2001 From: yoppi Date: Tue, 17 Mar 2026 10:41:39 +0900 Subject: [PATCH] fix: handle None text in message content sanitization `_normalize_messages` and `remove_blank_messages_content_text` call `.strip()` on `item["text"]` without guarding against `None`. When a model provider (e.g. Gemini) returns a content block with `{"text": None}`, this causes `TypeError: expected string or bytes-like object, got 'NoneType'`. Add explicit `None` checks before calling `.strip()`: - With tool_use: None text items are removed (same as blank text) - Without tool_use: None text is replaced with "[blank text]" (same as blank) --- src/strands/event_loop/streaming.py | 20 ++++++++++++-------- tests/strands/event_loop/test_streaming.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/strands/event_loop/streaming.py b/src/strands/event_loop/streaming.py index b7d85ca30..21c0009c1 100644 --- a/src/strands/event_loop/streaming.py +++ b/src/strands/event_loop/streaming.py @@ -82,15 +82,17 @@ def _normalize_messages(messages: Messages) -> Messages: replaced_tool_names = True if has_tool_use: - # Remove blank 'text' items for assistant messages + # Remove blank or None 'text' items for assistant messages before_len = len(content) - content[:] = [item for item in content if "text" not in item or item["text"].strip()] + content[:] = [ + item for item in content if "text" not in item or (item["text"] is not None and item["text"].strip()) + ] if not removed_blank_message_content_text and before_len != len(content): removed_blank_message_content_text = True else: - # Replace blank 'text' with '[blank text]' for assistant messages + # Replace blank or None 'text' with '[blank text]' for assistant messages for item in content: - if "text" in item and not item["text"].strip(): + if "text" in item and (item["text"] is None or not item["text"].strip()): replaced_blank_message_content_text = True item["text"] = "[blank text]" @@ -136,15 +138,17 @@ def remove_blank_messages_content_text(messages: Messages) -> Messages: continue if has_tool_use: - # Remove blank 'text' items for assistant messages + # Remove blank or None 'text' items for assistant messages before_len = len(content) - content[:] = [item for item in content if "text" not in item or item["text"].strip()] + content[:] = [ + item for item in content if "text" not in item or (item["text"] is not None and item["text"].strip()) + ] if not removed_blank_message_content_text and before_len != len(content): removed_blank_message_content_text = True else: - # Replace blank 'text' with '[blank text]' for assistant messages + # Replace blank or None 'text' with '[blank text]' for assistant messages for item in content: - if "text" in item and not item["text"].strip(): + if "text" in item and (item["text"] is None or not item["text"].strip()): replaced_blank_message_content_text = True item["text"] = "[blank text]" diff --git a/tests/strands/event_loop/test_streaming.py b/tests/strands/event_loop/test_streaming.py index 6d376450a..80eea93bd 100644 --- a/tests/strands/event_loop/test_streaming.py +++ b/tests/strands/event_loop/test_streaming.py @@ -100,6 +100,24 @@ def test_remove_blank_messages_content_text(messages, exp_result): ], id="missing tool name", ), + pytest.param( + [ + {"role": "assistant", "content": [{"text": None}, {"toolUse": {"name": "a_name"}}]}, + ], + [ + {"role": "assistant", "content": [{"toolUse": {"name": "a_name"}}]}, + ], + id="none text with tool use", + ), + pytest.param( + [ + {"role": "assistant", "content": [{"text": None}]}, + ], + [ + {"role": "assistant", "content": [{"text": "[blank text]"}]}, + ], + id="none text without tool use", + ), ], ) def test_normalize_blank_messages_content_text(messages, exp_result):