多 Agent 是一張要持久化的圖
派出去的是節點,邊一出生就是 Open。信先入隊,followup 才叫醒。關掉的是邊,歷史還在。
send_message 只入隊,followup_task 才叫醒;關機卸運行時,關邊才從圖裏除名。第二天還能不能接着説話,先查邊。
thread_spawn_edges 記錄。- 計算下一層深度,寫入 ThreadSpawnregistry.rs L87
- 用 task_name 拼出絕對路徑 /root/explore_authmulti_agents_common.rs L117
- 從科學家名單抽出外號 Hypatiacontrol/spawn.rs L32
- 非臨時會話立刻 upsert 一條 Open 邊control.rs L776
- send_message 按 QueueOnly 組包,只入隊message_tool.rs L103
- 信箱是會話級隊列,入隊後發 Mailbox 活動input_queue.rs L127
- followup_task 把 trigger_turn 打開,這才叫醒handlers.rs L98
- 子 turn 結束,給父發 Result,不叫醒session/mod.rs L1977
- 關機不改邊;關邊只標目標自己的入邊 Closedlegacy.rs L6
- 重啓只把 Open 後代的身份裝回註冊表control/spawn.rs L158
你讓主會話派一個探索者去查 auth 模組。模型調用 spawn_agent,工具回了一句任務名 /root/explore_auth,外號 Hypatia。過幾分鐘點 wait_agent,信箱裏躺着一份 FINAL_ANSWER。
第二天打開同一條 thread。子會話的運行時已經卸掉了。系統如果只記得昨天發生過一次調用,這條路徑就找不到。你再發 send_message,控制面會報 live agent path not found。
Codex 把父子做成有向邊。邊只有兩個值。Open 表示還能當打開的 spawned agent 恢復。Closed 表示從圖的視角已經關掉。序列化是 open 和 closed。
出處: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。相對名接到當前路徑後面。.. 和 . 被拒絕,所以不能靠相對路徑爬到兄弟。角色可以收能力,不能替換父會話的權威。內置活角色是 default、explorer、worker。explorer.toml 是空檔案。
出處:codex-rs/core/src/agent/role.rs 第 1 至 4 行
重啓之後必須能問:這個孩子還算活着嗎。只記一次 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 行
叫醒權是稀缺的。誰能開一輪,誰就不能隨便開。把投遞和開工拆開,完成通知預設不叫醒,叫醒權留給 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_message 或 followup_task 時,才按 rollout 把 thread 掛回來。
出處:codex-rs/core/src/agent/control/spawn.rs 第 144 至 162 行
運行時活着和從圖上除名是兩件獨立的事。駐留 LRU、進程重啓、用戶關視窗,都可能卸運行時。只有編排者明確 close,才寫成 Closed。恢復時沿着 Open 邊走。這條分帳不依賴 Rust。
DSH:接縫優先,圖是列舉結果
DSH 把子 agent 做成可替換的 provider 接縫。SubagentProvider 帶 name、capabilities、inheritsParentContext、start。進程內 fork、Claude Code、Codex、ACP,都是同一張接口上的不同實現。
它也能列出孩子和後代,從活着的 session store 和可選的 persistence 只讀枚舉。沒有一張 thread_spawn_edges 那樣的 Open / Closed 邊表。拓撲是 session header 的 origin: subagent 加上事後折出來的。換實現便宜,按邊恢復要另做。
Claude Code:工具調用加 transcript 側鏈
模型面對的工具現名是 Agent。舊綫名仍叫 Task,給權限規則、hook、恢復中的會話做相容。Explore / Plan 是一次性的,父不會再續跑。
運行時給每個孩子發一個 agentId。沒有單獨的 spawn-edge 表。恢復靠讀這個 id 對應的 transcript。父要列活孩子得掃側鏈,沒有按邊過濾。Codex 多一張表、兩套狀態,換來重啓後仍能按圖説話。
Closed 父邊下面的 Open 孫邊還在嗎
畫一棵三層樹:根到 A 為 Closed,A 到 B 為 Open。用 Some(Open) 和 None 各列一次後代。B 會不會出現?
過濾條件作用在走過的每一條邊上。然後對照 codex-rs/agent-graph-store/src/store.rs 第 49 至 54 行的註釋,把兩種結果寫下來。