From 165fbe2e6aa760447be0e5bdf4d585826135e87c Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 26 Feb 2026 19:45:37 +0900 Subject: [PATCH] Python: Fix streaming run ignoring default_options response_format (#4300) The streaming path in Agent.run(stream=True) used the raw options kwarg instead of the merged chat_options (which includes default_options) when building the finalizer for response_format parsing. This caused AgentResponse.value to be None when response_format was set via default_options. Fix by reading response_format from the merged ctx['chat_options'] (populated by _prepare_run_context) instead of the raw options parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_agents.py | 13 ++++-- .../packages/core/tests/core/test_agents.py | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index a3f4570b6e..ea5e734953 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -935,6 +935,15 @@ def _propagate_conversation_id(update: AgentResponseUpdate) -> AgentResponseUpda session.service_session_id = conv_id return update + def _get_merged_response_format() -> Any | None: + ctx = ctx_holder["ctx"] + if ctx is not None: + return ctx["chat_options"].get("response_format") + return options.get("response_format") if options else None + + def _finalize_with_merged_options(updates: Sequence[AgentResponseUpdate]) -> AgentResponse: + return self._finalize_response_updates(updates, response_format=_get_merged_response_format()) + return ( ResponseStream .from_awaitable(_get_stream()) @@ -943,9 +952,7 @@ def _propagate_conversation_id(update: AgentResponseUpdate) -> AgentResponseUpda map_chat_to_agent_update, agent_name=self.name, ), - finalizer=partial( - self._finalize_response_updates, response_format=options.get("response_format") if options else None - ), + finalizer=_finalize_with_merged_options, ) .with_transform_hook(_propagate_conversation_id) .with_result_hook(_post_hook) diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index 627987a1f2..1ca0c4ffbe 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -1136,4 +1136,47 @@ async def test_stores_by_default_with_store_false_injects_inmemory(client: Suppo # endregion +async def test_streaming_run_uses_default_options_response_format(client: SupportsChatGetResponse) -> None: + """Streaming run should honor default_options response_format for final value parsing (#4300).""" + from pydantic import BaseModel + + class Out(BaseModel): + x: int + + client.streaming_responses = [ # type: ignore[attr-defined] + [ChatResponseUpdate(contents=[Content.from_text('{"x": 42}')], role="assistant")], + ] + + agent = Agent(client=client, default_options={"response_format": Out}) + + stream = agent.run("hi", stream=True) + async for _ in stream: + pass + final = await stream.get_final_response() + + assert final.value is not None + assert isinstance(final.value, Out) + assert final.value.x == 42 + + +async def test_non_streaming_run_uses_default_options_response_format(client: SupportsChatGetResponse) -> None: + """Non-streaming run should honor default_options response_format for value parsing.""" + from pydantic import BaseModel + + class Out(BaseModel): + x: int + + client.responses = [ # type: ignore[attr-defined] + ChatResponse(messages=Message(role="assistant", text='{"x": 42}')), + ] + + agent = Agent(client=client, default_options={"response_format": Out}) + + result = await agent.run("hi") + + assert result.value is not None + assert isinstance(result.value, Out) + assert result.value.x == 42 + + # endregion