Skip to content

サブエージェント委譲

源码版本v2026.7.20

職務

delegate_tool が主 agent のサブタスクを隔離されたサブエージェントに委譲する:サブエージェントは自身の対話、自身のターミナル、自身のツールセット (toolset) を持ち、走り終えたら最終結果だけを親に返す。これで「コンテキストコストゼロ」のパイプラインを実現する——親 agent のコンテキスト (context) はサブタスクの過程詳細に汚染されない。サブエージェントは ThreadPoolExecutor worker で走り、危険コマンドはデフォルトで auto-deny(auto-approve に設定可能)。

設計動機

長いパイプラインが爆発するのは、モデルがダメだからではなく、親 agent がツール呼び出し (tool dispatch) の中间過程をすべて見てしまうからだ。grep の 200 行、patch に失敗した diff、試行錯誤の build——これらの詳細が親 agent のコンテキスト (context) に入ると、以降の意思決定が希釈される。delegate_task の賭けは「サブエージェントの過程は親 agent に見えない」こと、最終テキストだけを戻すことだ。代償は可観測性を失うことなので、観測層を補う:DaemonThreadPoolExecutor はサブエージェントスレッドがプロセス退出をブロックしないようにし、_active_subagents レジストリ (registry) は TUI が生存中のサブエージェントを列挙/一時停止/中断できるようにし、max_concurrent_childrenmax_spawn_depth で並行度と深さに硬い上限を付ける。隔離の境界は「見える、止められる」と引き換えに得たものだ。

主要ファイル

データフロー

  1. 主ループの provider が tool_call: delegate を返す。
  2. delegate_task:2426 が呼ばれ、サブタスク記述と任意の toolsets を受け取る。
  3. DaemonThreadPoolExecutor:2651 で worker を起動:
    • worker initializer が承認コールバックをインストール(tools/delegate_tool.py:102)、デフォルトで危険コマンドは auto-deny
    • サブエージェントは自身の対話コンテキストを構築し、run_conversation:588 を呼んでループを回す
    • サブエージェントのツール呼び出しは registry:765 を経る(親 agent が toolsets を制限可能)
  4. サブエージェント完了後、最終テキストだけを返し、delegate_task は tool_result:798 でラップして親 agent に戻す。
  5. 親 agent のコンテキストは tool 結果を一つ増やすだけで、過程に汚染されない。

なぜ承認コールバックをインストールするのか? コメントには率直に書かれている:CLI の対話型承認コールバックは terminal_tool.pythreading.local() に存在し、worker スレッドは継承しない。コールバックをインストールしないと input() にフォールバック (fallback) し、worker スレッドから stdin を読むと親 agent の prompt_toolkit TUI とデッドロックする。だからデフォルトは 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"

委譲実行が標準ライブラリではなく DaemonThreadPoolExecutor を使う理由は、daemon_pool.py のモジュール冒頭に書かれている。標準ライブラリの worker は非デーモンスレッドで、_python_exit が無条件に join する。一つの詰まった worker がプロセス退出を数分ブロックできる。Daemon 版はデーモンスレッドを spawn し、_threads_queues への登録をスキップする。委譲入口 delegate_task の最初のステップは一時停止スイッチ is_spawn_paused() をチェックすることだ。このスイッチは TUI の p キーまたは delegation.pause RPC でトリガーされ、暴走した委譲ツリーを発見したときに新規委譲を凍結するが、既に走っているサブエージェントには触れない。

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

深さも硬上限され、デフォルト MAX_DEPTH = 1 で、親(0)から子(1)で底につく。孫は拒否され、多段にするには明示的に delegation.max_spawn_depth を設定しなければならない。

境界と失敗

  • 予算 (budget) のリーク:サブエージェントは自身の max_iterations を持つが、token 消費は親 agent から見えない。N 個のサブエージェントをバッチ委譲すると、N 倍の provider 費用がそのまま積もる。max_concurrent_children のデフォルト 3 はソフト上限で、設定を高くすると一度に予算を焼き尽くす。
  • ネストデッドロック:デフォルトの深さは 1。max_spawn_depth を明示的に上げると、orchestrator ロールのサブエージェントがさらに委譲でき、中間層が一つでも詰まると鎖全体が引っかかる。DaemonThreadPoolExecutor はプロセス退出のスタルを解決するだけで、実行時のハングは解決しない。
  • 承認コールバックの誤用:subagent_auto_approve=true はプロセスレベルの設定で、オンにするとすべてのサブエージェントが YOLO になる。per-job 粒度はなく、バッチシナリオで誤ってオンにしやすい。
  • 結果の信頼度:親 agent は最終テキストだけを受け取り、中間過程はコンテキストに入らない。サブエージェントが幻覚を事実として最終返信に書いても、親 agent は過程に基づいて修正できない。長鎖委譲では幻覚が層ごとに増幅する。

まとめ

サブエージェント委譲 = コンテキスト隔離 + 危険コマンド管理。親 agent は結果だけを見て過程を見ない、これで長いパイプラインが過程ノイズで膨れ上がらない。安全デフォルトは auto-deny、バッチ/cron では opt-in で auto-approve 可能。ターミナル実行はサンドボックスバックエンドに落ちる。

非公式コミュニティ学習サイト。MIT ライセンスの NousResearch/hermes-agent ソースに基づく。