Delegación a subagentes
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
def delegate_task:2426— entrada de delegaciónclass DelegateEvent:625— enum de eventos de delegacióncallbacks auto-deny / auto-approve:61-103—_subagent_auto_deny(seguro por defecto) /_subagent_auto_approve(opt-in YOLO)_get_subagent_approval_callback:102-160— selecciona el callback según la configdelegation.subagent_auto_approveDaemonThreadPoolExecutor:1978-1990— pool de hilos residente para subagenteswith DaemonThreadPoolExecutor:2651-2660— contexto de ejecución de la delegacióntools/daemon_pool.py— implementación deDaemonThreadPoolExecutorterminal multi-backend:5-15— el terminal del subagente se ejecuta en uno de los 6 backends de sandbox (ver Sandbox)
Flujo de datos
- El bucle principal recibe del provider un
tool_call: delegate. - Se llama a
delegate_task:2426con la descripción de la subtarea y los toolsets opcionales. - 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)
- el initializer del worker instala el callback de aprobación (
- Al acabar, el subagente solo entrega el texto final al padre;
delegate_tasklo envuelve contool_result:798y lo devuelve al padre. - 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:
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:
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_childrenpor 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 deorchestratorpuede volver a delegar y cualquier bloqueo en una capa intermedia atasca toda la cadena.DaemonThreadPoolExecutorarregla el bloqueo a la salida del proceso, no los hangs en runtime. - Callback de aprobación mal aplicado:
subagent_auto_approve=truees 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.