Skip to content

Delegación a subagentes

源码版本v2026.7.20

Responsabilidad

delegate_tool permite al agent principal delegar una subtarea a un subagente aislado: el subagente tiene su propia conversación, su propio terminal y su propio conjunto de herramientas (tool), y al acabar solo entrega el resultado final al agente padre. Esto implementa una pipeline de «coste de contexto cero»: el contexto (context) del padre no se contamina con los detalles del proceso de la subtarea. El subagente corre en un worker de ThreadPoolExecutor; los comandos peligrosos se auto-deny por defecto (configurable a auto-approve).

Motivo de diseño

Una pipeline larga revienta no porque el modelo no dé la talla, sino porque el agente padre ha visto todo el proceso intermedio de cada llamada a herramienta. Un grep que escupe 200 líneas, un diff de patch fallido, un build de prueba y error — esos detalles se meten en el contexto del padre y diluyen las decisiones posteriores. La apuesta de delegate_task es que «el proceso del subagente es invisible al padre»; solo el texto final se reenvía. El coste es perder observabilidad, así que se añade una capa de observación: DaemonThreadPoolExecutor evita que el hilo del subagente bloquee la salida del proceso; el registro _active_subagents deja que la TUI liste/pause/interrumpa los subagentes vivos; max_concurrent_children y max_spawn_depth son topes duros de concurrencia y profundidad. La frontera de aislamiento se paga con «poder verlo, poder pararlo».

Archivos clave

Flujo de datos

  1. El bucle principal recibe del provider un tool_call: delegate.
  2. Se llama a delegate_task:2426 con la descripción de la subtarea y los toolsets opcionales.
  3. Arranca un worker en DaemonThreadPoolExecutor:2651:
    • el initializer del worker instala el callback de aprobación (tools/delegate_tool.py:102); por defecto, auto-deny de comandos peligrosos
    • el subagente construye su propio contexto de conversación y llama a run_conversation:588
    • las llamadas a herramientas del subagente van al registry:765 (el padre puede restringir los toolsets)
  4. Al acabar, el subagente solo entrega el texto final al padre; delegate_task lo envuelve con tool_result:798 y lo devuelve al padre.
  5. El contexto del padre solo crece en un resultado de tool, sin contaminación de proceso.

¿Por qué instalar el callback de aprobación? El comentario lo dice claro: el callback interactivo del CLI está en un threading.local() de terminal_tool.py; el hilo worker no lo hereda. Sin callback, cae a input(), y leer stdin desde el hilo worker se deadlocka con la TUI prompt_toolkit del padre. Por eso el default es 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"

La delegación usa DaemonThreadPoolExecutor en lugar de la stdlib; el motivo está al inicio de daemon_pool.py: los workers de la stdlib son non-daemon, _python_exit les hace join incondicional, y un worker colgado puede atascar la salida del proceso varios minutos. La versión daemon spawnea hilos daemon y se salta el registro en _threads_queues. La entrada delegate_task primero revisa el interruptor de pausa is_spawn_paused(), activado por la tecla p de la TUI o por el RPC delegation.pause; si se detecta un árbol de delegación desbocado, congela nuevas delegaciones sin tocar los subagentes ya en marcha:

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 profundidad también tiene tope duro; por defecto MAX_DEPTH = 1: padre (0) → hijo (1) y se acaba. Un nieto se rechaza; para niveles múltiples hay que configurar explícitamente delegation.max_spawn_depth.

Límites y fallos

  • Fuga de presupuesto (budget): el subagente tiene su propio max_iterations, pero el coste en tokens es invisible para el padre; si lanzas N subagentes en lote, el coste de provider se multiplica por N. max_concurrent_children por defecto es 3 — un soft cap; subirlo en config puede quemarse el presupuesto de golpe.
  • Deadlock anidado: por defecto la profundidad es 1; si se sube max_spawn_depth, un subagente con rol de orchestrator puede volver a delegar y cualquier bloqueo en una capa intermedia atasca toda la cadena. DaemonThreadPoolExecutor arregla el bloqueo a la salida del proceso, no los hangs en runtime.
  • Callback de aprobación mal aplicado: subagent_auto_approve=true es config a nivel de proceso; una vez activado, todos los subagentes tiran en YOLO. No hay granularidad por job, fácil de activar por error en escenarios batch.
  • Fiabilidad del resultado: el padre solo recibe el texto final, el proceso intermedio no está en su contexto. Si el subagente toma una alucinación como hecho y la escribe en la respuesta final, el padre no puede corregir a partir del proceso; en cadenas largas, las alucinaciones se amplifican por capas.

Resumen

La delegación a subagentes = aislamiento de contexto + control de comandos peligrosos. El agente padre solo ve resultados, no el proceso, lo que evita que las pipelines largas revienten por el ruido del proceso. Por defecto, auto-deny; en escenarios batch/cron se puede opt-in a auto-approve. La ejecución del terminal cae en los backends de sandbox.

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT).