fix: coerce structured tool outputs for stop_on_first_tool - #3997
Conversation
Plain-text agents finalized via stop_on_first_tool/stop_at_tool_names were receiving Pydantic/dict __str__ representations for ToolOutputText and related structured returns. Reuse ItemHelpers structured conversion so text becomes the content string and non-text structured outputs become stable JSON.
|
Thanks for the contribution. I confirmed that ToolOutputText currently becomes its Pydantic representation when a default plain-text agent uses one of the built-in stop behaviors. However, the structured tool-output types define how results are sent back to the model; they do not define a public plain-text final-result serialization. The desired outcome is already supported by returning a string, using a custom ToolsToFinalOutputFunction to extract or serialize the result, or declaring a matching output_type when the structured value itself should be final. This change would additionally expose private Responses input_* wire shapes as public final-output JSON and reinterpret values from custom handlers. I am going to close this PR. We can revisit a narrower behavior if there is a concrete supported scenario where the existing approaches are insufficient. |
Summary
When
tool_use_behaviorisstop_on_first_toolorstop_at_tool_names, plain-text agents finalize through_maybe_finalize_from_tool_results, which previously calledstr(...)on the tool result. For structured returns such asToolOutputText/ToolOutputImage(and TypedDict equivalents), that produced Pydantic or dict__str__/__repr__values liketype='text' text='hello structured'instead of usable plain-text output.This pull request fixes that by coercing structured tool outputs through the existing
ItemHelpers._convert_tool_output_as_structuredpath:ToolOutputText/ text TypedDict → the text content stringint) → unchangedstr(...)behaviorAffected component:
src/agents/run_internal/turn_resolution.py(_maybe_finalize_from_tool_results)Problem:
stop_on_first_tool/stop_at_tool_namesturned structured tool returns into Pydantic/dict string representations for default plain-text agents.Minimal reproduction (FakeModel, no API key):
Root cause: Blind
str(final_output)for plain-text agents does not use the structured tool-output conversion already used for tool wire payloads.Why minimal: One shared helper in the finalization path used by both streamed and non-streamed runs. No public API changes. Does not change the rejected
None-preservation behavior from #3943 (plain-text agents still stringify non-structured values, includingNone→"None").Compatibility: Corrects buggy released behavior for structured tool finals on plain-text agents (
v0.19.0). Callers that depended on Pydantic/dict__str__text were relying on the bug.Non-goals: No change to custom
output_typeagents, approval/sticky-reject behavior, session persistence, or intentional empty-stringas_toolfallbacks.Test plan
tests/test_tool_use_behavior.pycovering:ToolOutputTextviastop_on_first_toolToolOutputTextDictviastop_on_first_toolToolOutputTextviastop_at_tool_namesToolOutputImage(JSON, not Pydantic repr)Runner.run_streamedpathintstill stringified to"42"uv run pytest tests/test_tool_use_behavior.py -q→ 15 passed (ran repeatedly).agents/skills/code-change-verification/scripts/run.shmake formatpassedmake lintpassedmake typecheckpassedmake testspassed/reviewbefore submitting this PRIssue number
N/A — focused correctness fix submitted directly (duplicate search found no open issue for this symptom).
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PR