Skip to content

Python: [Bug]: AG-UI confirm_changes approval-gated tool reverts to "in progress" after completing #6828

Closed

Description

Description

When a tool is gated by human-in-the-loop approval over AG-UI using the confirm_changes flow (AgentFrameworkAgent(require_confirmation=True), as CopilotKit uses), the tool's executed result is emitted only as a transient TOOL_CALL_RESULT event and is never recorded in flow.tool_results. As a result, the end-of-turn MESSAGES_SNAPSHOT omits the tool-result message.

Clients that reconcile rendered state from MESSAGES_SNAPSHOT (e.g. CopilotKit) then revert the already-completed tool call back to "in progress": the tool chip flips from done → running right after approval, and again on the next turn's snapshot.

Package Versions

agent-framework-core: 1.9.0, agent-framework-ag-ui:1.0.0rc5, agent-framework-ollama:1.0.0b260521, ollama: 0.5.3

Python Version

Python 3.12

Additional Context

Minimal reproduction

MAF advisor exposed over AG-UI (add_agent_framework_fastapi_endpoint, require_confirmation=True) with a tool requiring approval (e.g. the Microsoft Learn MCP with require_approval=True), driven by a CopilotKit useHumanInTheLoop({ name: "confirm_changes" }) client. Ask a question that calls the gated tool, approve it: the tool chip stays "Running" after the result returns, and remains "Running" after the next turn.

Root cause

_make_approval_tool_result_events (agent_framework_ag_ui/_agent_run.py) builds the TOOL_CALL_RESULT event but does not append to flow.tool_results, which is what _build_messages_snapshot uses to attach tool-result messages.

_clean_resolved_approvals_from_snapshot does patch the direct approval_mode flow — there the approval-response tool message is keyed by the original call_id, so it gets replaced with the executed result (covered by test_approval_resume_snapshot_replaces_approval_payload_with_tool_result). It does not cover the confirm_changes flow, where the HITL tool call carries its own id, so the original call's result never lands in the snapshot.

Expected vs actual

  • Expected: after approval, the tool chip shows a completed/result state and stays there across subsequent snapshots.
  • Actual: the chip reverts to "in progress" / "Running" after the tool has already produced its result.

Additional info

Regression-style: the direct approval_mode flow is fine; only the confirm_changes flow is affected. A fix PR follows.

