Subagent Delegation
Responsibility
delegate_tool lets the main agent delegate a subtask to an isolated subagent: the subagent has its own conversation, its own terminal, its own toolset, and on completion hands only the final result back to the parent. This realizes a "zero context cost" pipeline — the parent's context is not polluted by the subtask's process details. The subagent runs inside a ThreadPoolExecutor worker; dangerous commands are auto-denied by default (configurable to auto-approve).
Design Motivation
Long pipelines blow up — not because the model is bad, but because the parent agent has watched all the intermediate tool calls. 200 lines of grep output, a failed patch diff, an exploratory build — once these details flood the parent's context, later decisions get diluted. The bet of delegate_task is "the subagent's process is invisible to the parent": only the final text is backfilled. The cost is observability, so there's a layer to compensate: DaemonThreadPoolExecutor keeps subagent threads from blocking process exit; the _active_subagents registry lets the TUI list/pause/interrupt live subagents; max_concurrent_children and max_spawn_depth set hard caps on concurrency and depth. The isolation boundary is bought with "you can see it, you can stop it."
Key Files
def delegate_task:2426— delegation entryclass DelegateEvent:625— delegation event enumauto-deny / auto-approve callbacks:61-103—_subagent_auto_deny(safe default) /_subagent_auto_approve(opt-in YOLO)_get_subagent_approval_callback:102-160— picks a callback based ondelegation.subagent_auto_approveconfigDaemonThreadPoolExecutor:1978-1990— resident thread pool for subagentswith DaemonThreadPoolExecutor:2651-2660— delegation execution contexttools/daemon_pool.py—DaemonThreadPoolExecutorimplementationmulti-backend terminal:5-15— the subagent's terminal executes on one of 6 sandbox backends (see Sandbox)
Data Flow
- The main loop's provider returns
tool_call: delegate. delegate_task:2426is invoked with the subtask description and optional toolsets.- A worker is started in
DaemonThreadPoolExecutor:2651:- the worker initializer installs an approval callback (
tools/delegate_tool.py:102), auto-denying dangerous commands by default - the subagent builds its own conversation context and calls
run_conversation:588to run the loop - the subagent's tool calls go through
registry:765(toolsets can be restricted by the parent)
- the worker initializer installs an approval callback (
- When the subagent finishes, only the final text is handed back;
delegate_taskwraps it viatool_result:798and backfills it to the parent. - The parent's context grows by only one tool result, not by process noise.
Why install an approval callback at all? The comment is blunt: the CLI's interactive approval callback lives in threading.local() inside terminal_tool.py, and worker threads don't inherit it. Without a callback installed it falls back to input(), and reading stdin from a worker thread deadlocks against the parent's prompt_toolkit TUI. So the default is 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"Delegation runs on DaemonThreadPoolExecutor instead of the standard library because of what's at the top of daemon_pool.py: stdlib workers are non-daemon threads, and _python_exit joins them unconditionally — one stuck worker can hang process exit for minutes. The Daemon version spawns daemon threads and skips _threads_queues registration. The first thing delegate_task does is check the pause switch is_spawn_paused(). That switch is triggered by the TUI's p key or the delegation.pause RPC: when a delegation tree runs out of control, it freezes new spawns but leaves already-running subagents alone:
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."
)Depth is also hard-capped. Default MAX_DEPTH = 1 — parent (0) to child (1) is the end of the line, grandchildren are rejected. To open more levels you must explicitly configure delegation.max_spawn_depth.
Boundaries and Failure
- Budget leakage: A subagent has its own
max_iterations, but token spend is invisible to the parent. Fan out N subagents and N times the provider cost piles up.max_concurrent_childrendefaults to 3 — a soft cap; crank the config and you can burn through a budget in one shot. - Nesting deadlock: Default depth is 1. Once you explicitly raise
max_spawn_depth, anorchestrator-role subagent can delegate again, and any stuck middle layer holds up the whole chain.DaemonThreadPoolExecutoronly fixes process-exit hangs, not runtime stalls. - Wrong audience for the approval callback:
subagent_auto_approve=trueis a process-level setting. Turn it on and every subagent goes YOLO. There's no per-job granularity — easy to mis-enable in batch scenarios. - Result trustworthiness: The parent only gets the final text; the intermediate process isn't in context. If the subagent writes a hallucination as fact into its final reply, the parent has no way to correct it from the process. Long delegation chains let hallucinations amplify layer by layer.
Summary
Subagent delegation = context isolation + dangerous-command control. The parent sees only the result, not the process, so long pipelines aren't blown up by process noise. Safe default is auto-deny; cron/batch scenarios can opt into auto-approve. Terminal execution lands on the sandbox backends.