Python: Fix #3970: Python: [Bug]: AgentResponse.value is None when streaming workflow#4286
Python: Fix #3970: Python: [Bug]: AgentResponse.value is None when streaming workflow#4286moonbox3 wants to merge 1 commit intomicrosoft:mainfrom
AgentResponse.value is None when streaming workflow#4286Conversation
microsoft#3970) The streaming finalizer in Agent.run() used the raw `options` parameter to read `response_format`, which is empty when the format is set via `default_options` in the constructor. The non-streaming path correctly used the merged `chat_options` (which includes default_options). Change the finalizer to a closure that reads response_format from the merged chat_options via the context holder, matching the non-streaming behavior. 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: 88%
✓ Correctness
This diff fixes a bug where
response_formatset viadefault_optionsin the constructor was not being respected during streaming responses. The fix correctly changes the finalizer to readresponse_formatfrom the mergedchat_optionsin the context (which includesdefault_options) rather than from the per-calloptionsparameter. The closure capturesctx_holderand reads it at finalization time, which is appropriate since the context will be populated by then. The null check onctxis present. The test correctly validates the fix. The string-join changes intest_function_invocation_logic.pyare cosmetic and harmless.
✓ Security Reliability
This diff fixes a bug where
response_formatset viadefault_optionswas ignored during streaming because the finalizer only read from the per-calloptions. The fix captures the mergedchat_optionsfrom the context holder instead. The change is low-risk: it reads from an already-populated context dictionary and the closure overctx_holderis safe since it's defined in the same scope. The test changes are straightforward and the string reformatting in the other test file is cosmetic. No security or reliability issues found.
✓ Test Coverage
The new test directly validates the bug fix (issue #3970) where response_format set via default_options was ignored during streaming. Assertions are meaningful — checking text, parsed value type, and field content against a real Pydantic model. However, the diff lacks a complementary test verifying that response_format passed directly in per-call options still works after the refactor (the finalizer now reads from merged chat_options instead of raw options). There is also no test covering the precedence behavior when response_format appears in both default_options and per-call options.
Suggestions
- Consider using
ctx.get("chat_options", {}).get("response_format")instead ofctx["chat_options"].get("response_format")to guard against a missingchat_optionskey, though this may be unnecessary if the context contract guarantees its presence. - In
_stream_finalizer, ifctx_holder["ctx"]is unexpectedly missing the"chat_options"key (e.g., due to a future refactor), aKeyErrorwould propagate uncaught. Consider usingctx.get("chat_options", {}).get("response_format")for defensive access, though this is minor given the controlled internal usage. - Add a test for response_format passed directly via per-call options in streaming mode (e.g., agent.run('Hello', stream=True, response_format=Greeting)) to confirm the refactored finalizer still handles this path correctly.
- Add a test where response_format is specified in both default_options and per-call options to verify correct merge/override precedence in streaming mode.
- Consider a test where ctx_holder['ctx'] is None (the defensive branch on line 940) — though this is unlikely in practice, it would exercise the fallback path.
Automated review by maf-dashboard agent
| def _stream_finalizer(updates: Sequence[AgentResponseUpdate]) -> AgentResponse: | ||
| # Read response_format from merged chat_options (includes default_options) | ||
| # so that response_format set via the constructor is respected. | ||
| ctx = ctx_holder["ctx"] |
There was a problem hiding this comment.
Minor robustness note: if ctx is truthy but chat_options is unexpectedly absent, ctx["chat_options"] would raise KeyError. Using ctx.get("chat_options", {}).get("response_format") would be safer, though this is likely fine if the context schema guarantees the key.
| def _stream_finalizer(updates: Sequence[AgentResponseUpdate]) -> AgentResponse: | ||
| # Read response_format from merged chat_options (includes default_options) | ||
| # so that response_format set via the constructor is respected. | ||
| ctx = ctx_holder["ctx"] |
There was a problem hiding this comment.
Nit (non-blocking): ctx["chat_options"] will raise KeyError if the key is absent. Using ctx.get("chat_options", {}).get("response_format") would be more defensive, though the key is expected to always be present in practice.
|
|
||
| def _stream_finalizer(updates: Sequence[AgentResponseUpdate]) -> AgentResponse: | ||
| # Read response_format from merged chat_options (includes default_options) | ||
| # so that response_format set via the constructor is respected. |
There was a problem hiding this comment.
This defensive if ctx else None check is not exercised by any test. Consider whether ctx can realistically be None here; if not, dropping the guard (or adding a test that triggers it) would improve clarity.
| stream = agent.run("Hello", stream=True) | ||
| async for _ in stream: | ||
| pass | ||
| result = await stream.get_final_response() |
There was a problem hiding this comment.
Good: the test covers the exact bug scenario (default_options response_format in streaming). Consider adding a sibling test that passes response_format directly in run() options to ensure the refactored finalizer handles both paths.
AgentResponse.value is None when streaming workflowAgentResponse.value is None when streaming workflow
There was a problem hiding this comment.
Pull request overview
Fixes a Python streaming regression where AgentResponse.value was not parsed (remained None) when response_format is provided via an agent’s default_options, particularly impacting streamed workflow executions.
Changes:
- Update
Agent.run(stream=True)finalization to readresponse_formatfrom the mergedchat_optionsproduced during run-context preparation (includesdefault_options). - Add a regression test ensuring streamed
AgentResponse.valueis populated whenresponse_formatis set viadefault_options. - Minor test assertion string formatting cleanup.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
python/packages/core/agent_framework/_agents.py |
Uses run-context merged chat_options to propagate response_format into the streaming finalizer so AgentResponse.value can be parsed. |
python/packages/core/tests/core/test_agents.py |
Adds a streaming regression test covering response_format provided via default_options (issue #3970). |
python/packages/core/tests/core/test_function_invocation_logic.py |
Minor formatting change to assertion message strings (no functional change). |
|
Currently testing.. closing PR. |
Automated fix for #3970.
Verification passed after 0 retries.