Skip to content

Python: [Feature]: AG-UI agent adapter should forward HITL approval to a hosted/remote FoundryAgent (mcp_approval_response) instead of executing locally #6652

Closed

Description

Is your feature request related to a problem? Please describe.

When a deployed Foundry hosted agent is exposed over AG-UI with the native
add_agent_framework_fastapi_endpoint(FoundryAgent(...)), the human-in-the-loop
(HITL) approve step never re-executes the gated tool.

The AG-UI agent adapter resolves approvals locally:
agent_framework_ag_ui/_agent_run.py → _resolve_approval_responses()
(main, line ~455) calls _try_execute_function_calls() (line ~568) — i.e. it
tries to run the approved tool in-process. A hosted/remote agent has no local
tool bodies, so nothing runs: the tool never re-executes server-side, and state is
unchanged. The approval request surfaces correctly (with allow_preview=True),
but the response has nowhere to go — it is effectively dropped.

The hosting side already supports this round-trip
(#5666 / #5654 / #4054 — the deployed agent emits/consumes
mcp_approval_request / mcp_approval_response). The missing half is the client /
AG-UI agent-adapter
side forwarding the approval response to the remote agent.

This is the direct analog of #5054 → #5070, where FoundryAgent (Responses API)
did not surface oauth_consent_request as a CUSTOM AG-UI event and it was fixed by
surfacing that Responses event through the Foundry client. Here it's the
mcp_approval_request → mcp_approval_response round-trip that needs the same
treatment on the agent path.

Describe the solution you'd like

When the agent wrapped by add_agent_framework_fastapi_endpoint is a
Responses-backed remote/hosted agent (e.g. FoundryAgent), the AG-UI agent adapter
should forward an incoming function_approval_response to the agent (translated
to an mcp_approval_response on the Responses request) so the hosted runtime
re-executes the gated tool — instead of executing it locally via
_try_execute_function_calls. (Symmetric to how _workflow_run.py was taught to
honour approval responses in #4546 / #6360, and to the oauth_consent_request
surfacing in #5070.)

Versions

  • agent-framework-core 1.9.0
  • agent-framework-foundry 1.8.2
  • agent-framework-ag-ui 1.0.0rc5
  • Confirmed unchanged on main (_agent_run.py _resolve_approval_responses →
    _try_execute_function_calls).

Minimal reproduction

1. The hosted agent (agent.py) — one read tool + one approval-gated tool. Deploy
it as a Foundry hosted agent (or run it locally with azd ai agent run):

# agent.py
import json
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from azure.identity import DefaultAzureCredential

STATE = {"value": 100}

@tool
async def get_value() -> str:
    """Return the current value."""
    return json.dumps(STATE)

@tool(approval_mode="always_require")
async def apply_delta(delta: float) -> str:
    """Change the value by `delta`. Requires human approval."""
    STATE["value"] = round(STATE["value"] + float(delta), 4)
    return json.dumps({"status": "ok", **STATE})

def build_hosted_agent():
    client = FoundryChatClient(
        project_endpoint="https://<acct>.services.ai.azure.com/api/projects/<proj>",
        model="gpt-4.1",
        credential=DefaultAzureCredential(),
    )
    return Agent(client=client, name="approval_demo",
                 instructions="Call get_value to read. Call apply_delta to change it.",
                 tools=[get_value, apply_delta], default_options={"store": False})

2. The native AG-UI bridge (native_bridge.py) — points at the DEPLOYED agent:

# native_bridge.py  ->  uvicorn native_bridge:app --port 8080
from fastapi import FastAPI
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.foundry import FoundryAgent
from azure.identity import DefaultAzureCredential

agent = FoundryAgent(
    project_endpoint="https://<acct>.services.ai.azure.com/api/projects/<proj>",
    agent_name="approval_demo",        # the DEPLOYED hosted agent
    credential=DefaultAzureCredential(),
    allow_preview=True,                # required to reach the hosted-agent endpoint
    name="approval_demo", id="approval_demo",
)
app = FastAPI()
add_agent_framework_fastapi_endpoint(app, agent, "/")

3. Drive it over AG-UI:

# Turn 1: ask to change the value -> the agent PAUSES with a function_approval_request
curl -sN http://localhost:8080/ -H 'Content-Type: application/json' -d '{
  "threadId":"t1","runId":"r1","state":{},"tools":[],"context":[],"forwardedProps":{},
  "messages":[{"id":"m1","role":"user","content":"Apply a delta of 10."}]
}'   # -> emits a function_approval_request for apply_delta (good)

