Python: Prefer explicit AG-UI resume payloads (#6360)

* Prefer explicit AG-UI resume payloads

* test: tighten AG-UI resume assertions

---------

Co-authored-by: gezw <26155255+gezw@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
gezw
2026-07-09 07:06:13 +08:00
committed by GitHub
parent c64f8d9e86
commit ab90300a71
2 changed files with 92 additions and 2 deletions
@@ -226,6 +226,19 @@ def _resume_to_workflow_responses(resume_payload: Any) -> dict[str, Any]:
return responses
def _merge_workflow_response_sources(
resume_responses: dict[str, Any],
message_responses: dict[str, Any],
) -> dict[str, Any]:
"""Merge workflow response sources with explicit resume payloads taking precedence."""
if not resume_responses:
return dict(message_responses)
responses = dict(message_responses)
responses.update(resume_responses)
return responses
def _resume_entries_to_workflow_responses(entries: list[dict[str, Any]]) -> dict[str, Any]:
"""Convert validated resume entries into workflow responses."""
responses: dict[str, Any] = {}
@@ -773,12 +786,12 @@ async def run_workflow_stream(
yield resume_error
return
responses = (
resume_responses = (
_resume_entries_to_workflow_responses(resume_entries)
if pending_interrupt_ids
else _resume_to_workflow_responses(resume_payload)
)
responses.update(_extract_responses_from_messages(messages))
responses = _merge_workflow_response_sources(resume_responses, _extract_responses_from_messages(messages))
responses, response_error = _coerce_responses_for_pending_requests_strict(responses, pending_before_run)
if response_error is not None:
yield RunStartedEvent(run_id=run_id, thread_id=thread_id)
@@ -1403,6 +1403,83 @@ async def test_workflow_run_approval_resume_entry_approved() -> None:
assert "outcome" not in resumed_finished
async def test_workflow_run_explicit_resume_overrides_stale_message_approval() -> None:
"""Explicit resume payloads should not be overwritten by stale function_approvals in messages."""
class ApprovalExecutor(Executor):
def __init__(self) -> None:
super().__init__(id="approval_executor")
@handler
async def start(self, message: Any, ctx: WorkflowContext) -> None:
del message
function_call = Content.from_function_call(
call_id="refund-call",
name="submit_refund",
arguments={"order_id": "12345", "amount": "$89.99"},
)
approval_request = Content.from_function_approval_request(id="approval-1", function_call=function_call)
await ctx.request_info(approval_request, Content, request_id="approval-1")
@response_handler
async def handle_approval(
self, original_request: Content, response: Content, ctx: WorkflowContext[Any, str]
) -> None:
del original_request
status = "approved" if bool(response.approved) else "rejected"
await ctx.yield_output(f"Refund {status}.")
workflow = WorkflowBuilder(start_executor=ApprovalExecutor()).build()
first_events: list[Any] = [
event async for event in run_workflow_stream({"messages": [{"role": "user", "content": "go"}]}, workflow)
]
first_finished_events = [event for event in first_events if event.type == "RUN_FINISHED"]
assert len(first_finished_events) == 1
interrupt_payload = _interrupts_from_run_finished(first_finished_events[0])
assert len(interrupt_payload) == 1
interrupt_value = _interrupt_metadata_value(interrupt_payload[0])
resumed_events: list[Any] = [
event
async for event in run_workflow_stream(
{
"messages": [
{
"role": "user",
"content": "",
"function_approvals": [
{
"approved": True,
"id": "approval-1",
"call_id": "refund-call",
"name": "submit_refund",
"arguments": {"order_id": "12345", "amount": "$89.99"},
}
],
}
],
"resume": [
{
"interruptId": "approval-1",
"status": "resolved",
"payload": {
"type": "function_approval_response",
"approved": False,
"id": interrupt_value.get("id", "approval-1"),
"function_call": interrupt_value.get("function_call"),
},
}
],
},
workflow,
)
]
assistant_text = "".join(event.delta for event in resumed_events if event.type == "TEXT_MESSAGE_CONTENT")
assert "rejected" in assistant_text
assert "approved" not in assistant_text
async def test_workflow_run_approval_argument_mismatch_emits_run_error() -> None:
"""Workflow approval responses must fail when function arguments change."""