Skip to content

Python: Fix response_format resolution in streaming finalizer#4291

Merged
moonbox3 merged 3 commits intomicrosoft:mainfrom
moonbox3:agent/fix-3970-2
Feb 26, 2026
Merged

Python: Fix response_format resolution in streaming finalizer#4291
moonbox3 merged 3 commits intomicrosoft:mainfrom
moonbox3:agent/fix-3970-2

Conversation

@moonbox3
Copy link
Contributor

@moonbox3 moonbox3 commented Feb 26, 2026

Motivation and Context

When response_format is set in default_options (the typical workflow pattern), AgentResponse.value returns None during streaming, even though the text is correctly parsed. The non-streaming path works fine because it reads from the merged chat_options, but the streaming finalizer only checked the per-call options parameter.

Fixes #3970

Description

The streaming path in BaseAgent.run() used partial(self._finalize_response_updates, response_format=options.get("response_format")) to bind the finalizer at stream-creation time. Since options only contains per-call overrides (not default_options), response_format was None whenever it was set via default_options.

The fix replaces the partial with a _finalizer closure that defers the lookup to finalization time, reading response_format from ctx["chat_options"] — the merged options dict that includes both default_options and per-call overrides. This matches how the non-streaming path already resolves response_format.

A new test verifies that AgentResponse.value is correctly parsed when response_format is set in default_options and the workflow is streamed.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Note: PR autogenerated by moonbox3's agent

…icrosoft#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>
Copilot AI review requested due to automatic review settings February 26, 2026 03:40
Copy link
Contributor Author

@moonbox3 moonbox3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 3 | Confidence: 82%

✓ Correctness

This diff fixes a bug where response_format set in default_options was not picked up during streaming finalization, because the old partial-based finalizer only consulted per-call options. The new _finalizer closure defers the lookup to finalization time and checks ctx["chat_options"] first (which merges default and per-call options). The fix is logically sound and the new test validates the scenario. One minor robustness concern exists around the dict key access pattern.

✓ Security Reliability

This diff fixes response_format resolution during streaming by capturing the merged chat_options from the runtime context instead of only the per-call options. The change is small and mostly a bug fix. The only reliability concern is that ctx["chat_options"] uses bracket access, which will raise a KeyError if the context dict exists but lacks a chat_options key; using .get() would be more defensive. No injection, secrets, or resource-leak issues identified.

✓ Test Coverage

The diff fixes a bug where response_format from default_options was ignored during streaming because the finalizer only read from per-call options. A new test correctly verifies the fix by setting response_format in default_options and asserting the streamed response is parsed into a Pydantic model. Assertions are meaningful (checking result.value.greeting). However, the new _finalizer has a branching fallback (ctx path vs options path vs None) and only the default_options-via-ctx path is tested; the per-call-override and ctx-is-None paths lack dedicated streaming tests.

Suggestions

  • In _finalizer, ctx["chat_options"] uses bracket access which will raise KeyError if chat_options is absent from ctx. Consider using ctx.get("chat_options", {}).get("response_format") for defensive access, unless the presence of chat_options in ctx is guaranteed by contract.
  • Consider using ctx.get("chat_options", {}).get("response_format") instead of ctx["chat_options"].get("response_format") to avoid a potential KeyError if ctx is present but missing the chat_options key.
  • Add a test where response_format is passed both in default_options and as a per-call option to verify that per-call options take precedence in the streaming finalizer.
  • Add a test where response_format is passed only as a per-call kwarg (not in default_options) to ensure the fallback branch in _finalizer (when ctx is None or missing chat_options) still works correctly under the new code path.

Automated review by maf-dashboard agent

@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Feb 26, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _agents.py3354387%425, 429, 481, 846, 882, 898, 977–980, 1041–1043, 1164, 1180, 1182, 1195, 1201, 1237, 1239, 1248–1253, 1258, 1260, 1266–1267, 1274, 1276–1277, 1285–1286, 1289–1291, 1299–1300, 1302, 1307, 1309
TOTAL22178276287% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4674 247 💤 0 ❌ 0 🔥 1m 18s ⏱️

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where AgentResponse.value is None when streaming a workflow with an agent that has response_format set in default_options. The issue occurred because the streaming path's finalizer was reading response_format from the raw options parameter instead of from the merged chat options that combine default_options with runtime options.

Changes:

  • Fixed the streaming finalizer to access response_format from the merged chat_options in ctx_holder
  • Added a test to validate that response_format from default_options is correctly parsed when streaming

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
python/packages/core/agent_framework/_agents.py Updated the streaming finalizer to read response_format from merged ctx["chat_options"] instead of raw options parameter
python/packages/core/tests/core/test_agents.py Added test validating that AgentResponse.value is properly parsed when using response_format in default_options with streaming

@moonbox3 moonbox3 changed the title Python: [Bug]: AgentResponse.value is None when streaming workflow Python: Fix response_format resolution in streaming finalizer Feb 26, 2026
Copy link
Member

@eavanvalkenburg eavanvalkenburg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

@moonbox3 moonbox3 added this pull request to the merge queue Feb 26, 2026
Merged via the queue into microsoft:main with commit a033721 Feb 26, 2026
29 checks passed
@github-project-automation github-project-automation bot moved this from In Review to Done in Agent Framework Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: AgentResponse.value is None when streaming workflow

5 participants