サブエージェント委譲
職務
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_children と max_spawn_depth で並行度と深さに硬い上限を付ける。隔離の境界は「見える、止められる」と引き換えに得たものだ。
主要ファイル
def delegate_task:2426— 委譲入口class DelegateEvent:625— 委譲イベント列挙auto-deny / auto-approve コールバック:61-103—_subagent_auto_deny(安全デフォルト)/_subagent_auto_approve(opt-in YOLO)_get_subagent_approval_callback:102-160—delegation.subagent_auto_approve設定でコールバックを選ぶDaemonThreadPoolExecutor:1978-1990— サブエージェント用の常駐スレッドプールwith DaemonThreadPoolExecutor:2651-2660— 委譲実行コンテキストtools/daemon_pool.py—DaemonThreadPoolExecutor実装多バックエンドターミナル:5-15— サブエージェントのターミナルは 6 つのサンドボックス (sandbox) バックエンドのいずれかで実行(サンドボックス参照)
データフロー
- 主ループの provider が
tool_call: delegateを返す。 delegate_task:2426が呼ばれ、サブタスク記述と任意の toolsets を受け取る。DaemonThreadPoolExecutor:2651で worker を起動:- worker initializer が承認コールバックをインストール(
tools/delegate_tool.py:102)、デフォルトで危険コマンドは auto-deny - サブエージェントは自身の対話コンテキストを構築し、
run_conversation:588を呼んでループを回す - サブエージェントのツール呼び出しは
registry:765を経る(親 agent が toolsets を制限可能)
- worker initializer が承認コールバックをインストール(
- サブエージェント完了後、最終テキストだけを返し、delegate_task は
tool_result:798でラップして親 agent に戻す。 - 親 agent のコンテキストは tool 結果を一つ増やすだけで、過程に汚染されない。
なぜ承認コールバックをインストールするのか? コメントには率直に書かれている:CLI の対話型承認コールバックは terminal_tool.py の threading.local() に存在し、worker スレッドは継承しない。コールバックをインストールしないと input() にフォールバック (fallback) し、worker スレッドから stdin を読むと親 agent の prompt_toolkit TUI とデッドロックする。だからデフォルトは 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"委譲実行が標準ライブラリではなく DaemonThreadPoolExecutor を使う理由は、daemon_pool.py のモジュール冒頭に書かれている。標準ライブラリの worker は非デーモンスレッドで、_python_exit が無条件に join する。一つの詰まった worker がプロセス退出を数分ブロックできる。Daemon 版はデーモンスレッドを spawn し、_threads_queues への登録をスキップする。委譲入口 delegate_task の最初のステップは一時停止スイッチ is_spawn_paused() をチェックすることだ。このスイッチは TUI の p キーまたは delegation.pause RPC でトリガーされ、暴走した委譲ツリーを発見したときに新規委譲を凍結するが、既に走っているサブエージェントには触れない。
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 可能。ターミナル実行はサンドボックスバックエンドに落ちる。