子代理派生
职责
delegate_tool 让主 agent 把一个子任务派生给隔离的子代理:子代理有自己的对话、自己的终端、自己的工具集,跑完只把最终结果交回父代理。这实现了「零上下文成本」的流水线——父代理上下文不被子任务的过程细节污染。子代理在 ThreadPoolExecutor worker 里运行,危险命令默认 auto-deny(可配置 auto-approve)。
设计动机
长流水线会爆,不是因为模型不行,而是因为父代理看完了所有工具调用的中间过程。grep 出 200 行、patch 失败的 diff、试错性的 build——这些细节塞进父代理上下文后,后面决策就被稀释。delegate_task 的赌注是「子代理的过程对父代理不可见」,只让最终文本回填。代价是失去可观测性,所以补了一套观测层:DaemonThreadPoolExecutor 让子代理线程不阻塞进程退出,_active_subagents 注册表让 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 个沙箱后端之一执行(见沙箱)
数据流
- 主循环 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(可被父代理限制 toolsets)
- worker initializer 安装审批回调(
- 子代理完成后只把最终文本交回,delegate_task 经
tool_result:798封装回填给父代理。 - 父代理上下文只增长一条 tool 结果,不被过程污染。
为什么要装审批回调?注释里写得直白:CLI 的交互式审批回调存在 terminal_tool.py 的 threading.local() 里,worker 线程不继承,不装回调会 fallback 到 input(),从 worker 线程读 stdin 会和父代理的 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 版本 spaw daemon 线程并跳过 _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。
边界与失败
- 预算泄漏:子代理有自己的
max_iterations,但 token 花费对父代理不可见,批量派生 N 个子代理时,N 倍 provider 费用直接堆上去。max_concurrent_children默认 3 是软上限,配置高了会一次性烧穿预算。 - 嵌套死锁:默认深度是 1,显式调高
max_spawn_depth后,orchestrator角色的子代理能再派生,任意中间层卡住都会拖住整条链。DaemonThreadPoolExecutor只解决进程退出卡死,不解决运行时挂起。 - 审批回调用错人:
subagent_auto_approve=true是 process 级配置,开了之后所有子代理都 YOLO。没有 per-job 粒度,批量场景容易误开。 - 结果可信度:父代理只拿最终文本,中间过程不在上下文里。子代理把幻觉当事实写进最终回复,父代理没办法基于过程纠偏,长链路派生时幻觉会逐层放大。
小结
子代理派生 = 上下文隔离 + 危险命令管控。父代理只看结果,不看过程,这让长流水线不被过程噪声撑爆。安全默认 auto-deny,批量/cron 场景可 opt-in auto-approve。终端执行落在沙箱后端。