Skip to content

Python: Fix #3970: Python: [Bug]: AgentResponse.value is None when streaming workflow#4286

Closed
moonbox3 wants to merge 1 commit intomicrosoft:mainfrom
moonbox3:agent/fix-3970-10
Closed

Python: Fix #3970: Python: [Bug]: AgentResponse.value is None when streaming workflow#4286
moonbox3 wants to merge 1 commit intomicrosoft:mainfrom
moonbox3:agent/fix-3970-10

Conversation

@moonbox3
Copy link
Contributor

Automated fix for #3970.

Verification passed after 0 retries.

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>
Copilot AI review requested due to automatic review settings February 26, 2026 02:13
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: 88%

✓ Correctness

This diff fixes a bug where response_format set via default_options in the constructor was not being respected during streaming responses. The fix correctly changes the finalizer to read response_format from the merged chat_options in the context (which includes default_options) rather than from the per-call options parameter. The closure captures ctx_holder and reads it at finalization time, which is appropriate since the context will be populated by then. The null check on ctx is present. The test correctly validates the fix. The string-join changes in test_function_invocation_logic.py are cosmetic and harmless.

✓ Security Reliability

This diff fixes a bug where response_format set via default_options was ignored during streaming because the finalizer only read from the per-call options. The fix captures the merged chat_options from the context holder instead. The change is low-risk: it reads from an already-populated context dictionary and the closure over ctx_holder is 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 of ctx["chat_options"].get("response_format") to guard against a missing chat_options key, though this may be unnecessary if the context contract guarantees its presence.
  • In _stream_finalizer, if ctx_holder["ctx"] is unexpectedly missing the "chat_options" key (e.g., due to a future refactor), a KeyError would propagate uncaught. Consider using ctx.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"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

@github-actions github-actions bot changed the title Fix #3970: Python: [Bug]: AgentResponse.value is None when streaming workflow Python: Fix #3970: Python: [Bug]: AgentResponse.value is None when streaming workflow Feb 26, 2026
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

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 read response_format from the merged chat_options produced during run-context preparation (includes default_options).
  • Add a regression test ensuring streamed AgentResponse.value is populated when response_format is set via default_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).

@moonbox3
Copy link
Contributor Author

Currently testing.. closing PR.

@moonbox3 moonbox3 closed this Feb 26, 2026
@moonbox3 moonbox3 deleted the agent/fix-3970-10 branch February 26, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants