Python: Fix RUN_FINISHED.interrupt to accumulate all interrupts when multiple tools need approval#4717
Python: Fix RUN_FINISHED.interrupt to accumulate all interrupts when multiple tools need approval#4717moonbox3 wants to merge 3 commits intomicrosoft:mainfrom
Conversation
…osoft#4590) Change flow.interrupts assignment to append so that all interrupt entries accumulate when multiple tools require approval in a single turn. Both _run_common.py and _agent_run.py used assignment (=) which caused each new interrupt to overwrite the previous one. Switching to append() ensures RUN_FINISHED.interrupt contains all pending approvals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Python Test Coverage Report •
Python Unit Test Overview
|
|||||||||||||||||||||||||||||||||||
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 83%
✗ Correctness
The diff changes
flow.interrupts = [...](list assignment) toflow.interrupts.append(...)in two locations, enabling accumulation of multiple interrupts per turn. The logic is sound and the test correctly validates the new behavior. However, the diff does not show a corresponding change to theFlowStatedataclass to add aninterruptsfield withdefault_factory=list. The installed version ofFlowStatehas nointerruptsfield — the old assignment (= [...]) worked because it dynamically created the attribute, but.append()will raiseAttributeErrorif the attribute doesn't already exist. If this field was added in another commit in the same PR, the change is correct; otherwise this is a blocking bug.
✓ Security Reliability
The diff changes
flow.interruptsfrom list replacement (= [...]) to list append (.append(...)) in two locations, enabling accumulation of multiple interrupt entries per turn. A corresponding test validates the new behavior. The change is straightforward and correct, with no security issues. The only reliability consideration is thatflow.interruptsmust be pre-initialized as a list (e.g., viafield(default_factory=list)) onFlowStatefor.append()to succeed on the first call — the old assignment-based code would have worked even without prior initialization. Since theFlowStatedefinition change is not included in this diff, this is worth confirming but the new test exercisingFlowState(message_id="msg-1")implies the field is properly defined.
✓ Test Coverage
The diff changes
flow.interruptsfrom assignment (= [...]) to append (.append(...)) in two locations:_run_common.py(_emit_approval_request) and_agent_run.py(run_agent_stream). A new test covers the accumulation behavior for_emit_approval_request, but no test covers the identical change in_agent_run.py's streaming path. The added test is well-structured with meaningful assertions (checks count and interrupt IDs), but only exercises one of the two modified code paths.
✓ Design Approach
The change fixes a real bug: every call to
_emit_approval_requestwas overwritingflow.interruptswith a new single-element list instead of accumulating entries, meaning only the last approval request in a turn was ever surfaced. The fix (.append()) is the correct approach and aligns with the list-accumulation pattern implied byflow.interruptsbeing a list. The accompanying test directly validates the multi-interrupt accumulation scenario. No design issues are present — this is a straightforward and correct targeted fix with no leaky abstractions, fragile assumptions, or symptom-level masking.
Flagged Issues
- FlowState dataclass does not appear to define an
interruptsfield in this diff. The oldflow.interrupts = [...]dynamically created the attribute via assignment, but.append()requires it to already exist. IfFlowStatewas not updated elsewhere in this PR to includeinterrupts: list[dict[str, Any]] = field(default_factory=list), both call sites will raiseAttributeErroron first invocation.
Suggestions
- Ensure
FlowStatedeclaresinterrupts: list[dict[str, Any]] = field(default_factory=list)so that.append()works on a freshly constructed instance. If this was added in a separate commit not shown in this diff, confirm it is present. - Consider whether
flow.interruptsshould be cleared between turns or agent runs to prevent stale interrupt data from carrying over. SinceFlowStaterepresents state for a single AG-UI run this is likely safe, but worth verifying the lifecycle. - Add a test exercising the
run_agent_streamcode path in_agent_run.py(line 924) to verify that multiple approval requests in a streaming run also accumulate inflow.interrupts. Currently only the_emit_approval_requesthelper in_run_common.pyis covered, so a regression in the streaming path would go undetected.
Automated review by moonbox3's agents
There was a problem hiding this comment.
Pull request overview
This PR fixes AG-UI’s interrupt metadata so that when multiple tool calls require approval in the same agent turn, all approval interrupts are preserved and surfaced in RUN_FINISHED.interrupt (instead of only the last one), enabling clients to see/approve every pending tool call.
Changes:
- Update
_emit_approval_requestto append toflow.interruptsrather than overwriting it. - Update the predictive-tool confirmation path in
run_agent_streamto append additional interrupts rather than overwriting. - Add a unit test asserting that multiple approval requests accumulate into multiple
flow.interruptsentries.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
python/packages/ag-ui/agent_framework_ag_ui/_run_common.py |
Accumulates approval interrupts by appending to FlowState.interrupts. |
python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py |
Accumulates multiple confirmation interrupts during agent streaming runs. |
python/packages/ag-ui/tests/ag_ui/test_run.py |
Adds regression test ensuring multiple approval requests produce multiple interrupts. |
You can also share your feedback on Copilot code review. Take the survey.
…icrosoft#4590) Add integration test exercising run_agent_stream with multiple predictive tool calls requiring confirmation. Verifies that flow.interrupts.append() correctly accumulates all interrupt entries and they appear in the RUN_FINISHED event. Also confirms FlowState already declares interrupts field with default_factory=list, addressing the AttributeError concern from review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 97% | Result: All clear
Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach
Automated review by moonbox3's agents
Motivation and Context
When multiple tool calls required approval in the same turn,
flow.interruptswas being reassigned with a single-element list for each approval request, causing only the last interrupt to appear in theRUN_FINISHEDevent. This meant clients could not see or approve earlier tool calls.Fixes #4590
Description
The root cause was that both
_emit_approval_requestin_run_common.pyand the confirmation handling in_agent_run.pyusedflow.interrupts = [...](list reassignment) instead of appending to the existing list. The fix changes these assignments toflow.interrupts.append(...)so that each approval request is accumulated. A new test verifies that three consecutive approval requests result in three distinct interrupts inflow.interrupts.Contribution Checklist