OpenAI Codex · 多 Agent 圖

多 Agent 是一張要持久化的圖

派出去的是節點,邊一出生就是 Open。信先入隊,followup 才叫醒。關掉的是邊,歷史還在。

課程目標讀完能説清三件事:子 agent 是圖上的節點,邊只有 Open 和 Closed;send_message 只入隊,followup_task 才叫醒;關機卸運行時,關邊才從圖裏除名。第二天還能不能接着説話,先查邊。
先玩一遍 · 派生一個探索者
從 /root 派出 Hypatia,看信怎麼走、結果怎麼迴流
收尾
關機只卸運行時,邊仍是 Open。關邊才寫成 Closed。切一下再播,重啓後的名單不一樣。
根會話 /root 空閒,可以派孩子
還沒派出 等待 spawn 圖上還沒有這個節點
SQLite 邊卡
還沒有 thread_spawn_edges 記錄。
信箱與註冊表
MESSAGE FOLLOWUP RESULT
註冊表空着。恢復只沿着 Open 邊把身份貼回來。
邏輯軌跡 · 動畫每一步對應源碼裏的哪一段
  1. 計算下一層深度,寫入 ThreadSpawnregistry.rs L87
  2. 用 task_name 拼出絕對路徑 /root/explore_authmulti_agents_common.rs L117
  3. 從科學家名單抽出外號 Hypatiacontrol/spawn.rs L32
  4. 非臨時會話立刻 upsert 一條 Open 邊control.rs L776
  5. send_message 按 QueueOnly 組包,只入隊message_tool.rs L103
  6. 信箱是會話級隊列,入隊後發 Mailbox 活動input_queue.rs L127
  7. followup_task 把 trigger_turn 打開,這才叫醒handlers.rs L98
  8. 子 turn 結束,給父發 Result,不叫醒session/mod.rs L1977
  9. 關機不改邊;關邊只標目標自己的入邊 Closedlegacy.rs L6
  10. 重啓只把 Open 後代的身份裝回註冊表control/spawn.rs L158
點播放,看一個探索者怎麼長到圖上,信怎麼走,結果怎麼迴流。
圖先於運行時節點一出生就有路徑、外號和一條 Open 邊。運行時可以卸掉,邊還在,歷史還在 rollout 裏。
信和叫醒是兩件事MESSAGE 只入隊。FOLLOWUP 才開工。RESULT 飛回父信箱,預設不搶當前輪。
收尾決定明天還在不在切換上面的收尾再播一遍,看重啓後註冊表還認不認這個孩子。
教學示意:外號固定為 Hypatia,路徑固定為 /root/explore_auth,用於展示圖、信箱和邊狀態。邏輯軌跡右側行號對應 openai/codex 倉庫 commit 4f39251a01。
思路一 · 父子關係做成有狀態的邊
它解決什麼問題

你讓主會話派一個探索者去查 auth 模組。模型調用 spawn_agent,工具回了一句任務名 /root/explore_auth,外號 Hypatia。過幾分鐘點 wait_agent,信箱裏躺着一份 FINAL_ANSWER

第二天打開同一條 thread。子會話的運行時已經卸掉了。系統如果只記得昨天發生過一次調用,這條路徑就找不到。你再發 send_message,控制面會報 live agent path not found

思路是什麼

Codex 把父子做成有向邊。邊只有兩個值。Open 表示還能當打開的 spawned agent 恢復。Closed 表示從圖的視角已經關掉。序列化是 openclosed

出處:codex-rs/agent-graph-store/src/types.rs 第 4 至 12 行

圖存在 SQLite 的 thread_spawn_edges 表。child_thread_id 是主鍵,一個孩子不能掛兩個父。同一孩子再 spawn 一次,父和狀態都會被新值蓋住。會話正文不進這張表。子 agent 的模型上下文仍走自己的 rollout。圖只回答誰生了誰,這條邊現在開還是關。

出處:codex-rs/state/migrations/0021_thread_spawn_edges.sql 第 1 至 8 行

非臨時會話在綫程建出來之後立刻 upsert 一條 Open 邊。寫入失敗只打 warn。子 thread 已經在跑,圖可以稍後補。補寫用 ON CONFLICT DO NOTHING,不會把已經 Closed 的邊改回去。

出處:codex-rs/core/src/agent/control.rs 第 767 至 780 行

列後代時,過濾條件作用在走過的每一條邊上。Some(Open) 只沿着 Open 走。父邊已經 Closed 的子樹,就算孫邊仍是 Open,也不會被列出來。

出處:codex-rs/agent-graph-store/src/store.rs 第 49 至 54 行

路徑才是尋址鍵,外號是給人認的。根是 /root。相對名接到當前路徑後面。... 被拒絕,所以不能靠相對路徑爬到兄弟。角色可以收能力,不能替換父會話的權威。內置活角色是 defaultexplorerworkerexplorer.toml 是空檔案。

出處:codex-rs/core/src/agent/role.rs 第 1 至 4 行

