Repository navigation
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-core1.9.0agent-framework-foundry1.8.2agent-framework-ag-ui1.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
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
docs: clarify we use agent-framework-ag-ui (the shim is the gap), lin…
rpelevin commentedon Jun 23, 2026
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."
rpelevin commentedon Jul 6, 2026
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:
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.
moonbox3 commentedon Aug 18, 2026
Fixed in #7480 and #7594
shimjaemandu commentedon Sep 20, 2026
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?