One approval policy for every subagent
A lead agent ships a feature: it starts a tester and a docs writer, waits for both, and reports. Their tools ask for approval. You do not want a handler per child: one policy, set on the lead's session, should answer for the whole tree.
The children run as subagents, in sessions of their own. Their approvals go to the lead session's handler while a call on that session waits: here, first the call that drives the session, then a bot that answers for a session it does not drive.
Demonstrates: subagents, approvals answered for a session tree, following a session's events, a timeout.
Run: put the code blocks in one file, subagent_approvals.py, and run python subagent_approvals.py
(setup).
The lead and its children
import asyncio
from mistralai import agents
PUBLISHED: list[str] = []
@agents.tool
def run_tests(feature: str) -> str:
"""Run the test suite of a feature."""
return f"{feature}: 128 passed"
@agents.tool
def publish_docs(feature: str, text: str) -> str:
"""Publish the docs of a feature."""
PUBLISHED.append(feature)
return f"published the docs of {feature}"
lead = agents.Agent(
model="mistral-large-4",
name="release-lead",
instructions=(
"You ship features. For each feature, start one tester and one docs writer, "
"wait for both, then say in one line what was done."
),
subagents=[
agents.Subagent(
name="tester",
description="Runs the tests of one feature and reports the result.",
instructions="Call run_tests once, then answer with the result.",
tools=[run_tests],
builtins=agents.Builtins(subagents=False),
),
agents.Subagent(
name="docs-writer",
description="Writes two lines of docs for one feature and publishes them.",
instructions="Write two lines of docs, call publish_docs once, then answer in one line.",
tools=[publish_docs],
builtins=agents.Builtins(subagents=False),
),
],
permission_mode="ask", # the default "auto" asks nothing, so no handler runs
)A child inherits every setting it leaves unset from the lead; a list replaces it, and an empty list
clears it. agents.Builtins(subagents=False) keeps the children from starting children of their
own. The children run
under the lead's permission_mode, so their tools ask too.
One approval policy for every subagent
def ci_policy(
ctx: agents.ApprovalCallbackContext, request: agents.ApprovalRequest
) -> agents.AllowApproval | agents.KeepCallbackOpen:
ids = ("client.run_tests", "client.publish_docs", "agents.spawn") # the children's tools, and starting them
if request.tool.id in ids:
return agents.AllowApproval()
return agents.KeepCallbackOpen()ci_policy allows the two tools the children use, and agents.spawn, which the lead calls to start
each child. In ask mode, starting a child asks too, so the policy allows agents.spawn: the
Allow tools.agents.spawn? lines in the output below are those approvals. The policy returns
agents.KeepCallbackOpen() for anything else, so a person decides.
Approvals across subagents explains which handler answers a
child's approval.
ship sets the policy on the session, and the call waits while it answers the children's
approvals. The children can still run after the lead's turn ends; session.subagents() finds them.
session.cancel() in finally ends the session and its children. Until then, the policy answers
every approval of every child of the session, even after the lead's turn ends, so keep it narrow: an
allow-all policy would also approve a dangerous tool that a child calls later.
async def ship(lead: agents.Agent) -> agents.TurnState:
session = await lead.start_session(on_approval=ci_policy) # answers the whole tree
try:
state = await session.run("Ship feature X", timeout=600) # children's approvals too
for child in await session.subagents(): # they can still run after the lead's turn
print(f" child session {child.id}")
return state
finally:
await session.cancel()A bot that answers for another client's session
Another client starts a turn with no handler. A review bot follows the session and, for each turn, waits on it with the policy, so it answers the whole tree while it waits:
async def keep_answering(lead: agents.Agent, session_id: str) -> None:
"""A review bot: answers for a session that it does not drive."""
session = lead.session(session_id, on_approval=ci_policy)
async for event in session.iter_events(): # replays from the start, then follows
if isinstance(event, agents.TurnStarted):
await session.turn(event.turn_id).result() # answers the whole tree while it waitsmain runs both: ship, then a session where the bot answers. It prints each approval that opens
on the session it watches:
async def main() -> None:
state = await ship(lead)
print(f"ship: {type(state).__name__}: {state.text}")
# Another client starts a turn with no handler; the review bot answers its approvals.
session = await lead.start_session()
bot = asyncio.create_task(keep_answering(lead, session.id))
try:
await session.send_message("Ship feature Y")
async for event in session.iter_events():
if isinstance(event, agents.CallbackOpened):
print(f" open: {event.callback.title}")
if isinstance(event, agents.TurnEnded) and event.session_id == session.id:
print(f"bot: {type(event.state).__name__}: {event.state.text}")
break
finally:
bot.cancel()
await session.cancel()
print(f"docs published: {PUBLISHED}")
if __name__ == "__main__":
asyncio.run(main())Output
ship answered both children's approvals and listed their sessions. In the second session, the
client that sent "Ship feature Y" had no handler: the bot answered the approvals that opened on
it, the children's tools ran (docs published lists both features), and the turn completed.