Skip to content

Python: [BREAKING] Standardize orchestration terminal outputs as AgentResponse - #5301

Merged
Evan Mattson (moonbox3) merged 21 commits into
microsoft:mainfrom
moonbox3:improve-orchestration-outputs
Apr 29, 2026
Merged

Python: [BREAKING] Standardize orchestration terminal outputs as AgentResponse#5301
Evan Mattson (moonbox3) merged 21 commits into
microsoft:mainfrom
moonbox3:improve-orchestration-outputs

Conversation

@moonbox3

@moonbox3Evan Mattson (moonbox3) commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

Important

Breaking changes are scoped to the still-experimental agent-framework-orchestrations package. Core agent-framework workflow code is non-breaking.

workflow.as_agent().run(prompt) for SequentialBuilder returned the user's input plus every agent's reply instead of the last agent's answer. The same class of bug affected Concurrent, GroupChat, Magentic, and Handoff: they yielded list[Message] conversation dumps as output events, so as_agent() consumers got a conversation dump instead of an answer.

This PR standardizes the terminal-output shape for every built-in orchestration to AgentResponse (or per-chunk AgentResponseUpdate while streaming). One contract: each orchestration's terminal output event carries the meaningful answer, not a conversation transcript.

Per-orchestration contract

OrchestrationTerminal output event
Sequential (agent terminator)Last agent's AgentResponse (streaming chunks — the last AgentExecutor itself is the workflow's output executor)
Sequential (custom-executor terminator)The custom executor's own AgentResponse — see "Custom terminators" below
Concurrent (default aggregator)AgentResponse with one assistant message per participant (no user prompt prepended)
Concurrent (custom aggregator)Whatever the aggregator yields
GroupChatAgentResponse with the orchestrator's completion message
MagenticAgentResponse with the manager's synthesized final answer
HandoffNo terminal yield — per-agent responses surface as output events as agents speak

SequentialBuilder simplification

SequentialBuilder.build() no longer wraps the chain with a synthetic terminal node (_EndWithConversation removed). The last participant is registered as the workflow's output_executor directly. Two consequences:

  1. Custom-executor terminators must yield directly. A custom Executor used as the last participant must call await ctx.yield_output(AgentResponse(messages=[...])). Intermediate custom executors continue to use ctx.send_message(list[Message]) for chaining.
  2. Output type uniformity. The terminal output is AgentResponse, not list[Message].

The one in-tree sample using this pattern (samples/03-workflows/orchestrations/sequential_custom_executors.py) and one test were updated.

intermediate_outputs=True on builders continues to work as on main — it flips the workflow's output_executors whitelist off so every executor's yield_output surfaces as a workflow output event. No semantic change.

HIL terminator support

AgentApprovalExecutor now accepts allow_direct_output. When the HIL-wrapped agent is the last participant in SequentialBuilder, the inner approval workflow's response surfaces as the wrapping workflow's terminal output event. A small sibling class _TerminalAgentRequestInfoExecutor yields AgentResponse (instead of AgentExecutorResponse) in the terminator role, so the unwrap happens at the source rather than at the WorkflowExecutor boundary in core.

Behavior changes (orchestrations package)

  • workflow.as_agent().run(prompt) returns only the answer for each orchestration, matching the agent contract.
  • Sequential / GroupChat / Magentic terminal output: list[Message] (full conversation) → AgentResponse (answer only).
  • Concurrent default aggregator: list[Message] (user + per-agent) → AgentResponse (per-agent only).
  • Handoff: no synthetic terminal event. Per-agent output events are the end of the stream.
  • _EndWithConversation removed from SequentialBuilder; custom-executor terminators must yield_output(AgentResponse) directly.

Core changes (non-breaking)

  • WorkflowExecutor handlers annotated WorkflowContext[Any, Any] (was WorkflowContext[Any]) — permissive, lets WorkflowExecutor serve as a workflow's output_executor when allow_direct_output=True.
  • WorkflowAgent's streaming converter constructs fresh AgentResponseUpdate instances instead of mutating the executor's payload — independent mutation-safety fix surfaced during review.

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.

CopilotAI review requested due to automatic review settings April 16, 2026 06:28
@moonbox3Evan Mattson (moonbox3) added the python Usage: [Issues, PRs], Target: Python label Apr 16, 2026
@github-actionsgithub-actionsBot changed the title [BREAKING] Python: Fix orchestration outputs so as_agent() returns the final answer only. Align other orchestration outputs.Python: [BREAKING] Python: Fix orchestration outputs so as_agent() returns the final answer only. Align other orchestration outputs.Apr 16, 2026
@moonbox3

