Description
When a tool is gated by human-in-the-loop approval over AG-UI using the confirm_changes flow (add_agent_framework_fastapi_endpoint, require_confirmation=True, as CopilotKit's useHumanInTheLoop uses), the approved tool call is silently re-executed a second time on a later, unrelated conversation turn — with no new approval anywhere in that turn's input. This is a duplicate real side effect, not merely a UI/display issue.
I believe this shares the exact same root cause as #6828 (_clean_resolved_approvals_from_snapshot not covering the confirm_changes flow), but the consequence is more severe than that issue's reported "tool chip reverts to running" symptom: the gated tool's actual side effect runs again, unprompted, on a subsequent turn. For any real (non-demo) consequential action — sending a payment, approving a request, deleting a resource — this means the action can be silently duplicated without the user ever clicking Approve a second time.
Package Versions
agent-framework-core==1.9.0, agent-framework-ag-ui==1.0.0rc5, agent-framework-foundry==1.8.2 (also reproduced with a locally-served demo agent via ResponsesHostServer/FoundryChatClient; identical package versions to #6828).
Python Version
Python 3.12
Minimal reproduction (isolated to the raw Responses API — no CopilotKit, no custom bridge/proxy code involved)
A minimal agent with a get_value read tool and an apply_delta(delta) tool decorated @tool(approval_mode="always_require"), served via ResponsesHostServer/FoundryChatClient. Four turns chained purely via previous_response_id, sent directly to the raw /responses endpoint with plain curl (no AG-UI/CopilotKit client involved at all):
# Turn 1 (read): input="What is the current value?"
# -> value=100, response_id=R1
# Turn 2 (propose, pause): previous_response_id=R1, input="Apply a delta of 10."
# -> mcp_approval_request emitted, response_id=R2
# Turn 3 (approve): previous_response_id=R2
# input=[{"type": "mcp_approval_response", "approval_request_id": <id>, "approve": true}]
# -> tool executes: "The new value is 110.", response_id=R3
# Turn 4 (unrelated follow-up): previous_response_id=R3, input="Just checking in, what's the value now?"
# -> EXPECTED: reads 110 (no new approval was sent, so nothing should execute).
# -> ACTUAL: value has become 120 - apply_delta ran AGAIN.
This reproduces consistently across independent runs (verified multiple times, with values drifting by exactly one extra delta application per additional turn taken after the approval, regardless of what that turn's actual content is).
I separately verified this through an AG-UI bridge (add_agent_framework_fastapi_endpoint + a custom hosted-agent-forwarding SupportsAgentRun) and confirmed the bridge's own approval-detection logic correctly identifies turn 4 as a normal (non-approval) turn and does not re-send an mcp_approval_response — yet the hosted agent still re-executes the tool. This confirms the duplication happens entirely within the core Responses/HITL resolution path (ResponsesHostServer/FoundryChatClient/agent_framework_ag_ui), upstream of any bridge/proxy/AG-UI-client code, and is triggered purely by the stale, never-cleaned-up approval content lingering in the chained conversation history.
Root cause (same as #6828)
_clean_resolved_approvals_from_snapshot (agent_framework_ag_ui/_agent_run.py) only cleans up the stale approval-response content for the direct approval_mode flow (where the approval-response tool message is keyed by the original call_id). It does not cover the confirm_changes flow (where the HITL tool call carries its own separate id) — so for confirm_changes, the original tool call is never durably marked resolved anywhere the framework can see on a later turn, and gets reconsidered for (re-)execution once a new turn arrives in the same chained conversation.
Expected vs actual
- Expected: once approved and executed, a gated tool call must never execute again for any reason short of a fresh, explicit new approval.
- Actual: the SAME approved tool call executes a second time on the next turn in the same conversation, regardless of that turn's content — silently duplicating a real, consequential side effect.
Relationship to #6828
Same underlying defect (#6828 discovered the missing-snapshot-cleanup root cause via a UI symptom — approved tool chip reverting to "Running"). Filing separately because the safety impact here (duplicate execution of a real side effect, not just incorrect UI state) is significantly more severe and may need explicit test coverage/verification beyond what fixes the display symptom — a fix that only makes _build_messages_snapshot show the right state, without also preventing _resolve_approval_responses/the underlying provider from re-resolving the same approval, would leave this issue open even after #6828's fix lands. Happy to consolidate if maintainers prefer, but wanted the duplicate-execution behavior tracked explicitly given the safety implications for HITL-gated consequential actions.
Description
When a tool is gated by human-in-the-loop approval over AG-UI using the
confirm_changesflow (add_agent_framework_fastapi_endpoint,require_confirmation=True, as CopilotKit'suseHumanInTheLoopuses), the approved tool call is silently re-executed a second time on a later, unrelated conversation turn — with no new approval anywhere in that turn's input. This is a duplicate real side effect, not merely a UI/display issue.I believe this shares the exact same root cause as #6828 (
_clean_resolved_approvals_from_snapshotnot covering theconfirm_changesflow), but the consequence is more severe than that issue's reported "tool chip reverts to running" symptom: the gated tool's actual side effect runs again, unprompted, on a subsequent turn. For any real (non-demo) consequential action — sending a payment, approving a request, deleting a resource — this means the action can be silently duplicated without the user ever clicking Approve a second time.Package Versions
agent-framework-core==1.9.0,agent-framework-ag-ui==1.0.0rc5,agent-framework-foundry==1.8.2(also reproduced with a locally-served demo agent viaResponsesHostServer/FoundryChatClient; identical package versions to #6828).Python Version
Python 3.12
Minimal reproduction (isolated to the raw Responses API — no CopilotKit, no custom bridge/proxy code involved)
A minimal agent with a
get_valueread tool and anapply_delta(delta)tool decorated@tool(approval_mode="always_require"), served viaResponsesHostServer/FoundryChatClient. Four turns chained purely viaprevious_response_id, sent directly to the raw/responsesendpoint with plaincurl(no AG-UI/CopilotKit client involved at all):This reproduces consistently across independent runs (verified multiple times, with values drifting by exactly one extra
deltaapplication per additional turn taken after the approval, regardless of what that turn's actual content is).I separately verified this through an AG-UI bridge (
add_agent_framework_fastapi_endpoint+ a custom hosted-agent-forwardingSupportsAgentRun) and confirmed the bridge's own approval-detection logic correctly identifies turn 4 as a normal (non-approval) turn and does not re-send anmcp_approval_response— yet the hosted agent still re-executes the tool. This confirms the duplication happens entirely within the core Responses/HITL resolution path (ResponsesHostServer/FoundryChatClient/agent_framework_ag_ui), upstream of any bridge/proxy/AG-UI-client code, and is triggered purely by the stale, never-cleaned-up approval content lingering in the chained conversation history.Root cause (same as #6828)
_clean_resolved_approvals_from_snapshot(agent_framework_ag_ui/_agent_run.py) only cleans up the stale approval-response content for the directapproval_modeflow (where the approval-response tool message is keyed by the originalcall_id). It does not cover theconfirm_changesflow (where the HITL tool call carries its own separate id) — so forconfirm_changes, the original tool call is never durably marked resolved anywhere the framework can see on a later turn, and gets reconsidered for (re-)execution once a new turn arrives in the same chained conversation.Expected vs actual
Relationship to #6828
Same underlying defect (#6828 discovered the missing-snapshot-cleanup root cause via a UI symptom — approved tool chip reverting to "Running"). Filing separately because the safety impact here (duplicate execution of a real side effect, not just incorrect UI state) is significantly more severe and may need explicit test coverage/verification beyond what fixes the display symptom — a fix that only makes
_build_messages_snapshotshow the right state, without also preventing_resolve_approval_responses/the underlying provider from re-resolving the same approval, would leave this issue open even after #6828's fix lands. Happy to consolidate if maintainers prefer, but wanted the duplicate-execution behavior tracked explicitly given the safety implications for HITL-gated consequential actions.