Skip to content

Délégation à un sous-agent

源码版本v2026.7.20

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

Flux de données

  1. La boucle principale reçoit un tool_call: delegate du provider.
  2. delegate_task:2426 est appelé, reçoit la description de la sous-tâche et les toolsets optionnels.
  3. 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:588 pour boucler
    • les appels d'outils du sous-agent passent par registry:765 (les toolsets peuvent être restreints par le parent)
  4. Une fois le sous-agent terminé, seul le texte final est renvoyé ; delegate_tool l'enveloppe via tool_result:798 pour réinjection au parent.
  5. 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 :

python
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 :

python
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_children par 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ôle orchestrator peut re-déléguer, et un seul étage intermédiaire bloqué fige toute la chaîne. DaemonThreadPoolExecutor ne 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=true est 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.

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).