Activity

  1. lordlinus commented on Jul 1, 2026

    @lordlinus

    Confirming this is real, and flagging a more severe variant: for a hosted/remote agent driven through this same confirm_changes flow (add_agent_framework_fastapi_endpoint + a custom SupportsAgentRun that forwards to a Foundry hosted agent over previous_response_id-chained Responses calls — the pattern agent-framework-ag-ui itself recommends for #6652), the missing snapshot cleanup isn't just a UI rendering glitch — it causes the approved tool to be genuinely re-executed a second time on the very next unrelated turn, with a real, duplicated side effect.

    Reproduction (isolates this to the raw Responses API / previous_response_id chaining, no CopilotKit, no bridge code involved)

    Packages: agent-framework-core==1.9.0, agent-framework-ag-ui==1.0.0rc5, agent-framework-foundry==1.8.2 — identical core/ag-ui versions to this issue.

    A minimal demo 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 against the raw /responses endpoint (no AG-UI/CopilotKit involved at all in this specific reproduction — just curl):

    # Turn 1: read -> value=100, response_id=R1
    # Turn 2 (previous_response_id=R1): "Apply a delta of 10." -> mcp_approval_request, response_id=R2
    # Turn 3 (previous_response_id=R2): {"type": "mcp_approval_response", "approval_request_id": ..., "approve": true}
    #   -> tool executes, "The new value is 110", response_id=R3
    # Turn 4 (previous_response_id=R3): "Just checking in, what's the value now?"
    #   -> EXPECTED: reads 110. ACTUAL: value has become 120 - apply_delta ran AGAIN,
    #      with no new approval anywhere in turn 4's input.

    This reproduces consistently (verified across multiple independent runs). Turn 4 sends nothing approval-shaped in its own input — the only way apply_delta could have executed again is if the chained conversation state on the ResponsesHostServer/FoundryChatClient side still considers the turn-2 tool call "pending" and resolves/executes it again once a new turn arrives, which lines up with this issue's root cause (the approval's tool result never gets durably recorded/cleaned up for the confirm_changes-style flow, _clean_resolved_approvals_from_snapshot only covers the direct approval_mode flow).

    I also verified via the AG-UI bridge itself (add_agent_framework_fastapi_endpoint + a custom hosted-agent-forwarding SupportsAgentRun, same package versions) that 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, confirming the duplication happens upstream of any AG-UI-layer bridge/proxy code, in the core Responses/HITL resolution path itself.

    Given the tool executed was gated specifically because it's consequential (side-effecting), a second, unrequested execution triggered purely by an unrelated later message is a correctness/safety issue beyond the reported UI "chip reverts to running" symptom — worth confirming whether the fix PR for the missing tool-result snapshot entry also addresses (or needs to separately address) the underlying re-execution, not just the display state.

    Happy to share the full repro script / logs if useful.

  2. lordlinus commented on Jul 1, 2026

    @lordlinus

    Filed #6851 to track the more severe duplicate-execution consequence of this same root cause separately, since it's a safety issue (real side-effect duplication) beyond the UI display symptom reported here — cross-linking for visibility.

  3. lordlinus commented on Jul 1, 2026

    @lordlinus

    Update: I posted a correction on the root-cause attribution over on #6851 (which I'd cross-linked here) — it's not agent_framework_ag_ui, the bug reproduces with that package not even installed. The actual trigger is chaining previous_response_id through a response that resolved an mcp_approval_response. See #6851 for the isolation details and a client-side workaround we shipped in the meantime.

  4. antsok commented on Jul 2, 2026

    @antsok
    ContributorAuthor

    Hi Evan Mattson (@moonbox3), could you help tag this issue as a Python bug, since I failed to open it properly in the first place?

  5. deleted a comment from on Jul 7, 2026
  6. added
    pythonUsage: [Issues, PRs], Target: Python
    ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integration
    on Jul 7, 2026
  7. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Jul 7, 2026
  8. github-actions commented on Jul 7, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: In python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py, _make_approval_tool_result_events (line 390) builds ToolCallResultEvent objects but never appends corresponding dicts to flow.tool_results; additionally _clean_resolved_approvals_from_snapshot (line 650) cannot match the confirm_changes tool's toolCallId (the confirm_id) against result_by_call_id which is keyed by the original tool's call_id. Minimal repro: call run_agent_stream with resume messages containing an assistant tool_calls array (original tool + confirm_changes) followed by a tool result for the confirm_changes call with {"accepted": true, "steps": [...]}, then assert the MESSAGES_SNAPSHOT includes the original tool's result — it will be missing.

    • Failing test: python/packages/ag-ui/tests/ag_ui/test_confirm_changes_snapshot_bug.py
    • Files examined: python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py, python/packages/ag-ui/agent_framework_ag_ui/_run_common.py, python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py, python/packages/ag-ui/tests/ag_ui/test_approval_result_event.py, python/packages/ag-ui/tests/ag_ui/conftest.py, python/packages/core/agent_framework/_tools.py
    • Tests run: tests/ag_ui/test_confirm_changes_snapshot_bug.py::test_confirm_changes_flow_snapshot_includes_tool_result, tests/ag_ui/test_confirm_changes_snapshot_bug.py::test_confirm_changes_tool_result_in_flow_tool_results, tests/ag_ui/test_approval_result_event.py::test_approval_resume_snapshot_replaces_approval_payload_with_tool_result
    • Reported version: 1.0.0rc5
    • Current version: 1.0.0rc7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

ag-uiUsage: [Issues, PRs], Target: AG-UI protocol integrationpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions