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())
Output
The Atlas costs 1,200 EUR and the Nova costs 850 EUR.

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 on request.tool.id, never on request.tool.name. See Approvals for why.
  • ctx.session.id and ctx.session.turn_id name 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()
Output
True agents.spawn
True agents.spawn
False client.lookup_price
False client.lookup_price
The Atlas costs 1,200 EUR and the Nova costs 850 EUR.
Warning

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

Common issues

You seeCauseFix
session.run returns Blocked with a callback whose session_id is not the root'sA 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.