Evan Mattson (moonbox3) commented Apr 16, 2026

Copy link
Copy Markdown
ContributorAuthor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework/_workflows
_agent.py3587180%66, 74–80, 116–117, 210, 271, 284, 351, 362, 364, 424, 430, 440–441, 448, 450, 456, 518–519, 528, 536, 562, 595–597, 599, 601, 603, 608, 613, 667, 697, 714, 753–756, 762, 768, 772–773, 776–782, 786–787, 795, 856, 863, 869–870, 881, 913, 920, 941, 950, 954, 956–958, 965
_agent_executor.py2001692%166, 190, 231, 255, 275–276, 356–358, 360, 370–371, 490–491, 563, 569
_workflow_executor.py1873083%94, 404, 464, 487, 489, 497–498, 503, 505, 510, 512, 593–599, 603–605, 613, 618, 629, 639, 643, 649, 653, 663, 667
packages/orchestrations/agent_framework_orchestrations
_base_group_chat_orchestrator.py1721293%109, 277, 292, 326–328, 332, 351, 439, 485–487
_concurrent.py1442681%51, 60–61, 69–70, 89–90, 95, 113, 118, 123–124, 145, 155, 162, 231, 247, 250, 307, 337, 339–340, 342, 347, 360, 364
_group_chat.py3297078%175, 344, 351, 384, 395–396, 402, 407, 428, 432, 445–446, 459, 474–475, 477, 493, 520–525, 527, 561–564, 566, 571–575, 664, 667, 706, 709, 712, 715, 723, 735–736, 738–739, 741–742, 744, 749, 752, 761, 767, 811–812, 816–817, 831–832, 834–835, 866–867, 933, 952, 960, 965–967, 974, 984
_handoff.py3355284%104–105, 107, 160–170, 172, 174, 176, 181, 312, 337, 364, 390, 454, 497, 505, 509–510, 541–543, 548–550, 670, 673, 686, 748, 753, 760, 770, 772, 791, 793, 875–876, 908–909, 1010, 1017, 1089–1090, 1092
_magentic.py5929184%65–74, 79, 83–94, 259, 270, 274, 294, 355, 364, 366, 408, 425, 434–435, 437–439, 441, 452, 595, 597, 637, 687, 723–725, 727, 737, 745–746, 813–816, 907, 913, 919, 961, 999, 1031, 1048, 1059, 1116–1117, 1121–1123, 1147, 1171–1172, 1185, 1205, 1228, 1273–1274, 1312–1313, 1477, 1480, 1489, 1492, 1497, 1548–1549, 1590–1591, 1639, 1669, 1727, 1741, 1752
_orchestration_request_info.py66198%162
_sequential.py80692%48, 120, 131, 137, 185, 213
TOTAL30444354188%

Python Unit Test Overview

TestsSkippedFailuresErrorsTime
612230 💤0 ❌0 🔥1m 35s ⏱️

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 changes Python orchestration output semantics so workflow.as_agent().run(prompt) returns only the final “answer” (as an AgentResponse) while intermediate participant activity is surfaced via data events (instead of additional output events). It adds a single new knob (AgentExecutor(emit_intermediate_data=...)) and updates orchestrations, tests, and a sample to align with the clarified contract.

Changes:

  • Add AgentExecutor.emit_intermediate_data to emit parallel data events for AgentResponse / AgentResponseUpdate.
  • Update Sequential/Concurrent/GroupChat/Magentic/Handoff orchestrations to reserve output for terminal answers and use data for intermediate agent activity.
  • Update orchestration tests and a sample to validate/illustrate the new terminal vs intermediate event behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
