Délégation à un sous-agent
Responsabilité
delegate_tool permet à l'agent principal de déléguer une sous-tâche à un sous-agent isolé : le sous-agent a sa propre conversation, son propre terminal, son propre set d'outils (tools) ; une fois terminé, il ne rend que le résultat final à l'agent parent. Cela réalise un pipeline « à coût de contexte (context) nul » — le contexte du parent n'est pas pollué par les détails du process de la sous-tâche. Le sous-agent tourne dans un worker ThreadPoolExecutor ; les commandes dangereuses sont auto-deny par défaut (auto-approve configurable).
Mot de conception
Les longs pipelines explosent, non pas parce que le modèle est mauvais, mais parce que l'agent parent a vu tout le détail intermédiaire des appels d'outils. Un grep qui renvoie 200 lignes, un diff de patch raté, un build d'essai-erreur — une fois ces détails entassés dans le contexte du parent, les décisions qui suivent sont diluées. Le pari de delegate_task est que « le process du sous-agent est invisible pour le parent » : seul le texte final est réinjecté. Le prix à payer est la perte d'observabilité, donc une couche d'observation est venue s'ajouter : DaemonThreadPoolExecutor pour que les threads des sous-agents ne bloquent pas la sortie du processus, le registre (registry) _active_subagents pour que la TUI puisse lister / suspendre / interrompre les sous-agents vivants, et max_concurrent_children + max_spawn_depth comme plafonds durs de concurrence et de profondeur. La frontière d'isolation est gagnée en échangeant « pouvoir voir, pouvoir arrêter ».
Fichiers clés
def delegate_task:2426— entrée de la délégationclass DelegateEvent:625— enum des événements de délégationcallbacks auto-deny / auto-approve:61-103—_subagent_auto_deny(défaut sûr) /_subagent_auto_approve(opt-in YOLO)_get_subagent_approval_callback:102-160— choisit le callback selon la configdelegation.subagent_auto_approveDaemonThreadPoolExecutor:1978-1990— pool de threads résident pour les sous-agentswith DaemonThreadPoolExecutor:2651-2660— contexte d'exécution de la délégationtools/daemon_pool.py— implémentation deDaemonThreadPoolExecutorterminal multi-backends:5-15— le terminal du sous-agent s'exécute sur l'un des 6 backends de bac à sable (sandbox) (voir Bac à sable)
Flux de données
- La boucle principale reçoit un
tool_call: delegatedu provider. delegate_task:2426est appelé, reçoit la description de la sous-tâche et les toolsets optionnels.- Un worker est lancé dans
DaemonThreadPoolExecutor:2651:- l'initializer du worker installe le callback d'approbation (
tools/delegate_tool.py:102), auto-deny par défaut pour les commandes dangereuses - le sous-agent construit son propre contexte de conversation, appelle
run_conversation:588pour boucler - les appels d'outils du sous-agent passent par
registry:765(les toolsets peuvent être restreints par le parent)
- l'initializer du worker installe le callback d'approbation (
- Une fois le sous-agent terminé, seul le texte final est renvoyé ; delegate_tool l'enveloppe via
tool_result:798pour réinjection au parent. - Le contexte du parent n'a grossi que d'un résultat d'outil, sans pollution par le process.
Pourquoi installer un callback d'approbation ? Le commentaire est direct : le callback d'approbation interactif du CLI est stocké dans un threading.local() de terminal_tool.py, le thread worker ne l'hérite pas, et sans callback installé il retombe sur input() — lire stdin depuis un thread worker provoque un deadlock avec la TUI prompt_toolkit du parent. Le défaut est donc deny :
def _subagent_auto_deny(command: str, description: str, **kwargs) -> str:
"""Auto-deny dangerous commands in subagent threads (safe default)."""
logger.warning(
"Subagent auto-denied dangerous command: %s (%s). "
"Set delegation.subagent_auto_approve: true to allow.",
command, description,
)
return "deny"L'exécution déléguée utilise DaemonThreadPoolExecutor plutôt que la bibliothèque standard, pour la raison exposée en tête du module daemon_pool.py : les workers de la bibliothèque standard sont des threads non-daemon, et _python_exit les join sans condition, si bien qu'un worker bloqué peut figer la sortie du processus pendant plusieurs minutes. La version daemon spawn des threads daemon et saute l'enregistrement dans _threads_queues. La première étape de l'entrée delegate_task est de consulter le commutateur de pause is_spawn_paused() — ce commutateur est déclenché par la touche p de la TUI ou par le RPC delegation.pause, et quand un arbre de délégation s'auto-agrandit il gèle les nouvelles délégations sans toucher aux sous-agents déjà en cours :
if is_spawn_paused():
return tool_error(
"Delegation spawning is paused. Clear the pause via the TUI "
"(`p` in /agents) or the `delegation.pause` RPC before retrying."
)La profondeur est aussi plafonnée dur : par défaut MAX_DEPTH = 1, du parent (0) à l'enfant (1) on touche le fond, un petit-enfant est refusé ; pour ouvrir plusieurs niveaux il faut configurer explicitement delegation.max_spawn_depth.
Limites et échecs
- Fuite de budget (budget) : un sous-agent a son propre
max_iterations, mais la consommation de tokens est invisible au parent ; en déléguant en batch à N sous-agents, les coûts provider se multiplient par N d'un coup.max_concurrent_childrenpar défaut à 3 n'est qu'un plafond mou ; mal configuré, il fait fondre le budget d'un seul coup. - Deadlock imbriqué : la profondeur par défaut est 1 ; si on relève explicitement
max_spawn_depth, un sous-agent de rôleorchestratorpeut re-déléguer, et un seul étage intermédiaire bloqué fige toute la chaîne.DaemonThreadPoolExecutorne résout que le blocage de sortie du processus, pas les pendulations à l'exécution. - Mauvaise cible pour le callback d'approbation :
subagent_auto_approve=trueest une config au niveau process ; une fois activée, tous les sous-agents passent en YOLO. Il n'y a pas de granularité par job, et en scénario batch il est facile de l'activer par erreur. - Fiabilité du résultat : le parent ne reçoit que le texte final, le process intermédiaire n'est pas dans son contexte. Si un sous-agent présente une hallucination comme un fait dans sa réponse finale, le parent ne peut pas corriger à partir du process ; sur les longues chaînes de délégation les hallucinations se propagent d'étage en étage.
Résumé
Délégation à un sous-agent = isolation de contexte + contrôle des commandes dangereuses. Le parent ne voit que le résultat, pas le process, ce qui évite que les longs pipelines n'explosent sous le bruit de process. La sécurité par défaut est auto-deny ; l'auto-approve est opt-in pour les scénarios batch / cron. L'exécution du terminal se pose sur un backend de bac à sable.