Approvals across subagents
API reference: Approvals
One on_approval handler answers the approvals of the whole tree, the parent's and every
subagent's. With permission_mode="ask", a subagent's tool call waits for an approval as the
parent's does. Pass the handler when you start the session. It answers while a driving call such as
session.run waits.
In "ask" mode, starting a subagent asks for approval, since the subagent can act. The other
subagent tools, such as messaging, waiting and interrupting, never ask.
import asyncio
from pydantic import BaseModel
from mistralai import agents
PRICES = {"atlas": "1,200 EUR", "nova": "850 EUR"}
class PriceArgs(BaseModel):
product: str
@agents.tool(name="lookup_price", description="Price of one product.", input_schema=PriceArgs)
async def lookup_price(args: PriceArgs) -> dict[str, str]:
return {"price": PRICES.get(args.product.casefold(), "unknown")}
agent = agents.Agent(
model="mistral-large-4",
permission_mode="ask",
instructions=(
"You answer questions about our products. Start one researcher per product, "
"wait for every result, then answer in one sentence."
),
subagents=[
agents.Subagent(
name="researcher",
description="Looks up one product and reports its price.",
instructions="Call lookup_price once, then answer with the product name and its price.",
tools=[lookup_price],
builtins=agents.Builtins(subagents=False),
)
],
)
def price_policy(
ctx: agents.ApprovalCallbackContext, request: agents.ApprovalRequest
) -> agents.AllowApproval | agents.KeepCallbackOpen:
allowed = ("agents.spawn", "client.lookup_price")
return agents.AllowApproval() if request.tool.id in allowed else agents.KeepCallbackOpen()
async def main() -> None:
session = await agent.start_session(on_approval=price_policy) # answers the whole tree
state = await session.run("What do the Atlas and the Nova cost?")
if isinstance(state, agents.Completed):
print(state.text)
await session.cancel()
asyncio.run(main())The handler allowed the two agents.spawn calls of the parent and the lookup_price call of each
researcher.
- A Python tool's stable ID is
client.<name>. Decide onrequest.tool.id, never onrequest.tool.name. See Approvals for why. ctx.session.idandctx.session.turn_idname the session that asked. For a subagent's approval, they are the subagent's own session and turn.- A handler that returns
agents.KeepCallbackOpen()leaves the approval open.
The approvals reference lists what a handler receives and returns.
When no handler answers an approval, the parent's or any subagent's, session.run returns
Blocked, and state.callbacks lists the open approvals of the whole tree. Each one names the
session and the turn that asked. state.turn_id is the parent's turn, and turn.answer() on it
answers a subagent's approval too, on the subagent's session. Then wait on the parent's turn again
with turn.result(after=state), as in
Step through approvals. Replace main() with:
async def main() -> None:
session = await agent.start_session()
state = await session.run("What do the Atlas and the Nova cost?")
while isinstance(state, agents.Blocked):
turn = session.turn(state.turn_id)
for callback in state.callbacks:
if isinstance(callback, agents.OpenApproval):
print(callback.session_id == session.id, callback.request.tool.id)
await turn.answer(callback.id, agents.AllowApproval())
state = await turn.result(after=state)
print(state.text)
await session.cancel()An allow-all handler approves older subagents' calls too
The handler answers every approval in the tree, including the approvals of subagents that an
earlier turn left running. Suppose turn 1 left a deployer subagent that waits on a deploy call,
and turn 2 asks "is CI green?". An allow-all handler on turn 2 also approves the deploy. Write the
handler for every tool in the tree, not only for the tools that the current task uses.
Common issues
| You see | Cause | Fix |
|---|---|---|
session.run returns Blocked with a callback whose session_id is not the root's | A subagent waits on an approval and no handler answered it. | Answer it with turn.answer(callback.id, ...) on the root's turn, or pass on_approval= to start_session. |
UserWarning: This agent declares subagents, but its permission policy denies 'agents.spawn', ... | A default="deny" policy has no rule for agents.spawn, the tool that starts a subagent. Every start is denied. | Add agents.PermissionRule(tools=("agents.spawn",), decision="allow"), or "ask", before the first rule that matches it. |
Next
- Approvals: which calls ask, and how a person answers them.
- Step through approvals: answer open approvals in a loop, then wait for the turn again.
- One approval policy for every subagent: one handler for a lead, a tester and a docs writer, in a full program.
- Approvals reference: what a handler receives and returns.