Une seule politique d'approbation pour tous les sous-agents

Un agent principal livre une fonctionnalité : il lance un testeur et un rédacteur de documentation, attend les deux, puis fait son compte rendu. Leurs outils demandent une approbation. Vous ne voulez pas d'un gestionnaire par agent enfant : une seule politique, définie sur la session de l'agent principal, doit répondre pour toute l'arborescence.

Les agents enfants s'exécutent en tant que sous-agents, chacun dans sa propre session. Leurs demandes d'approbation sont transmises au gestionnaire de la session principale pendant qu'un appel sur cette session reste en attente : nous utilisons d'abord l'appel qui pilote la session, puis un bot qui répond pour une session qu'il ne pilote pas.

Démonstration : sous-agents, demandes d'approbation traitées pour une arborescence de sessions, suivi des événements d'une session, délai d'expiration.

Exécution : placez les blocs de code dans un seul fichier, subagent_approvals.py, et exécutez python subagent_approvals.py (configuration).

L'agent principal et ses agents enfants

L'agent principal et ses agents enfants

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
)

Un agent enfant hérite de chaque paramètre de l'agent principal qu'il ne définit pas lui-même ; une liste remplace la valeur héritée et une liste vide l'efface. agents.Builtins(subagents=False) empêche les agents enfants de lancer leurs propres agents enfants. Les agents enfants s'exécutent avec le permission_mode de l'agent principal : leurs outils demandent donc aussi une approbation.

Une seule politique d'approbation pour tous les sous-agents

Une seule politique d'approbation pour tous les sous-agents

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 autorise les deux outils utilisés par les agents enfants, ainsi que agents.spawn, que l'agent principal appelle pour lancer chaque agent enfant. En mode ask, le lancement d'un agent enfant demande aussi une approbation. La politique autorise donc agents.spawn : les lignes Allow tools.agents.spawn? dans la sortie ci-dessous correspondent à ces approbations. Pour toute autre demande, la politique renvoie agents.KeepCallbackOpen(), afin de laisser une personne décider. Approbations entre sous-agents explique quel gestionnaire répond à une demande d'approbation d'un agent enfant.

ship définit la politique sur la session et l'appel reste en attente pendant que la politique répond aux demandes d'approbation des agents enfants. Les agents enfants peuvent continuer à s'exécuter après la fin du tour de l'agent principal ; session.subagents() permet de les trouver. session.cancel() dans finally met fin à la session et à ses agents enfants. Jusque-là, la politique répond à chaque demande d'approbation de chaque agent enfant de la session, même après la fin du tour de l'agent principal. Limitez donc sa portée : une politique qui autorise tout approuverait aussi un outil dangereux appelé plus tard par un agent enfant.

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()
Un bot qui répond pour la session d'un autre client

Un bot qui répond pour la session d'un autre client

Un autre client démarre un tour sans gestionnaire. Un bot de relecture suit la session et, à chaque tour, attend sa fin en appliquant la politique. Il répond ainsi pour toute l'arborescence pendant cette attente :

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 waits

main exécute les deux exemples : ship, puis une session où le bot répond. Il affiche chaque demande d'approbation ouverte sur la session qu'il surveille :

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())
Sortie

Sortie

Output
child session 01a1a86c-248e-7d92-b441-da4dee21de2b
child session 01a1a86c-2469-78c6-9b81-deb5ee01159a
ship: Completed: Feature X shipped: the tester ran its suite (128 tests passed) and the docs writer published two lines of docs for it.
open: Allow tools.agents.spawn?
open: Allow tools.agents.spawn?
bot: Completed: Feature Y shipped: tests passed (128/128) and its two-line docs were written and published.
docs published: ['Feature X', 'Y']

ship a répondu aux demandes d'approbation des deux agents enfants et a listé leurs sessions. Dans la seconde session, le client qui a envoyé « Livrer la fonctionnalité Y » n'avait pas de gestionnaire : le bot a répondu aux demandes d'approbation ouvertes sur cette session, les outils des agents enfants se sont exécutés (docs published liste les deux fonctionnalités) et le tour s'est terminé.

Et après

Et après

  • Sous-agents : les agents enfants, leurs sessions et session.subagents().
  • Approbations : quel gestionnaire répond à quelle demande d'approbation.