D5|Subagents:不自动乱 spawn,回传摘要而不是整段 transcript
先别把协作理解成「模型一忙就开分身」
读完 D1–D4,下一件错事是打开 multi_agent 就以为 Codex 会自己把任务拆成一树子进程。然后 TUI 里冒出一堆 explorer,额度被吃光,两个 worker 抢同一份工作区。
判断先说清楚:子代理是显式工具调用开出来的另一条 Thread。 默认协作模式是 ExplicitRequestOnly:用户或 AGENTS.md/Skill 没有点名要求委托,就不要 spawn(multi_agent_mode_instructions.rs:7, 48-50)。子代理从当前 turn 的有效配置出发,继承审批、沙箱、cwd、provider,再叠一层 只能减不能加 的 role。结果用 wait_agent / mailbox 回摘要,不是把子 thread 的 rollout 全贴回父 prompt。
C2 的记忆管道会跳过非 root;C5 的并行 Goal 要 worktree。子代理同样:并行写必须隔离,explorer 才鼓励并行只读。
1. 不自动乱 spawn:模式、工具、深度
Feature::Collab 键是 multi_agent,默认开(features/src/lib.rs:1267-1270)。开的是 协作工具在不在 ToolRouter 里,不是「允许模型自行开分身」,也不是 G5 的 Default/Plan 说明书。默认 MultiAgentMode::ExplicitRequestOnly(app-server thread_state.rs:291)。Proactive 才允许「能并行就委派」(multi_agent_mode_instructions.rs:8)。这是 fragment,不是沙箱旋钮——模型仍可能违抗,但产品默认文案是禁止主动拆。
工具面在 multi_agents handler(模块注释,handlers/multi_agents.rs:1-6):
| 工具 | 做什么 |
|---|---|
spawn_agent | 开子 Thread |
wait_agent | 等完成/超时,收回状态 |
send_input | 给已有子代理续话(steer) |
close_agent / resume_agent | 收掉 / 再拉起 |
spawn_agent 在深度超限时直接回模型:「Agent depth limit reached. Solve the task yourself.」(spawn.rs:68-73)。默认 agent_max_depth = 1(config/mod.rs:250):子代理默认 不能再 spawn 孙子。AgentRegistry 还限制 同一用户 session 下子 thread 总数(registry.rs:18-23),reserve_spawn_slot 触顶返回 AgentLimitReached。V2 配置把并发槽位数写进 usage hint:「包括你自己在内最多 N 个同时活着」(session/multi_agents.rs:122-129)。max_concurrent_threads_per_session 不能配 0(config/mod.rs:3788-3792)。
没有「显式 + 深度/并发帽」会怎样?一次 Regular turn 递归开树,每层都打模型,记忆管道还会误抽子代理独白(C2 已跳过 subagent,就是怕这个)。
2. 三个内置角色:default / explorer / worker
省略 agent_type 时用 "default"(role.rs:32-33, 348-353):描述就是「Default agent.」,没有额外 toml。
explorer: 给具体、范围清楚的代码库问题;快、权威;鼓励 多个独立问题并行开多个 explorer;相关问题复用已有 explorer(:356-367)。有内嵌 explorer.toml(文件当前几乎是空壳,角色主要靠这段 description)。
worker: 执行和生产:实现一块功能、修测试、把大重构切成独立块。必须划清 文件所有权,并告知「你不是仓库里唯一的人,别回滚别人的改动」(:370-382)。没有独立 config_file。
awaiter 已从内置表拿掉,只留注释(:384-401)。
Role 不得扩大父 session 的权力(role.rs:1-4)。叠上去的是 developer instructions、模型、effort、personality、以及 关掉 某些 feature(ShellTool / Apps / Plugins / Memories…),技能配置也只保留「禁用」项(:91-117)。不能靠 explorer.toml 把子代理拧成 DangerFullAccess。审批和沙箱从父 turn 继承(handler 模块注释)。测试钉死:spawn explorer 时 approval_policy 保持父级(multi_agents_tests.rs:284)。
自定义角色:各配置层的 config_folder/agents/*.toml(agent-roles/src/loader.rs:76-79, 290-298),也就是用户侧常见的 $CODEX_HOME/agents/,以及项目 .codex/agents/(仍受未信任闸:项目层 disabled 则那层 agents 目录不会作为有效角色源)。[agents.roles] 可声明 config_file。同名冲突打 startup warning。
没有「角色只能减权」会怎样?一个 repo 里的 hacker.toml 给子代理关审批、开全盘写,父 session 的 B3 旋钮被绕开。
3. 回传摘要,不要整段历史
子代理是完整 Thread:自己的 run_turn、自己的 rollout、SessionSource::SubAgent。父模型 不把子 rollout 当 messages 拼接。
收回来的路径:
wait_agent: 按 thread id 收集AgentStatus,可超时(wait.rs:191-204)。给父模型的是 JSON 状态,不是子对话全文。- Mailbox: B1 的
InterAgentCommunication。子代理写完一封信,父空闲时maybe_start_turn_for_pending_work。信是有界消息,不是 dump。 send_input: 往已有子代理排队续话(send_input.rs:17-20),等于 A2 的 steer,对象换成子 Thread。
父 prompt 里还有协作 usage hint(并发槽、何时 wait)。这是说明书,真正的硬帽在 Registry。
Explorer 并行读、worker 并行写:后者必须像 C5 一样配 worktree,否则所有权规则只是 prompt。隔离单位仍是 checkout,不是又开一套沙箱政策。
4. V2 是并行架构,不是 V1 spawn_agent 换皮
上一节的工具表是 V1。Feature::MultiAgentV2 键 multi_agent_v2,阶段 Stable,默认关(features/src/lib.rs:1273-1276)。打开之后,Turn 上的 multi_agent_version 变成 V2,广告出去的不再是同一套名字。
spec_plan.rs 按版本分叉(:644-686):
| V1 | V2 | |
|---|---|---|
| 开子 thread | spawn_agent | 仍叫 spawn_agent,handler 换成 multi_agents_v2/spawn.rs(create_spawn_agent_tool_v2) |
| 续话 | send_input | send_message / followup_task(message_tool.rs:1-4:同一提交路径,差在要不要立刻 wake) |
| 等 / 收 | wait_agent close_agent resume_agent | wait_agent 可关;另有 interrupt_agent、list_agents |
| 拓扑 | Registry 计数 | agent-graph-store/ 存 parent/child 边(文件头:storage-neutral topology) |
| 身份 | 角色 toml 减权 | 另有 agent-identity/,给托管身份,不是又一份 spawn |
V1 的 mailbox 还在。V2 把「给子代理写信」收成独立工具,并且可以挂自己的 tool namespace(测试里配成 "agents")。子代理能不能再看见 V2 工具,还要看模型自己声明的 multi_agent_version(collab_tools_enabled 的 V2 臂,:655-658)。
判断:V2 是另一套协议。 图、消息工具、可选 namespace,不是给 V1 换皮肤。读者默认抄 V1 的显式 + 减权 + 摘要。打开 V2 之前,先当「并行架构」,不要假设 send_input 还在。
没有这层对照会怎样?你在默认关的仓库里找 send_message 找不到,以为 D5 写错了;或者打开 flag 之后继续用 V1 的 mailbox 语义,图上的边和模型看见的工具对不上。
5. /agents 查看,steer 走工具不是新的 Op
TUI:
/agents:所有活着的 agent session,可切换(slash_command.rs:132)/subagents(SlashCommand::MultiAgents):当前 session 的子代理(:133)
这是投影。模型侧 V1 用 send_input,V2 用 send_message;人在 TUI 里切过去,打字进的是那条子 Thread 的 Op::TurnInput,同一条 SQ/EQ(B1)。不要给子代理再发明一套协议。
Guardian 评审员、Memories Phase 2 整合工人也是 Session,但会关 Collab / MemoryTool,避免递归(C2、B3)。它们不是用户树上的 worker。
6. 和 Goal、Skill、Hook 怎么选
| 要做的事 | 用 |
|---|---|
| 一条长任务、一个完成条件 | /goal(C5),不必开子代理 |
| 多个独立只读问题 | 并行 explorer |
| 多块可划分所有权的实现 | worker + worktree |
| 可复用的办事程序 | Skill(D1),不是新角色 |
| 每次工具前机械拦截 | Hook(D4) |
| 子代理出生/结束时跑脚本 | SubagentStart / SubagentStop |
Skill 禁止把读 SKILL.md 交给子代理(D1)。说明书必须由主 agent 读完再干活。
7. 结语:带走一句话
子代理是显式工具开出的另一条 Thread:默认抄 V1 的不主动拆、深度 1、角色只能减权、收回摘要不是全文;V2 是默认关掉的并行协议,打开之前不要当 V1 换皮。