From 3dedffd9d2166cec905df735f934e63f70207605 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 26 Feb 2026 12:37:30 +0900 Subject: [PATCH 1/2] Python: Fix AgentResponse.value being None when streaming workflow (#3970) The streaming path in BaseAgent.run() used the raw 'options' parameter (passed by the caller) to bind response_format into the outer stream's finalizer. When response_format was set in default_options rather than runtime options, it was missing from the finalizer and value was None. Fix: Use the merged chat_options from the run context (via ctx_holder), matching the non-streaming path which already uses ctx['chat_options']. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/_agents.py | 9 ++++--- .../packages/core/tests/core/test_agents.py | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index a3f4570b6e..cce1c720a6 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -935,6 +935,11 @@ def _propagate_conversation_id(update: AgentResponseUpdate) -> AgentResponseUpda session.service_session_id = conv_id return update + def _finalizer(updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]: + ctx = ctx_holder["ctx"] + rf = ctx["chat_options"].get("response_format") if ctx else (options.get("response_format") if options else None) + return self._finalize_response_updates(updates, response_format=rf) + return ( ResponseStream .from_awaitable(_get_stream()) @@ -943,9 +948,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=_finalizer, ) .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..a95d388dbf 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -97,6 +97,31 @@ async def test_chat_client_agent_run_streaming(client: SupportsChatGetResponse) assert result.text == "test streaming response another update" +async def test_chat_client_agent_streaming_response_format_from_default_options( + client: SupportsChatGetResponse, +) -> None: + """AgentResponse.value must be parsed when response_format is set in default_options and streaming.""" + from pydantic import BaseModel + + class Greeting(BaseModel): + greeting: str + + json_text = '{"greeting": "Hello"}' + client.streaming_responses.append( # type: ignore[attr-defined] + [ChatResponseUpdate(contents=[Content.from_text(json_text)], role="assistant", finish_reason="stop")] + ) + + agent = Agent(client=client, default_options={"response_format": Greeting}) + stream = agent.run("Hello", stream=True) + async for _ in stream: + pass + result = await stream.get_final_response() + + assert result.text == json_text + assert result.value is not None + assert result.value.greeting == "Hello" + + async def test_chat_client_agent_create_session(client: SupportsChatGetResponse) -> None: agent = Agent(client=client) session = agent.create_session() From 73be7f132927ae077c69a930da7604be060d2eaa Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 26 Feb 2026 18:00:33 +0900 Subject: [PATCH 2/2] Address review feedback for #3970: safer ctx access, add test coverage --- .../packages/core/agent_framework/_agents.py | 2 +- .../packages/core/tests/core/test_agents.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index cce1c720a6..580b6e2c6d 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -937,7 +937,7 @@ def _propagate_conversation_id(update: AgentResponseUpdate) -> AgentResponseUpda def _finalizer(updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]: ctx = ctx_holder["ctx"] - rf = ctx["chat_options"].get("response_format") if ctx else (options.get("response_format") if options else None) + rf = ctx.get("chat_options", {}).get("response_format") if ctx else (options.get("response_format") if options else None) return self._finalize_response_updates(updates, response_format=rf) return ( diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index a95d388dbf..b6f84dc970 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -119,9 +119,36 @@ class Greeting(BaseModel): assert result.text == json_text assert result.value is not None + assert isinstance(result.value, Greeting) assert result.value.greeting == "Hello" +async def test_chat_client_agent_streaming_response_format_from_run_options( + client: SupportsChatGetResponse, +) -> None: + """AgentResponse.value must be parsed when response_format is passed via run() options kwarg.""" + from pydantic import BaseModel + + class Greeting(BaseModel): + greeting: str + + json_text = '{"greeting": "Hi"}' + client.streaming_responses.append( # type: ignore[attr-defined] + [ChatResponseUpdate(contents=[Content.from_text(json_text)], role="assistant", finish_reason="stop")] + ) + + agent = Agent(client=client) + stream = agent.run("Hello", stream=True, options={"response_format": Greeting}) + async for _ in stream: + pass + result = await stream.get_final_response() + + assert result.text == json_text + assert result.value is not None + assert isinstance(result.value, Greeting) + assert result.value.greeting == "Hi" + + async def test_chat_client_agent_create_session(client: SupportsChatGetResponse) -> None: agent = Agent(client=client) session = agent.create_session()