| 你需要先具备 | 说明 |
|---|---|
| 读完第 1—5 章 | 会看 AgentRunner、Hook、权限、tool 注册即可 |
| 多 Agent 概念 | 不需要。本章就是第一次引入「子 Agent」 |
本章导读(你在这)
↓
① 先看一眼一次真实的委派 ← 子 Agent 干活的完整日志
↓
② 这章解决什么问题 ← 上下文隔离,不是沙箱
↓
③ 严格的 task 输入 ← 自包含 description
↓
④ SubagentTool = 执行边界 ← 复用同一个 AgentRunner
↓
⑤ 历史隔离 + 边界共享 ← 三、四
↓
⑥ 禁止递归委派 ← 三层防线
↓
⑦ 30 轮上限 + 脱敏错误 ← 六
↓
⑧ 组合根 + 离线证明 ← 七、八
↓
⑨ 运行 + 实验 + 差异 + 小结 ← 九~十一大任务塞进单一上下文会让模型注意力涣散。委派 = 把一段自包含探索搬到全新历史里跑(干净上下文),父 Agent 只拿回一句结论。注意:「隔离」= 上下文隔离,不是沙箱、不是提权、不是降权。子 Agent 与父共享 workspace、身份、权限和进程。
只有一个入参:description(自包含任务描述)。它必须完整——因为子 Agent 看不到父 history,它只能靠这句话独立干活。描述里该带路径、该带验收标准、该带返回格式要求。
{\"description\": \"检查本项目用的测试框架:读 package.json、找到测试目录...\"}
SubagentTool 不是第二套 Loop——它复用同一个 AgentRunner,对同一个 context 跑一次内部 run()。只是这个内部 run 用的是全新 history 和子工具集。父 history 极少被触碰。
父调用一次 task,父 history 只多三条消息:assistant(task) → tool(结论) → assistant(最终回答)。子 Agent 的完整推理过程不进父历史——代价是子推理无法审计,所以要求 evidence-based 结论(子 Agent 必须引用文件做证据)。
| 隔离 | 共享(同一个实例) |
|---|---|
| 消息历史 #history | HookRegistry(日志因此分不清父子) |
| 工具注册表 / 子工具集 | PermissionPolicy、workspace、identity、进程 |
共享意味着子 Agent 没有豁免:它的工具调用照样打 hook 日志、照样过权限、越界照样被硬边界 deny。审批框上不标注「这是子 Agent 发起的」——因为父子共享 identity。
三层防线保证子 Agent 不可能再调 task:① 提示层:子 system 明确禁止;② 工厂层:子工具集构建时剔除 task;③ 运行时:即使模型自己生成了 task 调用,也返回 subagent_configuration_error。第三层保证「模型零调用」——子 Agent 压根没被创建,模型请求次数只有父那一次。
子 Agent 有独立 maxTurns=30,构造期校验(>30 直接拒绝启动)。跑满轮数返回结构化错误 Error [subagent_turn_limit],绝不把最后一个工具输出冒充结论。父历史仍保持「一条 tool 消息」的整洁。
// 子 Agent 异常/超限 → 结构化 ToolResult,不污染父 Loop
P06 在 bootstrap 里构造 SubagentTool 并注册为父工具集的一员。SubagentTool 需要拿到父的 AgentRunner 工厂、子工具集(不含 task)、maxTurns。父子共用 ModelClient,因此只需要一组 .env。
子 Agent 的工具列表里有 todo_write,但没有挂 toolRoundObserver——所以它不会收到「计划陈旧」提醒。子 Agent 有工具但没这层机制,这是刻意的。测试用注入的假审批器证明:即使配「永远批准」,子 Agent 也写不出 workspace(硬边界拦下)。
npm run ch06 -- --prompt \"调用 task 独立检查本项目使用的测试框架,只返回有文件证据的结论\"
# stderr 上父子交错:
# [Hook] UserPromptSubmit (父)
# [Hook] PreToolUse: task (父)
# [Hook] UserPromptSubmit ← 子 Agent 那次 run() 也打日志!
# [Hook] PreToolUse: read_file (子)
# ...
# Hook 是共享实例,所以子 Agent 的 run() 也会打 UserPromptSubmit 和 Stop
# 观测局限:共享 HookRegistry 的代价是日志里分不清父子
Set-Location code + npm ci。
npm run test:ch06 预期 16 个文件 118 个测试。
父子日志交错出现。
task 是 allow;子 Agent 写入照样弹审批。
agent-tutorial -- run --chapter 6。
npm run test:ch06 # 预期 16 files / 118 tests
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,压根到不了审批 td> |
子 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 | 刻意的不对称:有工具但没机制 |
| # | 结论 | 出现在哪一节 |
|---|---|---|
| 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 章)都在后面。