模块:T3 解剖 OpenAI Codex:把安全观写进类型系统
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为《学 AI 产品,从入门到精通》配套解读,音频为二次演绎配音版。
派出去的是节点,边一出生就是 Open。信先入队,followup 才叫醒。关掉的是边,历史还在。
本集要回答的工程问题是:多 Agent 系统里,主会话昨天派出去的那个孩子,今天还在不在。
一个典型失败场景:
/root/explore_auth,外号 Hypatia;wait_agent,信箱里躺着一份 FINAL_ANSWER;send_message,控制面报 live agent path not found。根因不是 bug,是模型选错了:系统只记录了"昨天发生过一次派生调用"这件事,记录的是事件,不是关系。调用结束事件就归档,路径在系统眼里等于从未存在。
对玩具 demo 无关紧要;对需要跨天、跨进程、跨重启活下去的编排系统,这是核心命题。
读完本集,你应当能说清四件事:
| 能力 | 落点 | 一句话判断标准 |
|---|---|---|
| 把父子关系建成持久化图 | thread_spawn_edges 表 | 能画出谁能生谁、这条边现在是 Open 还是 Closed |
| 区分投递与唤醒 | trigger_turn 布尔 | 能判断一封信该不该打断对方当前轮次 |
| 区分关机与关边 | shutdown_live_agent / close_agent | 能解释为什么 LRU 淘汰不能顺手标 Closed |
| 按地址模型避坑 | 绝对路径寻址 | 能说清相对名为什么会发给自己孩子 |
Open(可作为打开的 spawned agent 恢复)与 Closed(从图视角已关掉),序列化为 open / closed。codex-rs/agent-graph-store/src/types.rs 第 4–12 行。thread_spawn_edges 表,child_thread_id 是主键 → 一个孩子不能挂两个父,同一孩子再 spawn 一次父和状态会被新值盖住,图保持树形。codex-rs/state/migrations/0021_thread_spawn_edges.sql 第 1–8 行。ON CONFLICT DO NOTHING,不会把已 Closed 的边改回 Open。codex-rs/core/src/agent/control.rs 第 767–780 行。Some(Open) 结果里。codex-rs/agent-graph-store/src/store.rs 第 49–54 行。InterAgentCommunication,靠 trigger_turn 区分要不要叫醒。send_message = QueueOnly,只入队;followup_task = TriggerTurn,才叫醒。空消息直接拒;followup_task 不能打根节点。codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs 第 11–24 行。trigger_turn 为假则信继续躺着;为真,或会话还有未完成的 durable sleep,才开工。V2 的完成通知是 Result,trigger_turn 为假,不抢父当前轮。codex-rs/core/src/session/handlers.rs 第 89–99 行。pending_input;子邮件 → mailbox_pending_mails。用户天然优先,不需要额外调度器。/root/worker_b);写相对名 worker_b 会接到自己路径后面,变成自己的孩子。codex-rs/core/src/session/input_queue.rs 第 76–80 行。shutdown_live_agent:关掉活着的 agent,刷 rollout,发 Shutdown,从管理器摘除 thread。边仍是 Open,下次恢复仍算活子树成员。codex-rs/core/src/agent/control/legacy.rs 第 6–8 行。close_agent:先把目标自己的入边标 Closed,再关机。后代的边不在这里被标 Closed。父 turn 正常结束走 TurnComplete,不调用 close_agent → 孩子继续跑,边保持 Open。codex-rs/core/src/agent/control/legacy.rs 第 48–58 行。send_message / followup_task 时,才按 rollout 把 thread 挂回来。省资源,重启后"只是看看有哪些孩子"不必拉起一堆运行时。codex-rs/core/src/agent/control/spawn.rs 第 144–162 行。| 方案 | 抽象方式 | 有无边表 | 恢复路径 | 取舍 |
|---|---|---|---|---|
| Codex | 图:节点 + Open/Closed 边 | 有,thread_spawn_edges | 沿 Open 边装回注册表,再按需挂运行时 | 多一张表一套状态机,换来实现成本高 |
| DSH | 接缝:SubagentProvider 接口 | 无 | session store 只读枚举 + 可选 persistence | 换实现便宜,按边恢复要另做 |
| Claude Code | 工具调用 + transcript 侧链 | 无 | 读 agentId 对应 transcript | 轻量,父列活孩子要扫侧链,无按边过滤 |
SubagentProvider 字段:name、capabilities、inheritsParentContext、start。进程内 fork、Claude Code、Codex、ACP 同为一张接口上的实现。拓扑由 session header 的 origin: subagent 事后折出。packages/subagent/subagent/src/types.ts 第 285–295 行Agent,旧线名 Task 保留以兼容权限规则、hook、恢复中的会话。Explore / Plan 是一次性的,父不再续跑。restored-src/src/tools/AgentTool/constants.ts 第 1–4 行结论:Codex 多一张表 + 一套状态机,换来"重启后仍能按图说话"。这是明确的交易,不是免费的优越。
使用这套机制时,以下边界不可越过:
child_thread_id 主键保证图永远是树,不是 DAG。若需求是"一个孩子挂多个父亲",需重新设计,不要在树上打补丁。.. 与 . 被明确拒绝,不能靠相对路径摸到兄弟节点。横向移动必须是显式的、可审计的绝对路径。explorer.toml 是空文件——能力差异不在提示词里。一旦子 agent 能反向决定父权限,权限模型即失效。在 review 一个多 Agent 编排实现时,逐条对照:
.. 与 .?兄弟互发是否强制绝对路径?最常见的错误顺序是一上来就写"怎么起子进程、怎么灌上下文",写完才发现重启后找不到孩子。正确顺序反过来:先定张贴持久化的边表(最小形态三列:parent、child、status),再定运行时。运行时可以重写,边表一旦没设计好,后面所有持久化都要返工。
不必照搬 Codex 的四标签,但一定要在协议里留一个"要不要唤醒"的字段,并让它的默认值是 false。这样完成通知、心跳、日志回流天然不打断;需要打断时由调用方显式置 true。将来换消息总线,这根布尔仍然成立。
一个接口只卸运行时(刷状态、发通知、从管理器摘除),另一个接口才改图上的状态。命名上也要区分清楚,避免后来者把两个语义混用一个动词。评审时可以把这一条当作硬门槛:凡是淘汰路径里出现写 Closed 的代码,一律打回。
列后代时最容易偷懒的地方是"只检查第一条边"。正确写法是在遍历过程中对走过的每条边套同一个过滤谓词。可以在单元测试里构造一棵三层树:根到 A 为 Closed,A 到 B 为 Open,分别用带过滤和不带过滤两种方式列后代,断言 B 在前者里不出现。
在代码规范里直接规定:跨 agent 寻址只允许绝对路径,相对名保留给"派生自己的孩子"这一种语义。这样即使有人写错,结果也是可预期的(发给了自己孩子),而不是不可预期的越权访问。
Some(Open) 与 None 各列一次后代,B 会不会出现?对照 store.rs 第 49–54 行的注释写下两种结果。子 agent 是图上的节点。边只有 Open 和 Closed,会话正文走 rollout。send 只入队,followup 才叫醒,完成通知不抢当前轮。关机卸运行时,关边才从图里除名。第二天先查边,再决定要不要挂回运行时。
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页正文为二次演绎配音版的配套解读,与音频逐段对应。