From cc1523b08a192da638896653474a7ae8f4cf8686 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 21 Sep 2026 15:17:02 +0200 Subject: [PATCH] Python: gate sequential output on approval --- .../_orchestration_request_info.py | 7 +++---- .../orchestrations/tests/test_sequential.py | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py index 234e64b284e..205d110e63b 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py @@ -221,13 +221,12 @@ def _build_workflow(self, agent: SupportsAgentRun, *, terminal: bool) -> Workflo request_info_cls = _TerminalAgentRequestInfoExecutor if terminal else AgentRequestInfoExecutor request_info_executor = request_info_cls(id="agent_request_info_executor") - # Both inner executors yield the inner workflow's terminal output (the agent - # during its turn; the _TerminalAgentRequestInfoExecutor after approval), so - # both must be designated for WorkflowExecutor.get_outputs() to surface them. + # Only the request info executor surfaces approved responses. The agent's + # unapproved response remains available through the request_info event. return ( WorkflowBuilder( start_executor=agent_executor, - output_from=[agent_executor, request_info_executor], + output_from=[request_info_executor], ) # Create a loop between agent executor and request info executor .add_edge(agent_executor, request_info_executor) diff --git a/python/packages/orchestrations/tests/test_sequential.py b/python/packages/orchestrations/tests/test_sequential.py index 39561df34ff..162b2e8971b 100644 --- a/python/packages/orchestrations/tests/test_sequential.py +++ b/python/packages/orchestrations/tests/test_sequential.py @@ -539,11 +539,7 @@ async def test_chain_only_agent_responses_three_agents() -> None: async def test_sequential_request_info_last_participant_emits_output() -> None: """When the last participant is wrapped via with_request_info(), the workflow - still emits a terminal output event after approval. - - This exercises the _EndWithConversation.end_with_agent_executor_response path - that converts the AgentApprovalExecutor's forwarded AgentExecutorResponse into - the workflow's final AgentResponse output. + emits a terminal output event only after approval. """ from agent_framework_orchestrations._orchestration_request_info import AgentRequestInfoResponse @@ -552,14 +548,20 @@ async def test_sequential_request_info_last_participant_emits_output() -> None: wf = SequentialBuilder(participants=[a1, a2]).with_request_info().build() - # First run: collect request_info events for both agents + # First run: collect the first request_info event. request_events: list[Any] = [] + output_events: list[Any] = [] async for ev in wf.run("hello with approval", stream=True): if ev.type == "request_info" and isinstance(ev.data, AgentExecutorResponse): request_events.append(ev) + elif ev.type == "output": + output_events.append(ev) - # Approve each agent in sequence until the workflow completes - output_events: list[Any] = [] + assert request_events + assert output_events == [] + + # Approve each agent in sequence until the workflow completes. A run that + # pauses for another approval must not expose that unapproved response. while request_events: responses = {req.request_id: AgentRequestInfoResponse.approve() for req in request_events} request_events = [] @@ -569,6 +571,8 @@ async def test_sequential_request_info_last_participant_emits_output() -> None: request_events.append(ev) elif ev.type == "output": output_events.append(ev) + if request_events: + assert output_events == [] # The workflow must produce a terminal output with the last agent's response. assert len(output_events) == 1