Skip to content

Subagent Delegation

源码版本v2026.7.20

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

Data Flow

  1. The main loop's provider returns tool_call: delegate.
  2. delegate_task:2426 is invoked with the subtask description and optional toolsets.
  3. 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:588 to run the loop
    • the subagent's tool calls go through registry:765 (toolsets can be restricted by the parent)
  4. When the subagent finishes, only the final text is handed back; delegate_task wraps it via tool_result:798 and backfills it to the parent.
  5. 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:

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"

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:

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."
    )

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_children defaults 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, an orchestrator-role subagent can delegate again, and any stuck middle layer holds up the whole chain. DaemonThreadPoolExecutor only fixes process-exit hangs, not runtime stalls.
  • Wrong audience for the approval callback: subagent_auto_approve=true is 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.

Unofficial community learning site. Content based on the MIT-licensed NousResearch/hermes-agent source.