python/samples/03-workflows/agents/sequential_workflow_as_agent.pyUpdates sample to reflect as_agent() returning only the final agent response and notes intermediate observation via data.
python/packages/orchestrations/tests/test_sequential.pyRewrites sequential tests to assert terminal output is last agent only and intermediate activity is data when enabled.
python/packages/orchestrations/tests/test_magentic.pyUpdates Magentic tests to expect AgentResponse terminal outputs and data events for intermediate updates.
python/packages/orchestrations/tests/test_handoff.pyUpdates Handoff tests to reflect no synthetic terminal output and per-agent outputs as the stream result.
python/packages/orchestrations/tests/test_group_chat.pyUpdates GroupChat tests to expect a single terminal AgentResponse from the orchestrator and intermediate agent updates as data.
python/packages/orchestrations/tests/test_concurrent.pyUpdates Concurrent tests to expect default aggregator returns AgentResponse with assistant messages only (no user prompt).
python/packages/orchestrations/agent_framework_orchestrations/_sequential.pyChanges sequential wiring so the last agent executor is the workflow output executor; uses data events for intermediate participants.
python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.pyPlumbs emit_intermediate_data through AgentApprovalExecutor into the inner AgentExecutor.
python/packages/orchestrations/agent_framework_orchestrations/_magentic.pyChanges Magentic terminal output to AgentResponse; adds participant emit_intermediate_data plumbing; restricts output executor to orchestrator.
python/packages/orchestrations/agent_framework_orchestrations/_handoff.pyRemoves terminal yield; relies on per-agent output events as the observable result.
python/packages/orchestrations/agent_framework_orchestrations/_group_chat.pyChanges GroupChat completion to yield an AgentResponse; uses participant emit_intermediate_data; restricts outputs to orchestrator.
python/packages/orchestrations/agent_framework_orchestrations/_concurrent.pyChanges default aggregator to yield AgentResponse (assistant replies only) and uses participant emit_intermediate_data for intermediate observation.
python/packages/orchestrations/agent_framework_orchestrations/_base_group_chat_orchestrator.pyUpdates base orchestrator termination/max-round outputs to yield AgentResponse completion messages.
python/packages/core/tests/workflow/test_agent_executor.pyAdds core tests asserting emit_intermediate_data produces data events in both streaming and non-streaming modes.
python/packages/core/agent_framework/_workflows/_agent_executor.pyImplements emit_intermediate_data by emitting WorkflowEvent.emit(...)/type='data' alongside existing outputs.
python/packages/core/agent_framework/_workflows/_agent.pyUpdates WorkflowAgent to consume data events carrying AgentResponse / AgentResponseUpdate as part of agent responses.

Comment threadpython/samples/03-workflows/agents/sequential_workflow_as_agent.py Outdated

@moonbox3Evan Mattson (moonbox3) left a comment

Copy link
Copy Markdown
ContributorAuthor

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 PR refactors orchestration output from list[Message] to AgentResponse and introduces emit_intermediate_data on AgentExecutor to surface intermediate participants via data events. The core mechanics are sound: output_executors is now always set (not conditioned on intermediate_outputs), intermediate agents emit data events, and the _agent.py adapter layer correctly processes both output and data events. The existing unresolved review comments remain valid (AgentApprovalExecutor as last participant in sequential, sample file stale imports/docstrings, data-event propagation through WorkflowExecutor, and docstring ordering in concurrent aggregator). I found no additional correctness bugs beyond those already raised.

✓ Security Reliability

This PR refactors orchestration outputs from list[Message] to AgentResponse and introduces emit_intermediate_data on AgentExecutor to surface intermediate participants as data events while reserving output events for the terminal answer. The design is clean and the output_executors / data-event split is well-implemented. The key security/reliability issues already identified in previous review comments (AgentApprovalExecutor as terminal participant producing no output, docstring ordering inaccuracy, data event forwarding in WorkflowExecutor) remain unresolved and still apply. One new concern: the handoff workflow removes all terminal yield_output calls, meaning a termination condition that fires before any agent speaks (e.g., from user input alone) will cause the workflow to go idle with zero output events — consumers relying on at least one output would see silent completion.

✓ Test Coverage

The PR introduces a significant refactoring of orchestration output contracts (from list[Message] to AgentResponse) and adds emit_intermediate_data wiring to surface per-agent responses as data events. Test coverage for sequential, group-chat, and magentic workflows is solid, with good new tests for non-streaming, streaming, intermediate outputs, and as_agent scenarios. However, there are notable gaps: ConcurrentBuilder's intermediate_outputs=True path is completely untested despite new wiring, the handoff async-termination test was weakened to a bare IDLE-state check, and sequential intermediate_outputs is only tested in non-streaming mode.


Automated review by moonbox3's agents

