Agents that call agents
One agent can hand part of the job to another by calling it inside a tool. Passing ctx.usage counts both agents' requests against one budget.
from pydantic_ai import Agent, ModelResponse, RunContext, TextPart, ToolCallPart
from pydantic_ai.models.function import FunctionModel
def write(messages, info):
return ModelResponse(parts=[TextPart("Sorry about the double charge. The extra payment will be back within 5 days.")])
writer = Agent(FunctionModel(write), instructions="Write a short, kind reply to the customer.")
def lead_model(messages, info):
last = messages[-1].parts[-1]
if last.part_kind == "user-prompt":
return ModelResponse(parts=[ToolCallPart("draft_reply", {"ticket": last.content})])
return ModelResponse(parts=[TextPart(f"Reply ready: {last.content}")])
desk = Agent(FunctionModel(lead_model), instructions="Handle the ticket. Use draft_reply for the wording.")writer is good at wording; desk handles the ticket. The desk's model calls a draft_reply tool, and that tool runs the writer:
@desk.tool
async def draft_reply(ctx: RunContext, ticket: str) -> str:
"""Ask the writer for the reply to send."""
result = await writer.run(ticket, usage=ctx.usage)
return result.output
result = desk.run_sync("I was charged twice")
print(result.output)
print(result.usage)The tool is async and awaits writer.run. usage=ctx.usage makes the writer add its request to the desk's usage: three requests, two by the desk and one by the writer. Without it, the desk would report two, and a usage limit on the desk would not see the writer's calls.
@desk.tool
async def draft_reply(ctx: RunContext, ticket: str) -> str:
"""Ask the writer for the reply to send."""
result = await writer.run(ticket)
return result.output
print(desk.run_sync("I was charged twice").usage)Delegate or hand off
- Delegation, above: the second agent works inside the first agent's run and returns to it.
- Hand-off: your code runs one agent, looks at its output, and decides which agent runs next. The triage agent from lesson 9 choosing between a
TicketandNeedsHuman, followed by yourif, is a hand-off.
The writer's messages are not part of the desk's history; the desk only sees the tool's return value. Each agent has its own instructions, tools and model, so the writer could use a cheaper model than the desk.
- Give
writeradeps_typeand passdeps=ctx.depsfrom the tool. - Run the desk with
usage_limits=UsageLimits(request_limit=2), with and withoutusage=ctx.usage. - Write the hand-off version: run triage, then run the writer only for tickets.
This is what real progress feels like.