From fa129741f35c5b6a523811d404b3267098ea19bf Mon Sep 17 00:00:00 2001 From: HeZzz Date: Wed, 2 Sep 2026 01:10:53 +0800 Subject: [PATCH 1/5] Python: preserve response update metadata in WorkflowAgent forwarding Forward finish_reason, continuation_token, agent_id and a shallow copy of additional_properties when WorkflowAgent reconstructs AgentResponseUpdate objects from workflow events, matching the documented as-is forwarding contract. --- .../core/agent_framework/_workflows/_agent.py | 4 ++ .../tests/workflow/test_workflow_agent.py | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 0df47d2b345..6450089ae40 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -636,9 +636,13 @@ def _convert_workflow_event_to_agent_response_updates( contents=list(data.contents), role=data.role, author_name=data.author_name or executor_id, + agent_id=data.agent_id, response_id=data.response_id, message_id=data.message_id, created_at=data.created_at, + finish_reason=data.finish_reason, + continuation_token=data.continuation_token, + additional_properties=dict(data.additional_properties) if data.additional_properties else None, raw_representation=data.raw_representation, ) ] diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 70fd94468ca..200e7ca24e5 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -852,6 +852,44 @@ async def yielding_executor(messages: list[Message], ctx: WorkflowContext[Never, assert "first output" in texts assert "second output" in texts + async def test_workflow_as_agent_stream_preserves_response_update_metadata(self) -> None: + """Test that streaming forwards finish_reason, continuation_token and additional_properties. + + This validates the fix for issue #7952: AgentResponseUpdate metadata should be + forwarded as-is when the workflow is wrapped via .as_agent(). + """ + + @executor + async def metadata_executor(messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: # type: ignore[valid-type] + await ctx.yield_output( + AgentResponseUpdate( + contents=[Content.from_text(text="payload")], + role="assistant", + agent_id="source-agent", + response_id="source-response", + message_id="source-message", + finish_reason="stop", + continuation_token="resume-token", + additional_properties={"provider_marker": "preserve-me"}, + ) + ) + + workflow = WorkflowBuilder(start_executor=metadata_executor).build() + agent = workflow.as_agent("metadata-test-agent") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hello", stream=True): + updates.append(update) + + metadata_updates = [u for u in updates if u.response_id == "source-response"] + assert len(metadata_updates) == 1 + update = metadata_updates[0] + assert update.text == "payload" + assert update.agent_id == "source-agent" + assert update.finish_reason == "stop" + assert update.continuation_token == "resume-token" + assert update.additional_properties == {"provider_marker": "preserve-me"} + async def test_workflow_as_agent_yield_output_with_content_types(self) -> None: """Test that yield_output preserves different content types (Content, Content, etc.).""" From fdc15a96096c30db5ce25003256638e8956ee1f3 Mon Sep 17 00:00:00 2001 From: HeZzz Date: Wed, 2 Sep 2026 01:20:37 +0800 Subject: [PATCH 2/5] Python: keep empty additional_properties dict when forwarding Use an explicit None check instead of a truthiness check so an explicitly empty additional_properties dict is not converted to None, preserving the forwarding contract. --- .../core/agent_framework/_workflows/_agent.py | 2 +- .../tests/workflow/test_workflow_agent.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 6450089ae40..65e088a9e96 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -642,7 +642,7 @@ def _convert_workflow_event_to_agent_response_updates( created_at=data.created_at, finish_reason=data.finish_reason, continuation_token=data.continuation_token, - additional_properties=dict(data.additional_properties) if data.additional_properties else None, + additional_properties=dict(data.additional_properties) if data.additional_properties is not None else None, raw_representation=data.raw_representation, ) ] diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 200e7ca24e5..87eac2b7004 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -890,6 +890,31 @@ async def metadata_executor(messages: list[Message], ctx: WorkflowContext[Never, assert update.continuation_token == "resume-token" assert update.additional_properties == {"provider_marker": "preserve-me"} + async def test_workflow_as_agent_stream_preserves_empty_additional_properties(self) -> None: + """Test that an explicitly empty additional_properties dict is not converted to None.""" + + @executor + async def empty_props_executor(messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: # type: ignore[valid-type] + await ctx.yield_output( + AgentResponseUpdate( + contents=[Content.from_text(text="payload")], + role="assistant", + response_id="empty-props-response", + additional_properties={}, + ) + ) + + workflow = WorkflowBuilder(start_executor=empty_props_executor).build() + agent = workflow.as_agent("empty-props-test-agent") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hello", stream=True): + updates.append(update) + + forwarded = [u for u in updates if u.response_id == "empty-props-response"] + assert len(forwarded) == 1 + assert forwarded[0].additional_properties == {} + async def test_workflow_as_agent_yield_output_with_content_types(self) -> None: """Test that yield_output preserves different content types (Content, Content, etc.).""" From eaa0131e67b78f8caa27d735aa82a7572f319687 Mon Sep 17 00:00:00 2001 From: HeZzz Date: Thu, 3 Sep 2026 16:45:50 +0800 Subject: [PATCH 3/5] Python: use ContinuationToken-shaped value in WorkflowAgent metadata test Fix type checker failures by constructing continuation_token as an opaque dict per the ContinuationToken TypedDict instead of a bare string, and resolve lint/typing nits in the new tests. --- python/packages/core/agent_framework/_workflows/_agent.py | 4 +++- .../packages/core/tests/workflow/test_workflow_agent.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 65e088a9e96..03cd277ceab 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -642,7 +642,9 @@ def _convert_workflow_event_to_agent_response_updates( created_at=data.created_at, finish_reason=data.finish_reason, continuation_token=data.continuation_token, - additional_properties=dict(data.additional_properties) if data.additional_properties is not None else None, + additional_properties=dict(data.additional_properties) + if data.additional_properties is not None + else None, raw_representation=data.raw_representation, ) ] diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 87eac2b7004..d78efd1d545 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -3,7 +3,7 @@ import uuid from collections.abc import Awaitable, Sequence from dataclasses import dataclass -from typing import Any, Literal, overload +from typing import Any, Literal, cast, overload import pytest from typing_extensions import Never @@ -869,7 +869,7 @@ async def metadata_executor(messages: list[Message], ctx: WorkflowContext[Never, response_id="source-response", message_id="source-message", finish_reason="stop", - continuation_token="resume-token", + continuation_token=cast(Any, {"token": "resume-token"}), additional_properties={"provider_marker": "preserve-me"}, ) ) @@ -887,14 +887,14 @@ async def metadata_executor(messages: list[Message], ctx: WorkflowContext[Never, assert update.text == "payload" assert update.agent_id == "source-agent" assert update.finish_reason == "stop" - assert update.continuation_token == "resume-token" + assert update.continuation_token == {"token": "resume-token"} assert update.additional_properties == {"provider_marker": "preserve-me"} async def test_workflow_as_agent_stream_preserves_empty_additional_properties(self) -> None: """Test that an explicitly empty additional_properties dict is not converted to None.""" @executor - async def empty_props_executor(messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: # type: ignore[valid-type] + async def empty_props_executor(messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: # type: ignore[valid-type] # noqa: E501 await ctx.yield_output( AgentResponseUpdate( contents=[Content.from_text(text="payload")], From 5e6ea2ae0a93e72813d74c28c273216a1d05b389 Mon Sep 17 00:00:00 2001 From: HeZzz Date: Tue, 8 Sep 2026 21:32:48 +0800 Subject: [PATCH 4/5] Python: Fix workflow response metadata forwarding --- .../core/agent_framework/_workflows/_agent.py | 6 ++- .../tests/workflow/test_workflow_agent.py | 42 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 03cd277ceab..92ad4d0e482 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -24,6 +24,8 @@ AgentResponseUpdate, AgentRunInputs, Content, + FinishReason, + FinishReasonLiteral, Message, ResponseStream, UsageDetails, @@ -640,7 +642,9 @@ def _convert_workflow_event_to_agent_response_updates( response_id=data.response_id, message_id=data.message_id, created_at=data.created_at, - finish_reason=data.finish_reason, + # The attribute is typed wider than the constructor accepts (custom + # connectors may set any string); forward the value unchanged. + finish_reason=cast(FinishReasonLiteral | FinishReason | None, data.finish_reason), continuation_token=data.continuation_token, additional_properties=dict(data.additional_properties) if data.additional_properties is not None diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index d78efd1d545..eed0901c2ad 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -15,6 +15,7 @@ AgentSession, Content, Executor, + FinishReason, HistoryProvider, InMemoryHistoryProvider, Message, @@ -890,11 +891,50 @@ async def metadata_executor(messages: list[Message], ctx: WorkflowContext[Never, assert update.continuation_token == {"token": "resume-token"} assert update.additional_properties == {"provider_marker": "preserve-me"} + async def test_workflow_as_agent_stream_preserves_custom_finish_reason(self) -> None: + """Test that a non-literal finish_reason is forwarded unchanged. + + Custom chat connectors can report finish reasons outside the standard + literals (modeled as `FinishReason`); the WorkflowAgent must preserve + them when re-emitting the update. + """ + + @executor + async def custom_reason_executor( + messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate] + ) -> None: # type: ignore[valid-type] # noqa: E501 + await ctx.yield_output( + AgentResponseUpdate( + contents=[Content.from_text(text="payload")], + role="assistant", + agent_id="source-agent", + response_id="custom-reason-response", + message_id="source-message", + finish_reason=FinishReason("custom_reason"), + ) + ) + + workflow = WorkflowBuilder(start_executor=custom_reason_executor).build() + agent = workflow.as_agent("custom-reason-test-agent") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hello", stream=True): + updates.append(update) + + metadata_updates = [u for u in updates if u.response_id == "custom-reason-response"] + assert len(metadata_updates) == 1 + update = metadata_updates[0] + assert update.text == "payload" + assert update.agent_id == "source-agent" + assert update.finish_reason == "custom_reason" + async def test_workflow_as_agent_stream_preserves_empty_additional_properties(self) -> None: """Test that an explicitly empty additional_properties dict is not converted to None.""" @executor - async def empty_props_executor(messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: # type: ignore[valid-type] # noqa: E501 + async def empty_props_executor( + messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate] + ) -> None: # type: ignore[valid-type] # noqa: E501 await ctx.yield_output( AgentResponseUpdate( contents=[Content.from_text(text="payload")], From 23e51477daf0767e7fa6a9145bca1a4d1c0c9f5b Mon Sep 17 00:00:00 2001 From: HeZzz Date: Wed, 9 Sep 2026 18:33:36 +0800 Subject: [PATCH 5/5] Python: Update WorkflowContext type in custom reason and empty props executors --- .../packages/core/tests/workflow/test_workflow_agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index eed0901c2ad..68a31640308 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -901,8 +901,8 @@ async def test_workflow_as_agent_stream_preserves_custom_finish_reason(self) -> @executor async def custom_reason_executor( - messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate] - ) -> None: # type: ignore[valid-type] # noqa: E501 + messages: list[Message], ctx: WorkflowContext[Any, AgentResponseUpdate] + ) -> None: await ctx.yield_output( AgentResponseUpdate( contents=[Content.from_text(text="payload")], @@ -933,8 +933,8 @@ async def test_workflow_as_agent_stream_preserves_empty_additional_properties(se @executor async def empty_props_executor( - messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate] - ) -> None: # type: ignore[valid-type] # noqa: E501 + messages: list[Message], ctx: WorkflowContext[Any, AgentResponseUpdate] + ) -> None: await ctx.yield_output( AgentResponseUpdate( contents=[Content.from_text(text="payload")],