# Turn 2: APPROVE it (replay history + the approval response)
curl -sN http://localhost:8080/ -H 'Content-Type: application/json' -d '{
  "threadId":"t1","runId":"r2","state":{},"tools":[],"context":[],"forwardedProps":{},
  "messages":[ ...prior turn..., {"id":"a1","role":"tool","content":"{\"accepted\":true}"} ]
}'   # -> RUN_FINISHED, but get_value still returns {"value": 100}

Observed (test matrix, against a real deployed agent)

Configuration HITL approve re-executes the hosted tool?
Native FoundryAgent (no preview) ❌ 400 "Hosted agents can only be called through the agent endpoint"
Native FoundryAgent(allow_preview=True) ❌ approval request surfaces, but approve leaves state unchanged
Native + allow_preview + disabling ag-ui local approval interception ❌ still unchanged

A subsequent get_value shows the value never changed; no mcp_approval_response
is ever sent to the hosted agent.

Expected

After approve, the hosted runtime receives an mcp_approval_response, re-executes
apply_delta, and get_value returns {"value": 110.0}.

Current workaround

A hand-rolled SupportsAgentRun proxy that streams the hosted agent's Responses,
surfaces mcp_approval_request as the AG-UI approval, and on approve POSTs an
mcp_approval_response to .../agents/<name>/endpoint/protocols/openai/responses.
It works, but it duplicates translation the framework already does for the
non-approval path — hence this request to support it natively on the agent adapter.

Additional context

Happy to provide the full proxy + a deterministic repro script if useful.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jun 21, 2026
  2. rpelevin commented on Jun 23, 2026

    @rpelevin

    The failure mode here looks like an approval-response routing boundary, not just an AG-UI adapter bug.

    For hosted or remote agents, the approval decision should be treated as a resume input for the original remote run, not as a license for the local adapter to re-execute the function. A safe contract would bind the approval response to the remote pending tool call id, hosted agent id, thread or run id, tool name, canonical args digest, and approval decision, then forward that envelope back to the hosted runtime as the only component allowed to resume the gated tool.

    I would also make local fallback fail closed: if the adapter cannot prove the tool body is local and the pending approval originated from the local executor, it should not call the tool in-process. It should either forward the approval response to the remote runtime or return a terminal "approval response not deliverable" state.

    Two regression tests would make this durable: one where approving a hosted FoundryAgent call mutates remote state through the response path, and one where a hosted approval response is intentionally prevented from invoking any local tool body. That separates "human approved" from "this process is authorized to execute."

  3. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jun 23, 2026
  4. deleted a comment from on Jul 6, 2026
  5. rpelevin commented on Jul 6, 2026

    @rpelevin

    This is the part I would make explicit in the adapter contract: an approval response should resume the remote pending transition, not become a fresh local execution signal.

    A minimal invariant could be:

    • the approval response names the pending remote tool-call id;
    • the hosted agent id, thread/run id, tool name, and args digest still match;
    • the approval has not expired or already been consumed;
    • no terminal outcome has already been recorded for that pending transition.

    If any of those checks fail, the adapter should not fall back to in-process tool execution. It should either re-query the remote pending state or return an approval-response-not-deliverable terminal state.

    That gives a clean regression shape: approving a hosted call mutates state only through the hosted response path, while a mismatched or replayed approval never invokes a local tool body.

  6. moonbox3 commented on Aug 18, 2026

    @moonbox3
    Contributor

    Fixed in #7480 and #7594

  7. shimjaemandu commented on Sep 20, 2026

    @shimjaemandu

    Hi, I came across your post about handling duplicate tool execution, approval state, retries, or other side effects in AI agents.
    I’m researching whether these problems are better solved inside each agent framework or by a separate runtime layer that sits between agents and real systems.
    Would you personally use an external layer that provides idempotency, execution state tracking, approval/policy enforcement, recovery, and audit across different agent frameworks, or would you strongly prefer these features to stay native to the framework you already use?
    What would stop you from adopting such a layer—for example integration complexity, latency, trust/security, having another dependency, or something else?
    If it actually removed the wrapper/proxy/shim code you maintain today, would you consider testing it in a real project, and under what conditions?

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: Python

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions