Description
Agent.as_tool
hides nested tool‑call events — blocks parallel sub‑agents with streaming
Summary
Agent.as_tool()
makes it easy to plug one agent inside another, but it does so by running the wrapped agent non‑streamed and returning only its final string. As a result all inner events (including inner tool calls) are invisible to the outer run. This is merely confusing in simple cases, but it becomes a hard blocker when you need to run multiple sub‑agents in parallel and surface their progress to the user in real time.
Environment
Library | Version (tested) |
---|---|
openai‑agents |
<fill in> |
Python | 3.12.x |
Model | gpt‑4o-2024‑08‑06 |
OS | macOS 15.5 (Apple Silicon) |
Minimal Reproduction
import asyncio
from agents import Agent, Runner, function_tool
@function_tool
async def grab_user_purchases(user_name: str):
return [{"name": user_name, "purchase_id": "123"}]
# ► Sub‑agent that *must* call the tool above
a_purchase_agent = Agent(
name="purchase_agent_A",
instructions="Grab purchases from source A.",
tools=[grab_user_purchases],
)
b_purchase_agent = Agent(
name="purchase_agent_B",
instructions="Grab purchases from source B.",
tools=[grab_user_purchases],
)
# ► Main agent runs BOTH sub‑agents concurrently
main_agent = Agent(
name="main_agent",
instructions=(
"Run both purchase agents in parallel, then merge their results."
),
tools=[
a_purchase_agent.as_tool(tool_name="fetch_A"),
b_purchase_agent.as_tool(tool_name="fetch_B"),
],
)
async def demo():
run = Runner.run_streamed(main_agent, input="John Doe")
async for ev in run.stream_events():
print(ev)
asyncio.run(demo())
Observed Output (trimmed)
RunItemStreamEvent(name='tool_called', item=… name='fetch_A')
RunItemStreamEvent(name='tool_called', item=… name='fetch_B')
RunItemStreamEvent(name='tool_output', item=… name='fetch_A')
RunItemStreamEvent(name='tool_output', item=… name='fetch_B')
No grab_user_purchases
events appear for either sub‑agent.
Expected Output
The stream should also contain, for each sub‑agent:
RunItemStreamEvent(name='tool_called', item=… name='grab_user_purchases')
RunItemStreamEvent(name='tool_output', item=… name='grab_user_purchases')
That would let the UI show two independent spinners while both sub‑agents work.
Analysis — Why events are lost
Internally Agent.as_tool()
wraps the agent in a FunctionTool
whose
on_invoke_tool
implementation calls Runner.run
(non‑streamed). All
events from the nested run are consumed privately, and only the final string is
returned to the outer agent.
Why current work‑arounds don’t solve it
Work‑around | Limitation |
---|---|
Handoff to the sub‑agent | Only one agent can own the run at a time → cannot run two sub‑agents concurrently. |
Manual Runner.run_streamed inside a proxy tool |
There is no public push_stream_event API, so you cannot forward nested events into the parent stream; plus it adds boilerplate and defeats the “simple orchestration” goal of the SDK. |
Separate runs outside the parent | Breaks the single unified event stream; forces custom front‑end multiplexing logic; again loses the simplicity of Agents SDK. |
Feature Request
Expose an official way to wrap an agent as a streaming tool that forwards
all nested events. Two concrete ideas:
Agent.as_streaming_tool(tool_name=…, description=…)
Agent.as_tool(..., stream_inner_events=True)
Either option should:
- Call
Runner.run_streamed
under the hood. - Forward each
sub_event
onto the parent run’s queue with proper ordering. - Preserve the ability to invoke multiple such tools in parallel when
parallel_tool_calls=True
.
Impact
Without this, any application that relies on parallel agent composition with real‑time progress (e.g., multi‑source data gathering, meta‑analysis agents, canvas dashboards) must abandon as_tool()
and implement fragile custom logic, undermining the SDK’s promise of easy composition.