Skip to content

Python: Orchestration output ADR#4799

Draft
TaoChenOSU wants to merge 2 commits intomicrosoft:mainfrom
TaoChenOSU:taochen/orchestration-output-adr
Draft

Python: Orchestration output ADR#4799
TaoChenOSU wants to merge 2 commits intomicrosoft:mainfrom
TaoChenOSU:taochen/orchestration-output-adr

Conversation

@TaoChenOSU
Copy link
Contributor

Motivation and Context

Description

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.

@TaoChenOSU TaoChenOSU self-assigned this Mar 19, 2026
@TaoChenOSU TaoChenOSU added python agent orchestration Issues related to agent orchestration workflows Related to Workflows in agent-framework labels Mar 19, 2026
@TaoChenOSU TaoChenOSU moved this to In Progress in Agent Framework Mar 19, 2026
@TaoChenOSU TaoChenOSU moved this from In Progress to In Review in Agent Framework Mar 19, 2026
@github-actions github-actions bot changed the title Orchestration output ADR Python: Orchestration output ADR Mar 19, 2026
@markwallace-microsoft markwallace-microsoft added the documentation Improvements or additions to documentation label Mar 19, 2026

Orchestrations (Concurrent, Sequential, Handoff, GroupChat, Magentic) are not standalone features — they are prebuilt workflow patterns built on top of the workflow system APIs. They serve as both ready-to-use solutions and as reference implementations that demonstrate how to correctly compose agents using the workflow primitives (`Executor`, `WorkflowBuilder`, `yield_output`, `as_agent()`, etc.).

This dual role makes orchestrations critically important: the patterns they establish become the patterns that developers follow when building their own workflows. In practice, developers use the framework to build workflows that coordinate Foundry agents, and ultimately deploy those workflows as hosted agents on Azure AI Foundry. This path — from workflow definition to agent deployment — relies on a seamless integration between workflows and agents. If orchestrations model this integration poorly (e.g., producing outputs that don't compose cleanly with `as_agent()`), developers building custom workflows will inherit the same problems.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
This dual role makes orchestrations critically important: the patterns they establish become the patterns that developers follow when building their own workflows. In practice, developers use the framework to build workflows that coordinate Foundry agents, and ultimately deploy those workflows as hosted agents on Azure AI Foundry. This path — from workflow definition to agent deployment — relies on a seamless integration between workflows and agents. If orchestrations model this integration poorly (e.g., producing outputs that don't compose cleanly with `as_agent()`), developers building custom workflows will inherit the same problems.
This dual role makes orchestrations critically important: the patterns they establish become the patterns that developers follow when building their own workflows. In practice, developers use the framework to build workflows that coordinate Foundry agents, and ultimately deploy those workflows as hosted agents on Microsoft Foundry. This path — from workflow definition to agent deployment — relies on a seamless integration between workflows and agents. If orchestrations model this integration poorly (e.g., producing outputs that don't compose cleanly with `as_agent()`), developers building custom workflows will inherit the same problems.


```python
workflow = SequentialBuilder(participants=[agent1, agent2, agent3]).build()
events = await workflow.run(message="Write a report")
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this missing a stream=True?

- **Non-streaming**: Collects all output events, then merges their data into a single `AgentResponse`. The full conversation dump from the orchestration's final output becomes `AgentResponse.messages` alongside any intermediate agent responses — producing a response that conflates progress with the actual answer.
- **Streaming**: Converts each output event into `AgentResponseUpdate` objects and yields them as they arrive. All updates — whether from intermediate agents or the final conversation dump — are yielded indiscriminately as streaming chunks.

In both modes, `WorkflowAgent` processes all output events without distinguishing intermediate from final. When `intermediate_outputs=True`, this means intermediate agent responses and the final conversation dump are merged together. Even when `intermediate_outputs=False`, the final output is still the full conversation rather than the meaningful answer.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this true for single agents? I only get back one AgentResponse with one content that is the final answer?

Copy link
Member

Choose a reason for hiding this comment

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

No, in single agent, you get all contents from all model calls/tool results in a single AgentResponse (through get_final_response when streaming)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For workflows, you will still receive intermediate works by setting intermediate_outputs=True. Users can configure it depending on their scenarios.

- Adds a new concept to the workflow framework (`run_output` vs `output`).
- `WorkflowAgent`, sub-workflow consumers, and event processing logic all need to handle two output event types.

### Option 3: Add `is_run_completed` flag to existing output event
Copy link
Contributor

Choose a reason for hiding this comment

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

When an AgentExecutor streams, it calls yield_output(update) for every AgentResponseUpdate chunk. The last streaming chunk is not itself the "final answer," it's just the last piece. Who assembles the full AgentResponse and sets is_run_completed=True? The ADR doesn't address this.

Currently AgentExecutor yields individual updates in streaming mode, so the orchestration layer above would need to aggregate and re-yield. How would this best work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, the agent executors don't know. Only the author of the workflow knows what makes up the run output. Our orchestration layer doesn't use the AgentExecutor directly or as the last executor in the flow (which I think it's not so good of a sign):

  1. Sequential: the end executor which knows it must be the end of the workflow run.
  2. Concurrent: the aggregator knows the end of the workflow run.
  3. Handoff: subclasses the AgentExecutor so it knows when it's the end of the run.
  4. GroupChat & Magentic: the manager knows the end.

There isn't an orchestration layer. The orchestrations are just like any workflow. When an AgentExecutor yields an update, it creates an output event.


### Option 3: Add `is_run_completed` flag to existing output event

Add an optional `is_run_completed: bool` parameter to the existing `yield_output()` method and `WorkflowEvent`:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd say that nothing in the framework would enforce that exactly one event has is_run_completed=True, or that it's the last output event. Custom workflow authors could forget it, set it multiple times, or set it on an intermediate event. Should the framework validate this invariant (in WorkflowRunResult or the event stream)? The ADR says "executors must remember to set it" in the cons section. Wouldn't that be a footgun for custom workflows?

Copy link
Member

Choose a reason for hiding this comment

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

I agree, this seems like a recipe for mistakes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, we can create a warning if there are more than one output event with is_run_completed=True.


Each orchestration pattern changes what data it yields as the final output and sets `is_run_completed=True`:

| Orchestration | Current Final Output | New Final Output | Rationale |
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there a breaking change here? WorkflowRunResult.get_outputs() currently returns list[Any] (all output event data). Changing final outputs from list[Message] to AgentResponse is a breaking change for any caller doing for msg in result.get_outputs()[0] or similar iteration. The ADR doesn't call this out as breaking, and the PR title doesn't have [BREAKING]. Just want to make sure we're okay with this.

Copy link
Member

Choose a reason for hiding this comment

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

Fundamentally it does not make sense to me that a Workflow defaults to returning a AgentResponse

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will mark this as breaking.

And to @eavanvalkenburg's comment, we don't force the workflow to output anything. The orchestrations are just a way to build workflows, and we think it makes sense to have the orchestrations to output AgentResponse better than a list of messages because orchestrations are designed to work with agents, and we are seeing increasingly people building orchestrations and turning them into agents and deploying them to Foundry.

Workflows don't return anything. It's event based. A workflow can generate output events containing anything.


Orchestrations can be wrapped as agents using `workflow.as_agent()`. The `WorkflowAgent` processes workflow output events differently depending on the mode:

- **Non-streaming**: Collects all output events, then merges their data into a single `AgentResponse`. The full conversation dump from the orchestration's final output becomes `AgentResponse.messages` alongside any intermediate agent responses — producing a response that conflates progress with the actual answer.
Copy link
Contributor

Choose a reason for hiding this comment

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

This says when intermediate_outputs=False, "only the is_run_completed=True event is surfaced." But today, WorkflowAgent collects all type='output' events and converts them. The proposed behavior requires WorkflowAgent to either:

  • Buffer all events and only use the ₩is_run_completed=True` one (discarding earlier outputs), or
  • Filter during collection

Which approach? Buffering means memory overhead for long-running orchestrations. Filtering means you lose the ability to retroactively include intermediate outputs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure if I understand the question. This line is a description of the current behavior, not the proposal.

In the proposed solution, the WorkflowAgent will not change and will not discriminate outputs. Users must define their workflow in a way they want before turning it to an agent.


| Orchestration | Current Final Output | New Final Output | Rationale |
|---|---|---|---|
| **Concurrent** | `list[Message]` (user prompt + one reply per agent) | `AgentResponse` containing all sub-agent response messages | The combined responses from all parallel agents represent the orchestration's answer. Messages are copied from each sub-agent's `AgentResponse`. |
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens when is_run_completed event has AgentResponse with messages from multiple agents? Could this happen?

Here it says the final output is an AgentResponse "containing all sub-agent response messages." An AgentResponse has a single agent_id field. Whose agent_id is it? The orchestration's? And AgentResponse.messages would mix messages from different agents, correct? Is that semantically sound, or should there be a different container?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this makes sense, perhaps I need to reword this a bit.

Concurrent allows custom aggregation strategies so not every concurrent will simply aggregate the messages into a list. Of course, the default one does simple aggregation. We can use the workflow name as the agent response in that case, and for the individual messages, they will be put into the list unchanged.

A different container also makes sense. There is no correct answer here. I think AgentResponse is a natural integration, the prerequisite is that users need to know that they are running a concurrent orchestration.

| **GroupChat** | `list[Message]` (all rounds + completion message) | `AgentResponse` containing the summary or completion message | The orchestrator's summary/end message is the meaningful result. Individual round messages are intermediate outputs visible when `intermediate_outputs=True`. |
| **Magentic** | `list[Message]` (chat history + final answer) | `AgentResponse` containing the synthesized final answer | The manager's synthesized final answer is the meaningful result. Individual agent work is intermediate. |

### Integration Points
Copy link
Contributor

Choose a reason for hiding this comment

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

If all orchestrations change their output from list[Message] to AgentResponse, existing consumers break. Are we including a deprecation period? A version flag? Or is this a clean break right before GA?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are not GAing orchestrations.

Copy link
Contributor

Choose a reason for hiding this comment

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

The intent is to GA orchestrations, please check with Shawn. Even if that doesn't mean the same day as core GA, it will fast-follow.


Orchestrations can be wrapped as agents using `workflow.as_agent()`. The `WorkflowAgent` processes workflow output events differently depending on the mode:

- **Non-streaming**: Collects all output events, then merges their data into a single `AgentResponse`. The full conversation dump from the orchestration's final output becomes `AgentResponse.messages` alongside any intermediate agent responses — producing a response that conflates progress with the actual answer.
Copy link
Member

Choose a reason for hiding this comment

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

this is what happens in single agents, as well, there might be multiple turns and tool calls, and you wait until all of that is done, and then you return the full set of Messages in one AgentResponse.

Orchestrations can be wrapped as agents using `workflow.as_agent()`. The `WorkflowAgent` processes workflow output events differently depending on the mode:

- **Non-streaming**: Collects all output events, then merges their data into a single `AgentResponse`. The full conversation dump from the orchestration's final output becomes `AgentResponse.messages` alongside any intermediate agent responses — producing a response that conflates progress with the actual answer.
- **Streaming**: Converts each output event into `AgentResponseUpdate` objects and yields them as they arrive. All updates — whether from intermediate agents or the final conversation dump — are yielded indiscriminately as streaming chunks.
Copy link
Member

Choose a reason for hiding this comment

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

This is also the same, regardless of where the Update originates, the user get's it back (there are really only 2 places in single agents, but still)

- **Non-streaming**: Collects all output events, then merges their data into a single `AgentResponse`. The full conversation dump from the orchestration's final output becomes `AgentResponse.messages` alongside any intermediate agent responses — producing a response that conflates progress with the actual answer.
- **Streaming**: Converts each output event into `AgentResponseUpdate` objects and yields them as they arrive. All updates — whether from intermediate agents or the final conversation dump — are yielded indiscriminately as streaming chunks.

In both modes, `WorkflowAgent` processes all output events without distinguishing intermediate from final. When `intermediate_outputs=True`, this means intermediate agent responses and the final conversation dump are merged together. Even when `intermediate_outputs=False`, the final output is still the full conversation rather than the meaningful answer.
Copy link
Member

Choose a reason for hiding this comment

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

No, in single agent, you get all contents from all model calls/tool results in a single AgentResponse (through get_final_response when streaming)


### Option 3: Add `is_run_completed` flag to existing output event

Add an optional `is_run_completed: bool` parameter to the existing `yield_output()` method and `WorkflowEvent`:
Copy link
Member

Choose a reason for hiding this comment

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

I agree, this seems like a recipe for mistakes

- Adds a new concept to the workflow framework (`run_output` vs `output`).
- `WorkflowAgent`, sub-workflow consumers, and event processing logic all need to handle two output event types.

### Option 3: Add `is_run_completed` flag to existing output event
Copy link
Member

Choose a reason for hiding this comment

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

nit: but is run the right verbiage for this, shouldn't it be step?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you elaborate?


## Definition of a Run

A **run** represents a single invocation of the workflow — from receiving an initial request to the workflow returning to idle status. The `is_run_completed` flag on an output event signals that this output represents the final result of the current run.
Copy link
Member

Choose a reason for hiding this comment

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

How would this work, what if multiple executors set this at the same time?

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 is just like outputs. Any executor in the workflow can create outputs. We can't prevent this because we don't know the internals of executors.

Similar to the comment above, we can create a warning when we see two output events with is_run_completed=True.


Each orchestration pattern changes what data it yields as the final output and sets `is_run_completed=True`:

| Orchestration | Current Final Output | New Final Output | Rationale |
Copy link
Member

Choose a reason for hiding this comment

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

Fundamentally it does not make sense to me that a Workflow defaults to returning a AgentResponse


When `WorkflowAgent` converts workflow events to an `AgentResponse`:

- Events with `is_run_completed=True` provide the `AgentResponse` that becomes the agent's response directly, with the name of the workflow as the author of the response.
Copy link
Member

Choose a reason for hiding this comment

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

this would make this inconsistent with single agents, because there you alwasy get all intermediate work in a AgentResponse

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can still get intermediate work too with workflows as agents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent orchestration Issues related to agent orchestration documentation Improvements or additions to documentation python workflows Related to Workflows in agent-framework

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

4 participants