Skip to content

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

Closed

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()).

Activity

  1. added
    bugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)
    on Mar 5, 2026
  2. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Mar 5, 2026
  3. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Mar 6, 2026
  4. eavanvalkenburg commented on Mar 6, 2026

    @eavanvalkenburg
    Member

    Giles Odigwe (@giles17) you were working on the oauth stuff, please have a look at this one as well!

  5. moved this from Planned to In Progress in Agent Frameworkon Mar 6, 2026
  6. moved this from In Progress to In Review in Agent Frameworkon Mar 9, 2026
  7. added a commit that references this issue on Mar 10, 2026
    7b7f94a
  8. moved this from In Review to Done in Agent Frameworkon Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentbugUsage: [Issues], Target: all issues (Legacy, prefer issue type: bug)pythonUsage: [Issues, PRs], Target: Python

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions