Description
When a Foundry-hosted agent is wrapped with Agent.as_tool() and the underlying agent requires OAuth consent for a connected MCP tool, the consent event is silently consumed inside the as_tool() implementation. The caller receives either an empty result or a partial text result with no indication that OAuth consent is required.
There is no supported way to intercept or observe consent events emitted by the sub-agent when calling it as a tool.
Background
Issue #4197 fixed the underlying SDK and AG-UI layers so that CUSTOM(oauth_consent_request) is now correctly emitted from the sub-agent's event stream when a Foundry-hosted agent requires OAuth consent for a connected MCP tool. However, as_tool() consumes that stream internally and still discards the event before it reaches the caller. #4197 is a prerequisite for this bug, not a resolution of it.
Root Cause
Looking at _agents.py, as_tool() has two internal execution paths. Both discard consent events:
Path 1 - no stream_callback (the common case)
# _agents.py - agent_wrapper() inside as_tool()
if stream_callback is None:
# Use non-streaming mode
return (await self.run(input_text, stream=False, **forwarded_kwargs)).text
Non-streaming mode. Returns .text only. The agent run completes entirely internally - there is no way for a consent event to surface to the caller.
Path 2 - with stream_callback
# _agents.py - agent_wrapper() inside as_tool()
async for update in self.run(input_text, stream=True, **forwarded_kwargs):
response_updates.append(update)
if is_async_callback:
await stream_callback(update)
else:
stream_callback(update)
# Create final text from accumulated updates
return AgentResponse.from_updates(response_updates).text
The stream_callback receives AgentResponseUpdate objects during the run. However:
- The return value is still
.text only - the caller gets no structured signal that consent is required.
stream_callback is designed for UI streaming feedback, not structured event interception. It is not a viable workaround for this use case.
- Even if a consent event were observable via the callback, there is no mechanism to communicate it back to the caller as a return value or exception.
The result in both cases: there is no code path in as_tool() where a consent event can cause anything other than an empty string return.
Expected Behaviour
One of:
-
Option A (minimal): When a consent event is emitted during the sub-agent run, as_tool() raises a typed exception that the caller can catch:
class ConsentRequiredException(Exception):
def __init__(self, url: str):
self.url = url
This lets callers handle it cleanly without reimplementing the run loop:
try:
result = await tool.ainvoke(...)
except ConsentRequiredException as e:
return f"__oauth_consent_required|{e.url}"
-
Option B (preferred): as_tool() accepts an async generator hook or on_event callback that exposes raw events - not just AgentResponseUpdate - so the caller can observe consent events and other structured events without reimplementing agent.run() internally.
Actual Behaviour
as_tool() runs the sub-agent internally and only returns the final .text value. Any consent events emitted during the run are discarded. The caller has no way to detect them.
Workaround
I temporarily worked around this by bypassing as_tool() entirely and driving agent.run() manually inside our own tool wrapper:
async for event in agent.run(messages=[...], stream=True, session=tool_session):
event_type = _norm_type(getattr(event, "type", None))
# manually detect CUSTOM(oauth_consent_request)
# manually accumulate text output
# manually handle errors
This means we have reimplemented the internals of as_tool() - including text extraction across multiple possible event shapes - and we are now tightly coupled to internal event structure that could change without notice. This is fragile and hard to maintain.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core==1.0.0rc3, agent-framework-azure-ai==1.0.0rc3
Python Version
Python 3.12
Additional Context
This is somewhat related to #4213. Both issues involve agent runtime events that are not surfaced to consumers, but they occur at different layers and have independent fixes. #4213 is a gap in the AG-UI SSE emission layer (_emit_content); this issue is a gap in the agent orchestration layer (as_tool()).
Description
When a Foundry-hosted agent is wrapped with
Agent.as_tool()and the underlying agent requires OAuth consent for a connected MCP tool, the consent event is silently consumed inside theas_tool()implementation. The caller receives either an empty result or a partial text result with no indication that OAuth consent is required.There is no supported way to intercept or observe consent events emitted by the sub-agent when calling it as a tool.
Background
Issue #4197 fixed the underlying SDK and AG-UI layers so that
CUSTOM(oauth_consent_request)is now correctly emitted from the sub-agent's event stream when a Foundry-hosted agent requires OAuth consent for a connected MCP tool. However,as_tool()consumes that stream internally and still discards the event before it reaches the caller. #4197 is a prerequisite for this bug, not a resolution of it.Root Cause
Looking at
_agents.py,as_tool()has two internal execution paths. Both discard consent events:Path 1 - no
stream_callback(the common case)Non-streaming mode. Returns
.textonly. The agent run completes entirely internally - there is no way for a consent event to surface to the caller.Path 2 - with
stream_callbackThe
stream_callbackreceivesAgentResponseUpdateobjects during the run. However:.textonly - the caller gets no structured signal that consent is required.stream_callbackis designed for UI streaming feedback, not structured event interception. It is not a viable workaround for this use case.The result in both cases: there is no code path in
as_tool()where a consent event can cause anything other than an empty string return.Expected Behaviour
One of:
Option A (minimal): When a consent event is emitted during the sub-agent run,
as_tool()raises a typed exception that the caller can catch:This lets callers handle it cleanly without reimplementing the run loop:
Option B (preferred):
as_tool()accepts an async generator hook oron_eventcallback that exposes raw events - not justAgentResponseUpdate- so the caller can observe consent events and other structured events without reimplementingagent.run()internally.Actual Behaviour
as_tool()runs the sub-agent internally and only returns the final.textvalue. Any consent events emitted during the run are discarded. The caller has no way to detect them.Workaround
I temporarily worked around this by bypassing
as_tool()entirely and drivingagent.run()manually inside our own tool wrapper:This means we have reimplemented the internals of
as_tool()- including text extraction across multiple possible event shapes - and we are now tightly coupled to internal event structure that could change without notice. This is fragile and hard to maintain.Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core==1.0.0rc3, agent-framework-azure-ai==1.0.0rc3
Python Version
Python 3.12
Additional Context
This is somewhat related to #4213. Both issues involve agent runtime events that are not surfaced to consumers, but they occur at different layers and have independent fixes. #4213 is a gap in the AG-UI SSE emission layer (_emit_content); this issue is a gap in the agent orchestration layer (as_tool()).