多 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 行的注释,把两种结果写下来。