可以独立运行——bounded 默认模式就是为"纯库、无 web"设计的;而"run loop 如何感知"的答案恰恰是:loop 完全不感知,这是设计的核心。分两部分说:

1. 独立运行的完整时间线(一个普通脚本)

agent = Agent(name="assistant", model="glm-5.2",
              plugins=[Subagents([researcher])])

result = await Runner.run(agent, "调研 X 与 Y,然后写对比结论")   # 就这一个调用
turn 1  模型:spawn_subagent("researcher", "调研 X") + spawn_subagent(..., "调研 Y")
        → 两个工具立即返回 "t1/t2 已启动";children 以 asyncio.create_task 起跑
turn 2  模型:干自己的活(读文件、整理提纲…)
        ── 与此同时:父 loop 每次 await(模型流式响应、工具执行、checkpoint 写盘)
           都把控制权交回事件循环,children 的协程正是在这些间隙里真正推进 ──
turn 3  t1 完成 → _finish() 推入 ctx.mailbox
turn 4  开始时 loop 照常 drain mailbox → "[subagent t1: done] …" 作为 user 消息进入上下文
        模型:活干完了,t2 还没好 → 调 wait_subagents() → 工具阻塞至 t2 完成,报告作为工具结果返回
turn 5  模型:汇总两份报告,产出最终答案 → run 正常结束,aclose 无事可做

单进程单事件循环,零额外基础设施。并发是真实的:LLM 调用是 I/O 密集,父的模型调用在流式等待时,children 在同一事件循环里跑得飞快——这与 web 层 RunController 并发驱动多个 run 是同一个机制。

2. "感知"发生在三个既有 seam 上,loop 本身零改动

loop 不需要知道 subagent 的存在——它照常做自己的五件事(build view → call model → run tools → drain mailbox → teardown),感知全部由插件通过既有贡献槽完成:

谁感知通过什么时机
模型(真正需要知道状态的是它)① view injector:每 turn 把 [subagents] running: t1 (93s)… 瞬态注入本次调用视图(turn 边界的状态快照,不进 transcript);② mailbox drain:完成报告在下个 turn 开始变成 user 消息;③ spawn/wait/cancel 的工具返回值每个 turn 边界
宿主程序child 继承父 tracer(span 并入同一 trace);用量折入 ctx.usage;child Agent 自己的 hooks 在子 run 里照常触发(想观察 child 的每个事件,把 hooks 配在 researcher 上即可)实时
loop只有两个被动接触点:工具/injector 是普通贡献槽;finally 里的 aclose 收尾不轮询、不阻塞、不等待

这正是方案能做到"core runtime 零改动"的原因:loop 对 children 的唯一"感知"是 teardown 时 aclose 把没收割的取消掉——它甚至不知道自己取消的是子 agent。

一个要诚实标注的边界

唤醒粒度是 turn 边界。child 在父的某个 turn 中途完成时,报告只是静静躺在 mailbox 里,模型要到下个 turn 开始才看见(与 steering 注入用户消息的语义完全一致);模型主动要结果就 wait_subagents。以及:bounded 模式下,模型一旦产出最终答案,run 就结束了——没有 harness 会像 Claude Code 那样"完成通知重新唤起模型"。所以 instructions + injector 才强制"收尾前必须 wait 或 cancel"。想要"先答复、结果稍后自动送达"的跨 run 体验,就是 deliver seam(detached 模式)存在的意义——web 的 inject-or-start 是它的一个实现,你在任何自定义宿主里给一个回调同样成立。