Python: Fix response_format resolution in streaming finalizer#4291
Python: Fix response_format resolution in streaming finalizer#4291moonbox3 merged 3 commits intomicrosoft:mainfrom
Conversation
…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>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 82%
✓ Correctness
This diff fixes a bug where
response_formatset indefault_optionswas not picked up during streaming finalization, because the oldpartial-based finalizer only consulted per-calloptions. The new_finalizerclosure defers the lookup to finalization time and checksctx["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 achat_optionskey; using.get()would be more defensive. No injection, secrets, or resource-leak issues identified.
✓ Test Coverage
The diff fixes a bug where
response_formatfromdefault_optionswas ignored during streaming because the finalizer only read from per-calloptions. A new test correctly verifies the fix by settingresponse_formatindefault_optionsand asserting the streamed response is parsed into a Pydantic model. Assertions are meaningful (checkingresult.value.greeting). However, the new_finalizerhas a branching fallback (ctxpath vsoptionspath vsNone) and only thedefault_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 raiseKeyErrorifchat_optionsis absent fromctx. Consider usingctx.get("chat_options", {}).get("response_format")for defensive access, unless the presence ofchat_optionsinctxis guaranteed by contract. - Consider using
ctx.get("chat_options", {}).get("response_format")instead ofctx["chat_options"].get("response_format")to avoid a potential KeyError ifctxis present but missing thechat_optionskey. - Add a test where
response_formatis passed both indefault_optionsand as a per-call option to verify that per-call options take precedence in the streaming finalizer. - Add a test where
response_formatis passed only as a per-call kwarg (not indefault_options) to ensure the fallback branch in_finalizer(whenctxis None or missingchat_options) still works correctly under the new code path.
Automated review by maf-dashboard agent
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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_formatfrom the mergedchat_optionsinctx_holder - Added a test to validate that
response_formatfromdefault_optionsis 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 |
AgentResponse.value is None when streaming workflow
Motivation and Context
When
response_formatis set indefault_options(the typical workflow pattern),AgentResponse.valuereturnsNoneduring streaming, even though the text is correctly parsed. The non-streaming path works fine because it reads from the mergedchat_options, but the streaming finalizer only checked the per-calloptionsparameter.Fixes #3970
Description
The streaming path in
BaseAgent.run()usedpartial(self._finalize_response_updates, response_format=options.get("response_format"))to bind the finalizer at stream-creation time. Sinceoptionsonly contains per-call overrides (notdefault_options),response_formatwasNonewhenever it was set viadefault_options.The fix replaces the
partialwith a_finalizerclosure that defers the lookup to finalization time, readingresponse_formatfromctx["chat_options"]— the merged options dict that includes bothdefault_optionsand per-call overrides. This matches how the non-streaming path already resolvesresponse_format.A new test verifies that
AgentResponse.valueis correctly parsed whenresponse_formatis set indefault_optionsand the workflow is streamed.Contribution Checklist