Skip to content

Commit b1041c2

Browse files
committed
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.
1 parent 86739b1 commit b1041c2

3 files changed

Lines changed: 71 additions & 4 deletions

File tree

src/agents/tracing/span_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def export(self) -> dict[str, Any]:
8888
"type": self.type,
8989
"name": self.name,
9090
"input": self.input,
91-
"output": str(self.output) if self.output else None,
91+
"output": self.output,
9292
"mcp_data": self.mcp_data,
9393
}
9494

tests/mcp/test_mcp_tracing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def test_mcp_tracing():
6262
"data": {
6363
"name": "test_tool_1",
6464
"input": "",
65-
"output": "{'type': 'text', 'text': 'result_test_tool_1_{}'}", # noqa: E501
65+
"output": {"type": "text", "text": "result_test_tool_1_{}"}, # noqa: E501
6666
"mcp_data": {"server": "fake_mcp_server"},
6767
},
6868
},
@@ -133,7 +133,7 @@ async def test_mcp_tracing():
133133
"data": {
134134
"name": "test_tool_2",
135135
"input": "",
136-
"output": "{'type': 'text', 'text': 'result_test_tool_2_{}'}", # noqa: E501
136+
"output": {"type": "text", "text": "result_test_tool_2_{}"}, # noqa: E501
137137
"mcp_data": {"server": "fake_mcp_server"},
138138
},
139139
},
@@ -197,7 +197,7 @@ async def test_mcp_tracing():
197197
"data": {
198198
"name": "test_tool_3",
199199
"input": "",
200-
"output": "{'type': 'text', 'text': 'result_test_tool_3_{}'}", # noqa: E501
200+
"output": {"type": "text", "text": "result_test_tool_3_{}"}, # noqa: E501
201201
"mcp_data": {"server": "fake_mcp_server"},
202202
},
203203
},

tests/tracing/test_span_data.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Tests for span data export methods."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from agents.tracing.span_data import FunctionSpanData
8+
9+
10+
class TestFunctionSpanDataExport:
11+
"""FunctionSpanData.export() must preserve output values faithfully."""
12+
13+
def test_dict_output_preserved_as_dict(self) -> None:
14+
"""Dict outputs should stay as dicts, not be converted to Python repr strings."""
15+
span = FunctionSpanData(name="my_tool", input="query", output={"key": "value", "n": 42})
16+
exported = span.export()
17+
assert exported["output"] == {"key": "value", "n": 42}
18+
assert isinstance(exported["output"], dict)
19+
20+
def test_string_output_preserved(self) -> None:
21+
span = FunctionSpanData(name="my_tool", input="query", output="hello world")
22+
exported = span.export()
23+
assert exported["output"] == "hello world"
24+
25+
def test_none_output_preserved(self) -> None:
26+
span = FunctionSpanData(name="my_tool", input="query", output=None)
27+
exported = span.export()
28+
assert exported["output"] is None
29+
30+
@pytest.mark.parametrize(
31+
"output",
32+
[0, False, "", []],
33+
ids=["zero", "false", "empty_str", "empty_list"],
34+
)
35+
def test_falsy_output_not_converted_to_none(self, output: object) -> None:
36+
"""Falsy but valid outputs (0, False, '', []) must not become None."""
37+
span = FunctionSpanData(name="my_tool", input="query", output=output)
38+
exported = span.export()
39+
assert exported["output"] is not None
40+
assert exported["output"] == output
41+
42+
def test_list_output_preserved(self) -> None:
43+
span = FunctionSpanData(name="my_tool", input="query", output=[1, 2, 3])
44+
exported = span.export()
45+
assert exported["output"] == [1, 2, 3]
46+
assert isinstance(exported["output"], list)
47+
48+
def test_numeric_output_preserved(self) -> None:
49+
span = FunctionSpanData(name="my_tool", input="query", output=42)
50+
exported = span.export()
51+
assert exported["output"] == 42
52+
53+
def test_export_includes_all_fields(self) -> None:
54+
span = FunctionSpanData(
55+
name="my_tool",
56+
input="query",
57+
output="result",
58+
mcp_data={"server": "test"},
59+
)
60+
exported = span.export()
61+
assert exported == {
62+
"type": "function",
63+
"name": "my_tool",
64+
"input": "query",
65+
"output": "result",
66+
"mcp_data": {"server": "test"},
67+
}

0 commit comments

Comments
 (0)