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

Labels

agentsIssues related to single agentsbugSomething isn't workingpythonv1.0Features being tracked for the version 1.0 GA

Type

Projects

Status

In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions