You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a model emits two or more approval_mode="always_require" calls in one assistant message, AG-UI correctly surfaces one interrupt per call, and a client that approves all of them gets exactly one executed. The others are dropped with no error, no event and no trace in the transcript: no TOOL_CALL_START, no result, and nothing in the final MESSAGES_SNAPSHOT.
The model therefore never learns the outcome of a call it made and the user approved, and a UI that renders tool calls from the event stream is left with a chip that never resolves.
A batch of one gated call plus never-require siblings works correctly — the siblings ride along in the pending entry's already_approved_requests and are released on the resume. It is specifically two or more gated calls that lose all but the first.
Reproduction
Self-contained, no network or credentials — a scripted chat client emitting a two-call batch, two always_require tools, and the stock AG-UI agent:
batch= [
Content.from_function_call(call_id="c1", name="deploy_change", arguments={"target": "a"}),
Content.from_function_call(call_id="c2", name="deploy_other", arguments={"target": "b"}),
]
# turn 1: run with a single user message -> RUN_FINISHED(outcome=interrupt, interrupts=[c1, c2])# turn 2: resume with BOTH ids resolved/acceptedresume= [
{"interruptId": "c1", "status": "resolved", "payload": {"accepted": True}},
{"interruptId": "c2", "status": "resolved", "payload": {"accepted": True}},
]
Observed on the resume run, verbatim, against main @ d56e81357e5179b6f916b180c0cb34ea2e73c80b:
Expected: both tools execute, both results stream, and the snapshot carries two role="tool" messages.
The chat client's view of the resume turn confirms the loss is not a display artifact — it is handed user/text and a single tool/function_result for c1, with no approval response, no result and no function call for c2 anywhere in the transcript.
Where it is lost
Not in the AG-UI resume synthesis. Instrumenting _canonical_approval_resume_messages on the resume shows it doing exactly the right thing for both entries:
Both interrupt ids are validated, both approval responses are built, and no resume error is raised. Execution then produces one result. So the drop is downstream of the AG-UI layer, where the synthesized approval responses are turned into function invocations — the second response is present in the input and does not lead to a call.
That also explains the asymmetry with never-require siblings: those are carried inside the gated entry as already_approved_requests (_PendingApprovalWithSiblings) and are released as part of one approval, whereas two gated calls are two independent entries whose responses must each drive an execution.
Impact
An approved tool call that silently does not happen is worse than one that fails: nothing surfaces to the user, the model proceeds as though the call never existed, and any UI that pairs TOOL_CALL_START with a result shows an unresolvable pending state.
It also bites hardest exactly where batching is most natural — a plan that approves several writes at once, or a model that emits parallel calls for speed. In our own deployment the mitigation is an app-side shim that pops the parked approvals and executes them itself, which then has to re-stream and re-seat the results because the framework has no record of a call it never ran.
Prior art (checked before filing)
Python: [Bug]: Mixed Tool Batch Applies Approval Wrapper To All Tool Calls #6385 (closed) — a mixed batch over-wrapping never-require siblings in approval requests at surfacing time; fixed by the per-item grouping this report references, and its scenario passes today. This report is the residual case: all-gated batches, where surfacing is correct but the resume executes only the first entry.
Affected packages:agent-framework-core (the function-invocation path that consumes synthesized approval responses). The AG-UI layer (agent-framework-ag-ui) appears to do its part correctly — see Where it is lost.
Verified against: live main @ d56e81357e5179b6f916b180c0cb34ea2e73c80b (2026-08-04T17:23Z, core 1.13.0) — installed fresh and reproduced with the stockagent_framework_ag_ui.AgentFrameworkAgent, no application wrappers of any kind. Also reproduces on 5f3ca8f9 and 07511b80.
The synthesized approval responses already carry everything needed — the AG-UI layer builds one per approved interrupt and reports both as handled. The gap is that only the first drives an invocation, so the natural fix is for the approval-response consumer to iterate every response present in the input rather than resolving a single pending approval per resume, and to release each one's call independently.
A regression test that approves two gated calls in one batch and asserts two executions and two results would pin it; the never-require sibling case should keep passing unchanged, since it already does.
Description
Summary
When a model emits two or more
approval_mode="always_require"calls in one assistant message, AG-UI correctly surfaces one interrupt per call, and a client that approves all of them gets exactly one executed. The others are dropped with no error, no event and no trace in the transcript: noTOOL_CALL_START, no result, and nothing in the finalMESSAGES_SNAPSHOT.The model therefore never learns the outcome of a call it made and the user approved, and a UI that renders tool calls from the event stream is left with a chip that never resolves.
A batch of one gated call plus never-require siblings works correctly — the siblings ride along in the pending entry's
already_approved_requestsand are released on the resume. It is specifically two or more gated calls that lose all but the first.Reproduction
Self-contained, no network or credentials — a scripted chat client emitting a two-call batch, two
always_requiretools, and the stock AG-UI agent:Observed on the resume run, verbatim, against
main@d56e81357e5179b6f916b180c0cb34ea2e73c80b:Expected: both tools execute, both results stream, and the snapshot carries two
role="tool"messages.The chat client's view of the resume turn confirms the loss is not a display artifact — it is handed
user/textand a singletool/function_resultforc1, with no approval response, no result and no function call forc2anywhere in the transcript.Where it is lost
Not in the AG-UI resume synthesis. Instrumenting
_canonical_approval_resume_messageson the resume shows it doing exactly the right thing for both entries:Both interrupt ids are validated, both approval responses are built, and no resume error is raised. Execution then produces one result. So the drop is downstream of the AG-UI layer, where the synthesized approval responses are turned into function invocations — the second response is present in the input and does not lead to a call.
That also explains the asymmetry with never-require siblings: those are carried inside the gated entry as
already_approved_requests(_PendingApprovalWithSiblings) and are released as part of one approval, whereas two gated calls are two independent entries whose responses must each drive an execution.Impact
An approved tool call that silently does not happen is worse than one that fails: nothing surfaces to the user, the model proceeds as though the call never existed, and any UI that pairs
TOOL_CALL_STARTwith a result shows an unresolvable pending state.It also bites hardest exactly where batching is most natural — a plan that approves several writes at once, or a model that emits parallel calls for speed. In our own deployment the mitigation is an app-side shim that pops the parked approvals and executes them itself, which then has to re-stream and re-seat the results because the framework has no record of a call it never ran.
Prior art (checked before filing)
confirm_changesflow re-executing a tool. Opposite symptom, and the legacyrequire_confirmation=Truepath; this report is the native-interrupt path withrequire_confirmation=False.5f3ca8f9, which contains all three._resolve_approval_responses), a different defect: there a resume is spent when the run fails after the consume, so the retry getsAPPROVAL_RESUME_NOT_FOUND; here the resume run succeeds and simply executes only the first of several approved calls. Fixing either leaves the other intact — though the dropped second call does land in exactly the "consumed but never applied" state Python: [Bug]: AG-UI approval resume is consumed (and the approved tool executed) before the run can fail — a post-consume failure makes the user's answer unrecoverable, and the retry is indistinguishable from a never-pending id #7458 names as indistinguishable.Affected packages:
agent-framework-core(the function-invocation path that consumes synthesized approval responses). The AG-UI layer (agent-framework-ag-ui) appears to do its part correctly — see Where it is lost.Verified against: live
main@d56e81357e5179b6f916b180c0cb34ea2e73c80b(2026-08-04T17:23Z, core 1.13.0) — installed fresh and reproduced with the stockagent_framework_ag_ui.AgentFrameworkAgent, no application wrappers of any kind. Also reproduces on5f3ca8f9and07511b80.Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.13.0, agent-framework-ag-ui: 1.0.1
Python Version
No response
Additional Context
Suggested direction
The synthesized approval responses already carry everything needed — the AG-UI layer builds one per approved interrupt and reports both as handled. The gap is that only the first drives an invocation, so the natural fix is for the approval-response consumer to iterate every response present in the input rather than resolving a single pending approval per resume, and to release each one's call independently.
A regression test that approves two gated calls in one batch and asserts two executions and two results would pin it; the never-require sibling case should keep passing unchanged, since it already does.