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

@lordlinus

Description

@lordlinus

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_requestmcp_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonIssues related to the Python codebasetriagePlaced on an issue or discussion that requires a maintainer to triage the item

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions