Codex Subagent 逆向笔记:从 spawn_agent 到父子线程协作
Codex 的 subagent 看起来像“主 Agent 派出几个小 Agent 并行工作”。从实现证据看,更准确的模型是:父 Agent 通过协作工具请求创建一个有独立线程身份的子 Agent;共享的 Agent 控制器负责登记、调度、通信和回收父子关系;客户端再把这些状态呈现成活动列表。
本文结合本机 ChatGPT 桌面应用内置 codex-cli
的静态分析、应用资源中的 UI JavaScript,以及 OpenAI codex
仓库与本机版本号一致的源码 tag
交叉整理。版本号相同提高了源码对照的可信度,但没有签名构建清单证明本机二进制确由该
commit 构建;因此我仍把本机观察和源码解释分开说明。
先看整条链路
父 Agent 的模型选择 spawn_agent |
上图中从工具处理器到子线程创建、持久化关系和首条输入的步骤,来自版本 tag 源码;本机二进制中也能找到相应符号和校验字符串,但静态反汇编没有独立证明所有异步分支的具体运行顺序。
1. 工具调用不是直接启动一个 shell 子进程
该版本源码中的 V2 spawn handler 会把模型给出的 agent
描述整理成
SpawnRequest,其中包含发起者、配置、初始消息、父子关系及
fork 选项,然后交给
agent_control.spawn(...)。初始消息被指定为触发 child
新一轮执行的输入。可对照 V2
spawn handler。
控制器的 spawn_agent_internal 随后会:
- 取得 agent-tree membership,并确定 parent thread;
- 计算适用的 multi-agent 版本并检查执行容量;
- 向 registry 预留 slot;
- 解析继承的 environment、插件设置和执行策略;
- 生成
SessionSource::SubAgent(ThreadSpawn { ... })及 AgentMetadata; - 按需准备 fork history,再调用
ThreadManager创建 child thread; - 持久化 thread-spawn edge,之后提交首条输入。
这条路径表明 Codex 的抽象中心是 thread/session 与 agent tree。被分析的调用路径没有显示“每个 subagent 再启动一份独立 codex 可执行文件”的证据;但仅凭这段静态证据,也不应把它扩大成对所有平台、所有运行模式的绝对断言。相关实现见 AgentControl spawn 流程。
2. 子线程有独立身份,也保留父子来源关系
child 并非只是在父会话里追加一段带标签的对话。它有自己的 thread id、session source 和 agent path;source 元数据记录父 thread、层级和路径。控制器还会把 spawn edge 持久化,供后续恢复和父子关系查找使用。
“有独立线程身份”不等于“完全隔离”。创建时仍可能从父级继承环境选择、插件禁用列表、执行策略和动态工具;如果请求指定 fork mode,还会准备历史记录。公开源码注释特别指出:复制对话历史不应改变发起 spawn 的那一轮原有能力。具体继承项取决于请求、配置及当前 feature,不能把它简化成“所有上下文都复制”或“子 Agent 从零开始”。
在新 thread 建好并建立关系后,控制器才提交初始输入;代码分别处理普通 user input 和 inter-agent communication。V2 下还会解析模型信息等启动配置。此后 child 作为独立执行单元继续跑自己的 agent loop。
3. 并行受容量与登记机制控制
spawn 不只是创建 thread。控制器会先登记 membership、检查 execution
capacity,并通过 registry 预留 spawn slot;V2 resident 模式还有对应的
residency slot。成功后才 commit 这些登记。源码也有
PendingSpawn,在创建尚未完成时暂时持有 agent-tree
membership,避免一个“待完成 spawn”失去树上的归属。
本机二进制字符串里出现了
max_concurrent_threads_per_session、min_wait_timeout_ms、max_wait_timeout_ms
等配置名,以及“pending spawn must own agent-tree
membership”一类错误文本。这些只能证明该构建包含相关配置与防护逻辑;没有检查运行时最终配置,不能据此给出具体并发上限。
4. wait_agent
等待的是活动,不是 join
当前 V2 wait_agent handler 先应用配置里的 timeout
下限、上限和默认值,然后订阅 session input queue 的
activity。等待结果分为三类:收到 mailbox activity、被新输入
steer、达到超时。它返回的状态也明确对应这三种情况。
因此它不是传统线程库里的 join(child):它不只是盯着 child
是否退出,也不保证“所有 child
都完成后才返回”。一条协作消息就可能唤醒等待;父 Agent
随后可以读取结果、继续等、给 child 发
follow-up,或自行整合现有进度。具体实现见 V2
wait handler。
本机二进制中还能找到
spawn、wait、list_agents、send_message、followup_task、interrupt_agent
等 V2 handler 名称,也能找到 legacy agent handler
符号。它们说明构建中存在这些操作面;不同版本、feature flag
和产品界面暴露哪些工具,需要另外确认。
5. 完成通知按 agent 版本分流
与本机 CLI 版本号对应的公开 tag
源码中,control.rs::maybe_start_completion_watcher 是
legacy 路径。它订阅 child
状态直到终态;订阅流结束或失败时会再查询一次状态。终态出来后,它会把结果送回
parent:带 V2 agent path 的情况使用 inter-agent completion message 并设
trigger_turn=false,legacy 情况则注入不启动新 turn 的
SubagentNotification。树正在关闭时的 shutdown
通知会被抑制。更关键的是,spawn.rs 调用点明确只在
multi_agent_version != V2 时启动这个 watcher,因此不能把
watcher 当作 V2 child 的通用完成机制。见 watcher
实现 和 spawn
调用点。
同一版本 tag 中,V2 的调用链是另一条:child session 发出
TurnComplete 或 TurnAborted
时,Session::send_event 会进入
maybe_notify_parent_of_terminal_turn;该方法先要求版本为
V2、来源是带 agent path 的
ThreadSpawn,再从终态事件和已记录错误构造
AgentTurnOutcome,交给
AgentControl::turn_finished。控制器随后由
completion.rs::notify_parent_of_terminal_turn
路由结果。成功完成且有 parent turn id 时会发
SubAgentActivityKind::Completed,之后按终态构造结果消息并排入
parent 通信队列,trigger_turn 为 false。错误终态使用相应的
completion 文案,部分拒绝错误有专门处理。见 Session
终态事件调用链 和 terminal
outcome 路由实现。
这两套实现都说明:状态/活动事件与结果消息不是同一个事件,而且排入
parent 队列不等于 parent 模型已经消费并整合了答案。客户端另外有一个针对
/flora/subagent/thread/turns
的查询模块,会在有可用记录时选择 child 的最新 final
answer;那属于另一条结果展示/读取路径。尽管版本号相同,公开源码 tag
与本机安装包之间仍缺少构建来源证明;以上版本分流描述的是被检查的 tag
源码,并由本机符号/字符串作静态交叉佐证。
6. UI 显示的是协作事件的投影
应用 JavaScript bundle 中观察到两类相关结构:
| 结构 | 能看到的字段 | UI 侧用途 |
|---|---|---|
codex_collab_agent_tool_call |
tool、status、sender/receiver thread ids、prompt、agentsStates | 表示 spawn、send input、resume、wait、close 等协作工具调用及其状态 |
codex_sub_agent_activity |
agentPath、agentThreadId、kind、id |
表示 started、interacted、completed、interrupted 等子 Agent 活动 |
bundle 把活动状态映射成 waiting、working、done、failed,并分组放进
Active / Done。换言之,界面列表是控制层事件的呈现结果,不是 Agent
调度本身;UI 的 completed
也不等于上文所说的结果消息必然已被主模型消费。
7. 本机版本与官方源码 tag 对照
本机 CLI 版本是 0.162.0-alpha.17.2;OpenAI
仓库中存在同名 tag rust-v0.162.0-alpha.17.2,其 peeled
commit 为 740e5af33c71225640e0c1c1555c514c2c93ab74。我对照了该
tag 下的 V2 spawn
handler、AgentControl::spawn_agent_internal、session
terminal-event 路由、turn_finished completion delivery 与
legacy completion watcher。源码中可直接看到:只有非 V2 spawn 会启动
watcher;V2 child 的终态由 session 事件路径转交控制器。
这一步把原先“当前 main
的实现可能与本机版本不同”的不确定性缩小为“同版本号源码 tag
可用于解释本机构建,但构建来源尚未被签名元数据证明”。本机 SHA-256 为
0d5ac7aae20c4eda6242428d5e218c55ba96d8763fda2a271088ff37104b9d28,Mach-O
UUID 为
2CE1483B-FF35-344F-9958-9D48FFE5D5ED;这些身份值和符号可用只读脚本复核。
本机直接观察
- ChatGPT.app 内嵌 CLI 的
--version输出为codex-cli 0.162.0-alpha.17.2,目标是 arm64 Mach-O。 - 该二进制
SHA-256:
0d5ac7aae20c4eda6242428d5e218c55ba96d8763fda2a271088ff37104b9d28。 - 符号表包含
LocalAgentControl::prepare_thread_spawn、maybe_start_completion_watcher、validate_loaded_v2_child、persist_thread_spawn_edge_for_source、restore_v2_agent_metadata、resume_agent_from_rollout等名字。 - 只读反汇编确认本机 watcher 入口会调用
LocalAgentRuntime::admit_start;本机符号表没有独立的notify_parent_of_terminal_turn主函数符号,但存在该函数闭包的 callsite metadata。由于优化/内联可能改变符号形态,这不能证明本机缺少该逻辑,也不能证明它被调用。 - 二进制字符串含有多 agent V2 配置字段、工具错误文本及
codex.multi_agent.result_delivery等指标名。 - 针对应用 bundle 的两个子 Agent UI 模块,可见协作工具/活动 schema 和 final-answer 查询逻辑。该分析只覆盖被提取的目标资源,不代表遍历了整个桌面应用。
- 后续用 REA 对本机
app.asar做了全包静态扫描:分析器完成了 16,240 个源文件的解析并重建应用图。ASAR 声明的permissions.node哈希与伴随文件实测哈希不一致,因此采用record-and-continue并把该原生文件标记为不可信;大型启动 bundle 的语义图受资源上限影响,保留了静态和模块事实。这个全包结果没有提供可用的子 Agent 定向调用链,本文 UI 结论仍限定在上面明确检查的模块。
以上均是静态文件观察,不是运行时抓包或真实 spawn 实验。特别是
maybe_start_completion_watcher
这个符号只能证明二进制中存在该方法,不能单靠符号名推出 watcher
的精确触发条件。
匹配版本的公开源码交叉核对
上文的精确处理顺序来自 openai/codex 的版本 tag
rust-v0.162.0-alpha.17.2,peeled commit 为 740e5af33c71225640e0c1c1555c514c2c93ab74。版本号匹配加上本机符号/字符串交叉检查,使该
tag 成为高可信源码参照;但没有签名构建元数据证明本机二进制确由该 commit
产生,所以它仍不能替代逐指令语义证明。官方多
Agent 指南也可帮助理解用户可见的上下文隔离与 parent coordination
心智模型,但不是桌面版内部实现规格。
仍未知
- 本机这一构建对应的精确 Codex 源码 commit;同版本 tag 是强参照,但构建来源尚未被证明。
- 运行时实际启用的 multi-agent 版本、feature flag、并发/等待配置和权限策略。
maybe_start_completion_watcher在本机版本中创建哪些具体 watcher、取消和恢复条件是什么;也未从本机反汇编恢复 V2 terminal-event 到 parent 通知的完整调用链。- 不同桌面端版本的 UI bundle、服务端
/flora/...接口和远端 executor 的实现细节。 - 子 Agent 在真实任务中的调度时序、资源成本和失败率;这次没有执行受控实验。
小结
目前证据支持一个更精确的图景:spawn 是经共享
AgentControl 进入统一的线程与 Agent tree 管理流程;child
有独立 thread/session 标识,同时按策略继承设置或历史;spawn
关系先被持久化,再投递首条消息;legacy completion watcher 与 V2
terminal-event forwarding 走不同路径,父子通信、等待、终态消息和 UI
活动事件是彼此相关但不同的通道。
最重要的实现细节不是“同时开几个模型”,而是如何可靠地登记子节点、约束容量、建立可恢复的父子边,并让协作消息与界面活动保持一致。下一步若要把逆向再推进,需要把本机二进制的 watcher 异步状态机和公开源码逐段对齐,或在明确授权的本机 Codex 会话中做可重复的运行时观察;可复核的采集脚本与证据台账已整理在 Codex Subagent Reverse Engineering 仓库。目前这篇笔记没有把静态分析写成运行时结论。