Comment threadpython/packages/orchestrations/tests/test_concurrent.py
Copilotand others added 7 commits April 16, 2026 08:39
1. Sample cleanup: Remove commented-out FoundryChatClient block and update
prerequisites to reference OPENAI_CHAT_MODEL_ID instead of FOUNDRY_* vars.
2. Sequential approval output: Change _EndWithConversation.end_with_agent_executor_response
from a no-op sink to yield response.agent_response. When the last participant is
AgentApprovalExecutor (via with_request_info), _EndWithConversation is the output
executor so the yield produces the terminal answer. When the last participant is a
regular AgentExecutor, _EndWithConversation is not in output_executors so the yield
is silently filtered out.
3. Forward data events through WorkflowExecutor: _process_workflow_result now also
forwards 'data' events from sub-workflows so that emit_intermediate_data=True on
AgentExecutor works correctly when wrapped in AgentApprovalExecutor.
4. Concurrent docstring: Update _AggregateAgentConversations docstring to say
'deterministic participant order' instead of 'completion order'.
5. Add test_concurrent_intermediate_outputs_emits_data_events verifying that
ConcurrentBuilder(intermediate_outputs=True) emits per-participant data events
alongside the single aggregated output event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…outputs (microsoft#5301)
Address PR review comments 2, 3, and 5:
- Add test_sequential_request_info_last_participant_emits_output:
Verifies that when the last participant is wrapped via with_request_info()
(AgentApprovalExecutor), the workflow still emits a terminal output after
approval, exercising the _EndWithConversation.end_with_agent_executor_response
fallback path.
- Add test_sequential_request_info_with_intermediate_outputs_emits_data_events:
Verifies that emit_intermediate_data=True works correctly through
AgentApprovalExecutor wrapping—WorkflowExecutor._process_result already
forwards data events from sub-workflows, so intermediate agent responses
surface as data events in the parent workflow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#5301)
Update cast() calls in _group_chat.py and _magentic.py to use
WorkflowContext[Never, AgentResponse] instead of the old
WorkflowContext[Never, list[Message]], matching the updated method
signatures in _base_group_chat_orchestrator.py.
Fix _sequential.py _EndWithConversation.end_with_agent_executor_response
to declare WorkflowContext[Any, AgentResponse] so yield_output accepts
AgentResponse[None].
Fix _workflow_executor.py data event forwarding to handle nullable
executor_id.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract event.data into a typed local variable before the isinstance
check to avoid pyright narrowing it to AgentResponse[Unknown].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#5301)
Add pyright: ignore[reportMissingImports] to orjson imports that are
already guarded by try/except ImportError, matching the existing pattern
used elsewhere in the samples.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

small nit on the env var names, otherwise good to go

Comment threadpython/samples/03-workflows/agents/sequential_workflow_as_agent.py Outdated
Comment threadpython/samples/03-workflows/agents/sequential_workflow_as_agent.py Outdated
Reverts the mistaken switch from FoundryChatClient to OpenAIChatClient
in the sequential workflow as agent sample.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadpython/packages/core/agent_framework/_workflows/_agent_executor.py Outdated
Comment threadpython/packages/core/agent_framework/_workflows/_agent.py Outdated
Comment threadpython/packages/core/agent_framework/_workflows/_agent.py Outdated
… reasoning conversion
Layered on top of the prior review-feedback work in this branch.
Renames:
- AgentExecutor.emit_intermediate_data -> emit_data_events (mechanical
rename; orchestration semantics live at the orchestration layer, not
the general-purpose executor). Forwarded through MagenticAgentExecutor,
AgentApprovalExecutor, and all orchestration call sites.
- HandoffAgentExecutor._check_terminate_and_yield -> _should_terminate
(pure predicate; no longer yields anything). HandoffBuilder docstring
rewritten to describe the new per-agent AgentResponse output contract.
WorkflowAgent reasoning-content conversion:
- Add _rewrite_text_to_reasoning(contents) and _msg_as_reasoning(msg)
helpers; the as_agent() path now reframes text content from data events
as text_reasoning Content blocks before merging into the AgentResponse.
- Consumers iterate msg.contents and branch on content.type — same path
they already use for Claude thinking and OpenAI reasoning. No new
field on Message/AgentResponse/WorkflowEvent.
- Streaming branch constructs fresh AgentResponseUpdate instances instead
of mutating shared payloads (regression test added).
- Helper _msg_maybe_reasoning consolidates the conditional rewrite at
three call sites in the non-streaming conversion.
Tests:
- TestWorkflowAgentReasoningHelpers + TestWorkflowAgentDataEventReasoningConversion
add 9 new tests covering helpers, non-streaming, streaming, mixed content,
already-reasoning passthrough, and mutation-safety regression.
- Updated test_sequential_as_agent_with_intermediate_outputs_includes_chain
to assert text_reasoning content for intermediate agents.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pythonUsage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@moonbox3@TaoChenOSU@eavanvalkenburg@alliscode@lokitoth