-
Notifications
You must be signed in to change notification settings - Fork 3.4k
fix: preserve output type in FunctionSpanData.export() #2871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
| } | ||
|
Comment on lines
88
to
93
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"}, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing
self.outputthrough unmodified can break trace export when a tool returns a non-JSON-serializable object (for example a custom class ordatetime) and theBackendSpanExporteris configured with a non-defaultendpoint. In that configuration_should_sanitize_for_openai_tracing_api()is false, sohttpx.Client.post(..., json=payload)receives raw span data and raises during JSON encoding, whereas the previousstr(...)conversion kept function span payloads serializable. This makes tracing brittle for custom exporters even though the same tool output still works for run execution.Useful? React with 👍 / 👎.