从「单兵死磕」到「分身协作」
task 工具。🎯 本章导读
- 说清「委派」里隔离的到底是什么:只有消息历史和工具注册表,其他全部共享。
- 写一个自包含的 task 工具:description 让子 Agent 不靠父 history 也能独立完成任务。
- 说出父 history 在委派后只多哪三条消息,以及为什么子 Agent 的推理过程无法审计。
- 解释为什么必须禁止递归委派(三层防线),以及子 Agent 跑满 30 轮返回什么。
- 跑通第 6 章:让父 Agent 委派一个子 Agent 干一件事,再自己给结论。
你需要先具备什么 ▸
| 需要 | 说明 |
|---|---|
| 读完第 1—5 章 | 会看 AgentRunner、Hook、权限、tool 注册即可 |
| 多 Agent 概念 | 不需要。本章就是第一次引入「子 Agent」 |
建议的阅读路线 ▸
本章导读(你在这)
↓
① 先看一眼一次真实的委派 ← 子 Agent 干活的完整日志
↓
② 这章解决什么问题 ← 上下文隔离,不是沙箱
↓
③ 严格的 task 输入 ← 自包含 description
↓
④ SubagentTool = 执行边界 ← 复用同一个 AgentRunner
↓
⑤ 历史隔离 + 边界共享 ← 三、四
↓
⑥ 禁止递归委派 ← 三层防线
↓
⑦ 30 轮上限 + 脱敏错误 ← 六
↓
⑧ 组合根 + 离线证明 ← 七、八
↓
⑨ 运行 + 实验 + 差异 + 小结 ← 九~十一
📇 术语速查
| 术语 | 一句话解释 |
|---|---|
| 委派(task) | 把一段自包含探索交给独立子 Agent,只拿回一句结论 |
| SubagentTool | 执行边界的实现:复用同一个 AgentRunner 跑一次内部 run() |
| 自包含 description | 子 Agent 只能看到 description,看不到父 history,所以描述必须完整 |
| 历史隔离 | 父与子各有一份独立 #history,互不可见 |
| 边界共享 | Hook、权限、workspace、identity、进程全部共享一个实例 |
| 递归委派 | 子 Agent 自己再调 task。三层防线禁止 |
| evidence-based 结论 | 因为子推理不可审计,所以要求子 Agent 给出有文件证据的结论 |
subagent_turn_limit | 子 Agent 跑满 30 轮返回的结构化错误码 |
subagent_configuration_error | 子工具集非法(例如包含 task)时的错误码 |
| toolRoundObserver | 父挂的「计划陈旧提醒」观察器;子 Agent 刻意没挂 |
📡 先看一眼一次真实的委派
父子日志交错 ▸
npm run ch06 -- --prompt "调用 task 独立检查本项目使用的测试框架,只返回有文件证据的结论"
# stderr 上父子交错:
# [Hook] UserPromptSubmit (父)
# [Hook] PreToolUse: task (父)
# [Hook] UserPromptSubmit ← 子 Agent 那次 run() 也打日志!
# [Hook] PreToolUse: read_file (子)
# ...
# Hook 是共享实例,所以子 Agent 的 run() 也会打 UserPromptSubmit 和 Stop
那如果子 Agent 想越界写呢 ▸
子 Agent 的 write_file 照样走第 3 章权限:工作区内弹审批、工作区外被硬边界 deny。`task` 自己是 allow(读取类),但子 Agent 的写入照样弹到你面前——审批框上不标注「这是子 Agent 发起的」,因为父子共享 identity。
🧠 核心概念
1 · 这章解决什么:上下文隔离 ▸
第 5 章 todo_write 解决「单会话如何保存计划」,但不减少探索过程本身的上下文。第 6 章解决「如何隔离一段独立探索」:父调用 task,运行时创建新 AgentRunner:
父 Agent
assistant: task(description=...)
↓
一次性 Subagent
system: 专注完成委派任务,不再委派
user: description
history: 全新
tools: shell/read_file/write_file/edit_file/glob/todo_write
↓
父 Agent
tool: 子 Agent 的 finalText
2 · 严格的 task 输入:自包含 ▸
description 必须 trim 后非空,.strict() 拒绝额外字段。为什么强调「自包含」?因为子 Agent 不继承父 history。
"继续处理刚才那个问题" — 子 Agent 看不到「刚才」,信息不足。"检查 package.json 和测试目录,确认项目测试框架;只返回能由文件内容支持的结论。" — 对象、目标、证据要求都明确。3 · 父 history 为什么保持干净 ▸
子 Agent 可多轮 read_file/glob/shell,但 SubagentTool 只返回 result.finalText。父 history 只保存:
user: 父任务
assistant: tool_calls=[task(parent-task)]
tool: 子 Agent 的 finalText, toolCallId=parent-task
assistant: 父 Agent 的最终回答
子内部历史不 append 到父。父 Loop 仍按 tool_call_id 配对写 tool message,OpenAI 工具配对契约无例外。
4 · 历史隔离,运行边界共享 ▸
子 Agent 用新 AgentRunner,但不是新权限系统。构造时直接传入父会话组合根创建的同一实例:hooks、permissionPolicy、workspace、identity。
子工具调用顺序仍是:PreToolUse → PermissionPolicy → handler → PostToolUse。子尝试向 workspace 外写入,第 3 章硬边界先 deny,审批器不会被调用。合法的子 write_file 仍按原策略请求审批并写入同一 workspace,父之后可继续读。
5 · 为什么禁止递归委派(两层防护) ▸
P06 父工具有 7 个(含 task),子工具工厂只创建前 6 个(不含 task)。两层防护:
- 工厂不注册:正常组装路径,子工具集不含 task。
- 运行时拒绝:若 tools.names.includes("task") 返回 subagent_configuration_error,保护工厂被错误改写。
若子模型仍生成 task(...),Registry 返回 unknown_tool: Unknown tool: task,不会创建孙 Agent。
6 · 30 轮上限与脱敏错误 ▸
DEFAULT_SUBAGENT_MAX_TURNS=30,构造时不允许高于 30。子连续 30 轮只返回工具调用时,公共 Loop 抛 AgentLimitError,task handler 转成稳定错误:
Error [subagent_turn_limit]: Subagent exceeded max_turns=30 without a final answer
父不会收到第 30 轮最后一个工具输出,不能把它误认为结论。其他异常也脱敏:subagent_execution_error,不泄漏 API Key/路径/适配器细节。
7 · effect: external 的含义 ▸
task 的 effect 是 "external"(发起一段独立执行,而非直接读写文件)。第 3 章审批仍由工具名和硬边界决定,不会因 task 标 external 就把所有子工具误判成磁盘写入。子工具是否允许写盘,仍由各自工具名和权限边界决定。
▶️ 交互演示:父-子上下文隔离
🧑 父 Agent 历史 只收 finalText
🧒 子 Agent 历史 全新,父看不到中间
🔑 一句话总结
子 Agent 可以有很长的探索轨迹,但父上下文只留下被委派的动作和可用于后续推理的结论。历史隔离 ≠ 权限隔离,运行边界共享。
🚀 运行第 6 章
第 0–1 步 · 环境与离线测试 ▸
Set-Location 'F:\笔记\Agent实操\code'
npm ci
npm run test:ch06
Test Files 16 passed (16)
Tests 118 passed (118)
第 2–4 步 · 观察一次委派、观察审批、统一入口 ▸
npm run ch06 -- --prompt "调用 task 独立检查本项目使用的测试框架,只返回有文件证据的结论"
# 父子日志交错;Hook 共享实例,子 Agent 的 run() 也打 UserPromptSubmit/Stop
npm run ch06 -- --prompt "调用 task 让子 Agent 把一句话写入 sub-demo.txt"
# task 自己 allow;子 Agent 的写入照样弹审批框
npm run agent-tutorial -- run --chapter 6 --prompt "调用 task 总结 chapters/ch06/src/core 目录职责,再由父 Agent 给出结论"
常见报错排查表 ▸
| 现象 | 原因 / 处理 |
|---|---|
Error [subagent_turn_limit] | 子 Agent 跑满 30 轮还没给最终文本。把委派任务拆小。不要调高上限——构造时会拒绝 >30 |
| 子 Agent 越界写入没弹审批框 | 预期行为。硬边界在规则之前,越界直接 deny,压根到不了审批 |
子 Agent 调 task 报 subagent_configuration_error | 运行时防递归第三层,正常。子工具集本就不含 task |
| 启动报 maxTurns must be at most 30 | 构造期校验,配置错误在组合根就炸 |
🧪 验证与实验
npm run test:ch06 执行 16 个测试文件、118 个测试。四个建议动手的小实验:四个建议动手做的小实验 ▸
| 实验 | 预期 | 学到什么 |
|---|---|---|
| 一·maxTurns 调到 31 在 new SubagentTool 里加 maxTurns: 31 | 启动就抛 maxTurns must be at most 30,模型一次没被调用 | 上限是构造期校验,配置错误应在组合根就炸 |
| 二·让子工具集包含 task | 返回 subagent_configuration_error,模型请求只有父那一次 | 运行时防递归第三道防线,子 Agent 压根没被创建 |
| 三·验证子 Agent 看不到父 history | 子 Agent 只看到 description 那一句,必须自己再读文件 | 隔离是真的;description 必须自包含 |
| 四·观察子 Agent 的 todo_write 有没有提醒 | 没有。子 Agent 没挂 toolRoundObserver | 刻意的不对称:有工具但没机制 |
📝 本章小结
- task 把一段自包含探索搬到一个全新历史里跑,父 Agent 只拿回一句结论。
- 隔离的只有消息历史和工具注册表;Hook、权限、workspace、identity、进程全部共享。
- 委派不创造新路径:子 Agent 该弹的审批照样弹,该拦的越界照样拦。
一定要记住的八条 ▸
| # | 结论 | 出现在哪一节 |
|---|---|---|
| 1 | 「隔离」= 上下文隔离,不是沙箱、不是提权、不是降权 | 这章解决什么问题 |
| 2 | 子 Agent 看不到父 history,所以 description 必须自包含 | 一 |
| 3 | SubagentTool 不是第二套 Loop,它复用同一个 AgentRunner | 二 |
| 4 | 父 history 只多三条:assistant(task)、tool(结论)、assistant(最终回答) | 三 |
| 5 | 代价是子 Agent 的推理过程无法审计——所以要求 evidence-based 结论 | 三 |
| 6 | 禁止递归防三层:提示、工厂、运行时;第三层保证「模型零调用」 | 五 |
| 7 | 跑满轮数返回结构化错误,绝不把最后一个工具输出冒充结论 | 六 |
| 8 | 子 Agent 有 todo_write 但没有 toolRoundObserver——刻意的不对称 | 七 |
本章代码边界 ▸
P06 只加了「一层同步委派」,没加并行、没加持久化、没加沙箱。并行(第 13 章后台任务)、多 Agent 认领(第 17、18 章)、Inbox 异步协作(第 15 章)都在后面。
检查你是否真的读懂了(不看文章回答) ▸
- 子 Agent 在 workspace 里写的文件,task 返回后还在吗?为什么?
- 父 Agent 调用一次 task,父 history 里多了几条消息?分别是什么?
- 子 Agent 的 write_file 会弹审批框吗?审批框上能看出这是子 Agent 吗?
- 给子 Agent 配「永远批准」的审批器,它能写出 workspace 吗?
- 子模型自己生成了 task(...) 调用,它会收到什么?
- 子 Agent 跑满 30 轮,父 Agent 收到的是第 30 轮的工具输出,还是别的东西?
- 子 Agent 的工具列表里有 todo_write。它会收到「计划陈旧」提醒吗?