spawn_agent 新 child thread 路徑 + 外號 upsert Open 邊 SQLite child rollout JSONL 誰生了誰,開還是關 會話正文不進邊表
一次 spawn 之後:身份進註冊表,邊進 SQLite,正文進 rollout。
為什麼長期成立

重啓之後必須能問:這個孩子還算活着嗎。只記一次 spawn 事件回答不了。Open 才能進恢復列表。Closed 從子樹遍歷裏消失。child_id 做主鍵,圖保持樹,遍歷可以按深度 BFS。換個語言重寫,最小形態仍是一張三列表:parent、child、status。

思路二 · 通信和叫醒分開
它解決什麼問題

子 agent 轉完了,要回一封 FINAL_ANSWER。如果這封信自帶叫醒,父正在寫用戶看得見的最終答案時,會被子結果強行開一輪。用戶看到半截話,再加一份突然插進來的完成通知。

思路是什麼

通信種類有四個標籤:Spawn、Message、Followup、Result。它們是 OTEL 用的標籤。協議側只有一份 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 行

send_message QueueOnly followup_task TriggerTurn 子 turn 結束 Result 父或子的信箱 先入隊,再看 trigger_turn 躺着,等下一輪 MESSAGE / RESULT 開工,maybe_start_turn 只有 FOLLOWUP 走這裏
三種信都進信箱。只有 followup 打開 trigger_turn,完成通知不搶當前輪。
為什麼長期成立

叫醒權是稀缺的。誰能開一輪,誰就不能隨便開。把投遞和開工拆開,完成通知預設不叫醒,叫醒權留給 followup_task 和用戶。換一套消息總綫也用得上這根布爾:wake 還是只入隊。

思路三 · 關邊和關機是兩件事
它解決什麼問題

V2 駐留名額滿了,會按 LRU 卸掉一個孩子。如果卸運行時順便把邊標成 Closed,這個孩子從 Open 子樹消失。下次恢復找不到它。用戶沒關過它,系統自己把它除名了。

思路是什麼

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 行

V2 恢復分兩步。先把 Open 後代的身份裝回註冊表,不重開運行時。真正有人 send_messagefollowup_task 時,才按 rollout 把 thread 掛回來。

出處:codex-rs/core/src/agent/control/spawn.rs 第 144 至 162 行

邊是 Open 關機 關邊 邊仍 Open,運行時卸掉 邊變成 Closed 恢復時身份裝回註冊表 恢復時這條邊被濾掉
卸運行時不等於從圖裏除名。只有 close 才改 status。
關掉的是邊,歷史還在。
為什麼長期成立

運行時活着和從圖上除名是兩件獨立的事。駐留 LRU、進程重啓、用戶關視窗,都可能卸運行時。只有編排者明確 close,才寫成 Closed。恢復時沿着 Open 邊走。這條分帳不依賴 Rust。

橫向對比 · 子 agent 該抽象成什麼

DSH:接縫優先,圖是列舉結果

DSH 把子 agent 做成可替換的 provider 接縫。SubagentProvidernamecapabilitiesinheritsParentContextstart。進程內 fork、Claude Code、Codex、ACP,都是同一張接口上的不同實現。

它也能列出孩子和後代,從活着的 session store 和可選的 persistence 只讀枚舉。沒有一張 thread_spawn_edges 那樣的 Open / Closed 邊表。拓撲是 session header 的 origin: subagent 加上事後折出來的。換實現便宜,按邊恢復要另做。

已核對源碼 · 2026-08-22 · packages/subagent/subagent/src/types.ts 第 285 至 295 行 · DSH · Subagent 是一個 seam

Claude Code:工具調用加 transcript 側鏈

模型面對的工具現名是 Agent。舊綫名仍叫 Task,給權限規則、hook、恢復中的會話做相容。Explore / Plan 是一次性的,父不會再續跑。

運行時給每個孩子發一個 agentId。沒有單獨的 spawn-edge 表。恢復靠讀這個 id 對應的 transcript。父要列活孩子得掃側鏈,沒有按邊過濾。Codex 多一張表、兩套狀態,換來重啓後仍能按圖説話。

已核對源碼 · 2026-08-22 · restored-src/src/tools/AgentTool/constants.ts 第 1 至 4 行
課堂練習
01

Closed 父邊下面的 Open 孫邊還在嗎

畫一棵三層樹:根到 A 為 Closed,A 到 B 為 Open。用 Some(Open)None 各列一次後代。B 會不會出現?

過濾條件作用在走過的每一條邊上。然後對照 codex-rs/agent-graph-store/src/store.rs 第 49 至 54 行的註釋,把兩種結果寫下來。

Takeaway:子 agent 是圖上的節點。邊只有 Open 和 Closed,會話正文走 rollout。send 只入隊,followup 才叫醒,完成通知不搶當前輪。關機卸運行時,關邊才從圖裏除名。第二天先查邊,再決定要不要掛回運行時。