Pi 明确不做子 agent("用 tmux")。dsh 做了四种委派机制。Codex 做了两代,第二代把主 agent 的角色从"干活的"改成"协调的"——并且把这个转变直接写进了系统提示词。本章拆这个演进。
[源码 protocol/src/protocol.rs:2844]
三态,且版本被持久化在 rollout 的会话元数据里(第 12 章的 RolloutParams::Create.multi_agent_version)。恢复一个 V1 时代的会话,它还是按 V1 跑。
两代的工具集不一样 [源码 core/src/tools/handlers/]:
| V1 (multi_agents/) | V2 (multi_agents_v2/) |
|---|---|
| spawn | spawn |
| wait | wait |
| send_input | send_message |
| close_agent | interrupt_agent |
| resume_agent | list_agents |
| —— | followup_task |
| —— | message_tool(共享分发) |
差异不大,但方向很清楚:
message_tool.rs 的注释点出了 V2 的一个实现细节 [源码]:
Shared argument parsing and dispatch for the v2 agent messaging tools. send_message and followup_task share the same submission path and differ only in whether the …
(v2 agent 消息工具的共享参数解析与分发。send_message 和 followup_task 走同一条提交路径,只在……上有区别。)
"发个消息"和"派个后续任务"底层是同一件事,区别只在语义标记上。这个抽象是对的——对一个 agent 来说,收到消息和收到任务本来就没有本质区别。
Codex 仓库里有一份 core/templates/agents/orchestrator.md(4972 字节)。它的最后一节直接规定了主 agent 的行为 [源码]:
General guidelines
- Prefer multiple sub-agents to parallelize your work. Time is a constraint so parallelism resolve the task faster.
- If sub-agents are running, wait for them before yielding, unless the user asks an explicit question.
- If the user asks a question, answer it first, then continue coordinating sub-agents.
- When you ask sub-agent to do the work for you, your only role becomes to coordinate them. Do not perform the actual work while they are working.
- When you have plan with multiple step, process them in parallel by spawning one agent per step when this is possible.
(优先用多个子 agent 并行化你的工作……如果子 agent 在跑,在交还控制权前先等它们,除非用户提了明确问题……当你让子 agent 替你干活时,你唯一的角色就变成协调它们。不要在它们工作时自己动手。……有多步计划时,尽可能每步派一个 agent 并行处理。)
"Do not perform the actual work while they are working" 是本章最值得记的一句。
它解决的是多 agent 系统里一个非常具体的失败模式:主 agent 派完活之后闲不住,自己又去干了一遍。结果是重复劳动、文件冲突、上下文爆炸。用一句提示词把主 agent 的角色钉死,比任何架构约束都直接。
可迁移的判断 ㉕ 多 agent 系统里,"主 agent 派活之后该干什么"必须被显式规定,否则它会两边都做。
Codex 的规定是三条:只协调不干活、交还控制权前先等、用户提问优先回答再继续协调。这三条可以直接抄进任何 orchestrator 提示词。
对本仓的启示:本仓的 subagent 使用(Explore / general-purpose)目前是"派出去、等结果、自己接着做"。如果要做真正的并行编排,这三条约束是起点。
第 3 章讲过那条约束:一个 Session 同时只能跑一个 Task。所以并行的唯一办法是多个 Session。
AgentControl(862 行)+ AgentRegistry(406 行)就是管这个的。看 control.rs 的 import 列表就知道 spawn 一个 agent 涉及多少东西 [源码 core/src/agent/control.rs:1-56]:
子 agent 拿到的是父会话历史的一个截断分叉(truncate_rollout_to_last_n_fork_turns),不是完整历史——第 12 章讲的分叉机制在这里被复用。
ThreadHistoryMode 的三种模式(Legacy / Paginated / …)和 subagent_history_start_ordinal 也是为这个服务的:子 agent 从父会话历史的哪一条开始看。
这个结构体的文档注释很直白 [源码 core/src/agent/registry.rs:18]:
This structure is used to add some limits on the multi-agent capabilities for Codex. In the current implementation, it limits:
- Total number of sub-agents (i.e. threads) per user session
This structure is shared by all agents in the same user session.
一个用户会话内的子 agent 总数有上限,且这个计数被所有 agent 共享。
为什么必须有?因为提示词里刚说了"尽可能每步派一个 agent"。没有上限的话,一个"重构整个项目"的任务能瞬间派出几十个 agent,每个都在跑模型、都在写文件。
注意 total_count 是 AtomicUsize 且只增不减——它记的是"这个会话总共派过多少个",不是"现在有几个活的"。限制的是累计量,防止"关一个开一个"绕过限制。
used_agent_nicknames: HashSet<String> + nickname_reset_count + rand::prelude::IndexedRandom——子 agent 有随机分配的昵称,且不重复。
这纯粹是给人用的:UI 里显示 "agent-falcon 正在跑测试" 比 "thread-01H8X…" 好懂太多。用完一轮再复用(nickname_reset_count)。
agent_tree: HashMap<String, AgentMetadata> 里的 key 是路径字符串,配合 codex_protocol::AgentPath 类型。agent 是一棵树(子 agent 还能派孙 agent),路径编码了它在树里的位置。
agent/role.rs 的模块注释是本章第二个关键点 [源码 core/src/agent/role.rs:1]:
Applies bounded agent-role overrides to an existing session config.
Roles may customize the child or reduce its capabilities, but never replace the parent session's authority. A projected layer keeps existing layer-based consumers in sync.
(对已有会话配置施加有界的 agent 角色覆盖。角色可以定制子 agent 或削减它的能力,但绝不能替换父会话的权限。)
可覆盖的字段 [源码 core/src/agent/role.rs:37]:
"只能收缩不能扩张"是多 agent 安全的核心不变量。
反例很好想:如果子 agent 的角色配置能把沙箱策略从 read-only 改成 danger-full-access,那攻击者只要能影响角色配置(比如通过一个仓库里的 .codex/agents/*.toml),就能用"派一个子 agent"绕过所有权限限制。
角色配置从文件读取时还用了 read_sensitive_file_to_string [源码 core/src/agent/role.rs:17]——角色文件被当作敏感文件读(走执行器抽象、受沙箱管辖)。
DEFAULT_ROLE_NAME = "default",以及一个专门的错误文案 "agent type is currently not available"——角色可以被禁用。
可迁移的判断 ㉖ 委派机制的权限必须是单调收缩的:子任务的权限 ⊆ 父任务的权限,无例外。
这条听起来显然,但很容易被"配置覆盖"这类机制悄悄打破。判断方法:问自己"如果这份配置来自一个不可信的仓库,最坏会怎样"。
agent_communication.rs + InterAgentCommunication(第 12 章看到它是一种被持久化的 rollout 条目)+ AgentCommunicationKind。
几个相关的上下文片段(第 6 章那 44 个里的):
| 片段 | 干什么 |
|---|---|
| inter_agent_message | agent 之间的消息 |
| inter_agent_completion_message | 子 agent 完成的通知 |
| subagent_notification | 子 agent 状态通知 |
| multi_agent_mode_instructions | 多 agent 模式说明 |
| multi_agent_role_instructions | 角色说明 |
| multi_agent_usage_hint | 使用提示 |
六个片段专门服务多 agent。 这说明通信不是"子 agent 返回一个字符串"那么简单——父子之间要同步状态、要通知完成、要传递角色语义。
session_prefix.rs 里的两个格式化函数 [源码 core/src/agent/control.rs:20-21]:
子 agent 的上下文里有一行前缀,说明"你是谁、你为谁工作"。
一个只有 479 行的小 crate,但它的文档注释说明了架构意图 [源码 agent-graph-store/src/lib.rs:1]:
Storage-neutral parent/child topology for thread-spawned agents.
父子拓扑被抽象成一个独立的、存储中立的 store,有 trait 和本地实现。
"storage-neutral"(存储中立)意味着将来可以有远程实现——agent 树可以跨机器。配合第 9 章的远程执行环境和第 3 章的 WebSocket 传输,这条线的方向很清楚:分布式的 agent 编排。
ThreadSpawnEdgeStatus 是"边"的状态——这是一张真正的图,不只是父子指针。
agent-identity crate(1000 行)的 import 列表很扎眼 [源码 agent-identity/src/lib.rs:1-25]:
Ed25519 签名密钥 + Curve25519 加密密钥 + JWT。
这不是"给子 agent 起个名字"级别的身份,这是密码学身份:agent 有自己的密钥对,能签名,能被验证,能拿到 JWT。
配合 workload-identity(870 行)这个 crate 名——这是云原生的工作负载身份概念。
意味着:一个 agent 可以向外部服务证明"我是某个用户会话下的某个子 agent",而不必共享用户的凭据。第 5 章的 ModelClient 里那个 agent_identity_policy: AgentIdentityAuthPolicy 字段就是这条线的落点。
这是最小权限原则在 agent 编排上的应用。 也是本章看到的、Codex 走得最远的一步——它在为"agent 作为独立的安全主体"做准备。
dsh 教程第 11 章讲了四种:spawn(子 agent)、fork(分叉)、workflow(编排)、ralph(循环)。
| 能力 | dsh | Codex |
|---|---|---|
| 子 agent | spawn | spawn_agent(V1/V2) |
| 分叉 | fork | thread/fork(第 12 章,协议级) |
| 编排 | workflow 插件 | 提示词层(orchestrator.md) |
| 循环续跑 | ralph | Stop 钩子(第 4/13 章)+ goal 扩展 |
| 数量上限 | 配置 | AgentRegistry 硬限 |
| 权限模型 | seam 可换 | 单调收缩,不可放宽 |
| 拓扑存储 | —— | 独立的 graph store |
| 密码学身份 | —— | Ed25519 + JWT |
最大的差别在"编排"这一格:dsh 把它做成一个可配置的 workflow 插件(声明式的图),Codex 把它做成提示词层的行为规范(让模型自己决定派几个、怎么派)。
两种做法的取舍很清楚:
Codex 选了后者,然后用 AgentRegistry 的硬上限兜住最坏情况。这个组合(自由的策略 + 硬性的资源上限)在本教程里已经出现了第三次——第 5 章的重试上限、第 11 章的 Guardian 熔断器、这里的 agent 计数。
下一章收尾:把 Pi、dsh、Codex 三种哲学摆在一起,看能带走什么。