From b1041c2acdc1e1664ab4a4ff412190733a3df22c Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Fri, 10 Apr 2026 10:51:31 +1000 Subject: [PATCH] fix: preserve output type in FunctionSpanData.export() FunctionSpanData.export() applied str(self.output) if self.output else None, which corrupted outputs in two ways: 1. Dict/list outputs were converted to Python repr strings (single quotes, Python booleans) instead of remaining as dicts that json.dumps can serialize correctly. 2. Falsy but valid outputs (0, False, empty string, empty list) were silently replaced with None. This is inconsistent with GenerationSpanData.export(), which passes self.output through directly. The fix removes the str() conversion to match the sibling pattern. Updated MCP tracing snapshots accordingly and added unit tests covering dict, string, None, falsy, list, and numeric output values. --- src/agents/tracing/span_data.py | 2 +- tests/mcp/test_mcp_tracing.py | 6 +-- tests/tracing/test_span_data.py | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 tests/tracing/test_span_data.py diff --git a/src/agents/tracing/span_data.py b/src/agents/tracing/span_data.py index cb3e8491d3..6f440fbd39 100644 --- a/src/agents/tracing/span_data.py +++ b/src/agents/tracing/span_data.py @@ -88,7 +88,7 @@ def export(self) -> dict[str, Any]: "type": self.type, "name": self.name, "input": self.input, - "output": str(self.output) if self.output else None, + "output": self.output, "mcp_data": self.mcp_data, } diff --git a/tests/mcp/test_mcp_tracing.py b/tests/mcp/test_mcp_tracing.py index 9cb3454b1b..11b801419a 100644 --- a/tests/mcp/test_mcp_tracing.py +++ b/tests/mcp/test_mcp_tracing.py @@ -62,7 +62,7 @@ async def test_mcp_tracing(): "data": { "name": "test_tool_1", "input": "", - "output": "{'type': 'text', 'text': 'result_test_tool_1_{}'}", # noqa: E501 + "output": {"type": "text", "text": "result_test_tool_1_{}"}, # noqa: E501 "mcp_data": {"server": "fake_mcp_server"}, }, }, @@ -133,7 +133,7 @@ async def test_mcp_tracing(): "data": { "name": "test_tool_2", "input": "", - "output": "{'type': 'text', 'text': 'result_test_tool_2_{}'}", # noqa: E501 + "output": {"type": "text", "text": "result_test_tool_2_{}"}, # noqa: E501 "mcp_data": {"server": "fake_mcp_server"}, }, }, @@ -197,7 +197,7 @@ async def test_mcp_tracing(): "data": { "name": "test_tool_3", "input": "", - "output": "{'type': 'text', 'text': 'result_test_tool_3_{}'}", # noqa: E501 + "output": {"type": "text", "text": "result_test_tool_3_{}"}, # noqa: E501 "mcp_data": {"server": "fake_mcp_server"}, }, }, diff --git a/tests/tracing/test_span_data.py b/tests/tracing/test_span_data.py new file mode 100644 index 0000000000..2b4cfe5704 --- /dev/null +++ b/tests/tracing/test_span_data.py @@ -0,0 +1,67 @@ +"""Tests for span data export methods.""" + +from __future__ import annotations + +import pytest + +from agents.tracing.span_data import FunctionSpanData + + +class TestFunctionSpanDataExport: + """FunctionSpanData.export() must preserve output values faithfully.""" + + def test_dict_output_preserved_as_dict(self) -> None: + """Dict outputs should stay as dicts, not be converted to Python repr strings.""" + span = FunctionSpanData(name="my_tool", input="query", output={"key": "value", "n": 42}) + exported = span.export() + assert exported["output"] == {"key": "value", "n": 42} + assert isinstance(exported["output"], dict) + + def test_string_output_preserved(self) -> None: + span = FunctionSpanData(name="my_tool", input="query", output="hello world") + exported = span.export() + assert exported["output"] == "hello world" + + def test_none_output_preserved(self) -> None: + span = FunctionSpanData(name="my_tool", input="query", output=None) + exported = span.export() + assert exported["output"] is None + + @pytest.mark.parametrize( + "output", + [0, False, "", []], + ids=["zero", "false", "empty_str", "empty_list"], + ) + def test_falsy_output_not_converted_to_none(self, output: object) -> None: + """Falsy but valid outputs (0, False, '', []) must not become None.""" + span = FunctionSpanData(name="my_tool", input="query", output=output) + exported = span.export() + assert exported["output"] is not None + assert exported["output"] == output + + def test_list_output_preserved(self) -> None: + span = FunctionSpanData(name="my_tool", input="query", output=[1, 2, 3]) + exported = span.export() + assert exported["output"] == [1, 2, 3] + assert isinstance(exported["output"], list) + + def test_numeric_output_preserved(self) -> None: + span = FunctionSpanData(name="my_tool", input="query", output=42) + exported = span.export() + assert exported["output"] == 42 + + def test_export_includes_all_fields(self) -> None: + span = FunctionSpanData( + name="my_tool", + input="query", + output="result", + mcp_data={"server": "test"}, + ) + exported = span.export() + assert exported == { + "type": "function", + "name": "my_tool", + "input": "query", + "output": "result", + "mcp_data": {"server": "test"}, + }