第 8 章:子代理与任务协议 —— 委派不是再调用一次模型
子代理的难点不是启动,而是边界:继承什么、不继承什么、如何取消、如何计费、如何把步骤重新投影到父线程。
task 是唯一的模型入口
启用 subagent_enabled 后,Lead Agent 获得 task(description, prompt, subagent_type)。内置类型通常有:
general-purpose:有独立上下文的通用执行器;bash:命令执行专用,但只有明确允许 host bash 或使用隔离 shell 时才可用;subagents.custom_agents中声明的自定义类型。
task_status 不再暴露给模型,后台轮询由工具内部处理,减少模型为了等任务不断调用状态工具的循环。
委派边界
子代理继承:
- 同一 sandbox 和 thread_data,因此可以协作同一工作区;
- 可信 user_id、role、OAuth/channel 身份与 authz attributes;
- 父 Agent 的 tool group 上界;
- skills allowlist 的交集;
- trace/run 元数据,便于统一归因。
子代理不继承:
- 父 Agent 的完整消息历史;
task工具本身,防止无限递归委派;- 当前轮上传扫描工具;
- Lead Agent 的全部 middleware 与产品状态。
这就是“共享能力边界,隔离思考上下文”。
执行器为何有独立事件循环
SubagentExecutor 可以把任务提交到隔离的 asyncio event loop。这样父 graph 的同步/异步调用方式、已有 loop 和子任务生命周期不会互相卡死。提交时复制必要 ContextVar,但身份仍以显式 runtime context 为准,避免线程切换后上下文变量丢失。
每个子代理仍使用 create_agent(),但工具表和 middleware 是子代理版本:保留输入/结果安全、授权、技能、预算、循环与 terminal guards,去掉标题、主线程 Todo 等无关产品行为。
任务状态机
SubagentResult 用原子式终态设置,防止取消和完成同时到达时互相覆盖。取消是协作式的:在 stream 迭代边界检查 cancel event;一个长时间不返回的单次工具调用不能立刻中止,因此底层 sandbox/tool 仍需要自己的 timeout。
父线程中的双重记录
任务结果以两种形式保存:
taskToolMessage:给模型下一轮使用,附带 status、result/error、model、token usage;delegationsledger:给摘要后的 durable context 使用,只保留 brief/hash/ref 等有界信息。
为什么两份?ToolMessage 属于活跃对话,未来可能被摘要移除;ledger 属于状态通道,确保 Lead Agent 记得已委派什么、结果在哪里,避免压缩后重复委派。
实时步骤如何到前端
执行器在运行中发出自定义事件:
| 事件 | 内容 |
|---|---|
task_started | task_id、描述、类型、实际模型 |
task_running | 新步骤、累计 token usage |
task_completed | 结果与终态元数据 |
task_failed/cancelled | 错误或取消原因 |
Worker 只把 root namespace 事件用于主状态,但会将子代理步骤持久化成 subagent.step run event。前端 core/tasks 对实时事件做增量折叠;刷新后展开卡片,再分页拉取该 task_id 的历史步骤。
usage 是累计快照而非增量。前端保留最大值,因此事件重放或乱序不会重复计费。
并发与总量上限
SubagentLimitMiddleware 同时约束:
- 单次模型输出允许的并行 task 数;
- 整个父运行允许创建的 task 总数。
只限制并发不够:模型可以每轮创建三项,循环二十轮。只限制总量也不够:第一次就扇出几十个会打爆模型/沙箱资源。两个维度共同约束。
委派何时值得
源码中的 tool docstring 明确把委派成本写给模型:重复仓库探索、协调、验证和结果合成都要算。推荐场景是独立并行、专用能力或需要上下文隔离;“任务很复杂”本身不是理由。
这是一条很成熟的设计原则:子代理是资源优化和边界工具,不是把所有问题层层外包的默认模式。
源码锚点
下一章研究时间尺度更长的问题:一次 run 结束后,Agent 还记得什么?