Subagents¶
A subagent is an agent another agent calls like a tool. Give it its own model, prompt and tools, and pass it to
the main agent with as_tool:
from pyox_agents import Agent
researcher = Agent(model="gpt-5-mini", instructions="Research topics. Be thorough.", tools=[web_search])
writer = Agent(
model="gpt-5",
instructions="Write articles. Use `research` to find facts.",
tools=[researcher.as_tool("research", "Research a topic and report what you found.")],
)
result = await writer.run("Write a short article about Vigo")
The model calls research(task="..."). The researcher starts a fresh conversation with the task as its first
message, works with its own tools, and its final answer is the tool's result. It knows nothing else about the
main conversation, so the main agent must put everything it needs in the task.
What a subagent shares¶
| Shared with the main agent | Its own |
|---|---|
deps (its tools get the same ctx.deps) |
model, prompt, tools, hooks, max_turns |
the person: approvals and ctx.ask questions |
its conversation |
| the run's events and token usage | |
| the run's state, if allowed |
A subagent cannot have subagents of its own: one level keeps runs easy to follow.
If the subagent fails (the API is down, too many turns), the main agent gets the error as the tool's result and can react to it.
Options¶
researcher.as_tool(
"research",
"Research a topic.",
inherit_tools=False, # True: also give it the main agent's tools
talk_to=None, # None, "user" or "parent": who it can ask questions
state=None, # None, "read" or "write": what it may do with the run's state
)
Tools. By default a subagent has only its own tools. With inherit_tools=True it also gets the main agent's
tools (never its subagents). When names clash, its own tool wins. Tools keep their approval rules wherever they run.
Talking to the user. With talk_to="user", the subagent gets an ask_user(question) tool. The question
waits for the person like a ctx.ask, and the answer is the tool's result.
Talking to the main agent. With talk_to="parent", the subagent gets an ask_parent(question) tool. When it
asks, it stops, and the main agent reads the question as the tool's result:
The subagent asks: How many days is the trip?
To answer, call plan with subagent_id="call_0" and your answer as the task.
The main agent answers by calling the tool again with that subagent_id: the subagent goes on with its own
conversation, with the answer as a new message. Its final answers end with the subagent_id too, so the main
agent can follow up later in the same way.
State. A subagent sees the run's state only if its tool says so: state="read" lets its tools
read ctx.state, state="write" lets them change it too.
Events¶
A subagent's events come in the same stream as the main agent's, marked with subagent_id: the id of the tool
call running it (None for the main agent's events). Its tool call ids start with it (call_0/call_1), so ids
stay unique:
async for event in writer.stream("Write about Vigo"):
match event:
case SubagentStarted(name=name, task=task):
print(f"[{name}] {task}")
case TextDelta(text=text, subagent_id=None):
print(text, end="") # only the main agent's text
case SubagentFinished(name=name, usage=usage):
print(f"[{name}] done ({usage.output_tokens} tokens)")
| Event | Fields |
|---|---|
SubagentStarted |
call_id (its id), name (the tool's), task |
SubagentFinished |
call_id, name, result, is_error, stop_reason ("waiting" while it waits for answers; None if it failed), usage (its tokens, also in the run's) |
Web frontends get them as AG-UI SUBAGENT_* events, or an AI SDK
data-subagent part.
Approvals and questions¶
A subagent's approvals, ctx.ask questions and frontend tools reach the person exactly like the main agent's
(human in the loop). Their ids say where they come from: call_0/call_1 is the
subagent's call call_1, inside the main agent's call call_0, and InputRequested.subagent_id is "call_0".
- Live:
on_requestanswers them while the subagent waits. - Stop and resume: the run stops, and
RunFinished.pendinglists them. The main agent's callcall_0waits too, and keeps the subagent's conversation in its tool message (undersubagent, never sent to the model). Answer with the same ids:
stopped = await assistant.run("Book me a flight to Vigo")
[request] = stopped.pending # call_id="call_0/call_0", tool_name="book", kind="approval"
result = await assistant.run(history=stopped.messages, answers={"call_0/call_0": Approve()})
The subagent goes on from where it stopped, then the main agent does.