From 1e613b72999909c3ed818a719a4a4e1126843ca1 Mon Sep 17 00:00:00 2001 From: zb-zhoufengen Date: Tue, 12 May 2026 13:30:05 +0800 Subject: [PATCH] fix: guard against None text in text_message_output and add output guardrail count to RunErrorDetails pretty-print - ItemHelpers.text_message_output: apply the same `or ""` guard that extract_text already uses. Provider gateways (e.g. LiteLLM) and model_construct paths during streaming can surface None for ResponseOutputText.text; without the guard the concatenation raises TypeError. - pretty_print_run_error_details: add the missing output_guardrail_results line so RunErrorDetails.__str__ is consistent with pretty_print_result and pretty_print_run_result_streaming, which both report both guardrail counts. - Add tests/utils/test_pretty_print_and_items.py covering both fixes. - Update the existing inline snapshot in tests/test_pretty_print.py. --- src/agents/items.py | 2 +- src/agents/util/_pretty_print.py | 1 + tests/test_pretty_print.py | 1 + tests/utils/test_pretty_print_and_items.py | 64 ++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/utils/test_pretty_print_and_items.py diff --git a/src/agents/items.py b/src/agents/items.py index 3b62ee6f98..50a017c221 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -764,7 +764,7 @@ def text_message_output(cls, message: MessageOutputItem) -> str: text = "" for item in message.raw_item.content: if isinstance(item, ResponseOutputText): - text += item.text + text += item.text or "" return text @classmethod diff --git a/src/agents/util/_pretty_print.py b/src/agents/util/_pretty_print.py index 29df3562e9..51fcb9b677 100644 --- a/src/agents/util/_pretty_print.py +++ b/src/agents/util/_pretty_print.py @@ -45,6 +45,7 @@ def pretty_print_run_error_details(result: "RunErrorDetails") -> str: output += f"\n- {len(result.new_items)} new item(s)" output += f"\n- {len(result.raw_responses)} raw response(s)" output += f"\n- {len(result.input_guardrail_results)} input guardrail result(s)" + output += f"\n- {len(result.output_guardrail_results)} output guardrail result(s)" output += "\n(See `RunErrorDetails` for more details)" return output diff --git a/tests/test_pretty_print.py b/tests/test_pretty_print.py index 79327cfb92..5d76e0cc0a 100644 --- a/tests/test_pretty_print.py +++ b/tests/test_pretty_print.py @@ -83,6 +83,7 @@ def test_pretty_run_error_details(): - 0 new item(s) - 0 raw response(s) - 0 input guardrail result(s) +- 0 output guardrail result(s) (See `RunErrorDetails` for more details)\ """) diff --git a/tests/utils/test_pretty_print_and_items.py b/tests/utils/test_pretty_print_and_items.py new file mode 100644 index 0000000000..34939df368 --- /dev/null +++ b/tests/utils/test_pretty_print_and_items.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from openai.types.responses import ResponseOutputMessage, ResponseOutputText + +from agents import Agent +from agents.exceptions import RunErrorDetails +from agents.items import ItemHelpers, MessageOutputItem +from agents.util._pretty_print import pretty_print_run_error_details + + +def _make_message_item(text: str | None) -> MessageOutputItem: + msg = ResponseOutputMessage.model_construct( + id="msg_1", + role="assistant", + status="completed", + content=[ResponseOutputText.model_construct(type="output_text", text=text, annotations=[])], + ) + agent = Agent(name="test") + return MessageOutputItem(agent=agent, raw_item=msg) + + +def test_text_message_output_returns_empty_string_for_none_text(): + """text_message_output must not crash when a content item has text=None.""" + item = _make_message_item(None) + assert ItemHelpers.text_message_output(item) == "" + + +def test_text_message_output_returns_text_normally(): + item = _make_message_item("hello") + assert ItemHelpers.text_message_output(item) == "hello" + + +def test_text_message_outputs_handles_none_text_across_items(): + """text_message_outputs must tolerate None text in any item.""" + from agents.items import RunItem + + items: list[RunItem] = [_make_message_item(None), _make_message_item("world")] + assert ItemHelpers.text_message_outputs(items) == "world" + + +def _make_run_error_details(n_input: int = 0, n_output: int = 0) -> RunErrorDetails: + return RunErrorDetails( + input="hi", + new_items=[], + raw_responses=[], + last_agent=Agent(name="test"), + context_wrapper=None, # type: ignore[arg-type] + input_guardrail_results=[None] * n_input, # type: ignore[list-item] + output_guardrail_results=[None] * n_output, # type: ignore[list-item] + ) + + +def test_pretty_print_run_error_details_includes_output_guardrail_count(): + """pretty_print_run_error_details must report output_guardrail_results like its siblings.""" + details = _make_run_error_details(n_input=1, n_output=2) + text = pretty_print_run_error_details(details) + assert "1 input guardrail result(s)" in text + assert "2 output guardrail result(s)" in text + + +def test_pretty_print_run_error_details_zero_output_guardrails(): + details = _make_run_error_details(n_input=0, n_output=0) + text = pretty_print_run_error_details(details) + assert "0 output guardrail result(s)" in text