Skip to content

Python: [Bug]: as_tool() swallows CUSTOM(oauth_consent_request) events — no way to surface consent flow to caller #4499

@djw-bsn

Description

@djw-bsn

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:

  1. The return value is still .text only - the caller gets no structured signal that consent is required.
  2. stream_callback is designed for UI streaming feedback, not structured event interception. It is not a viable workaround for this use case.
  3. 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()).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions