Background Agent 调研结论与 lovia 方案

TL;DR:core runtime 零改动——所需的每一个 seam 都已存在。建议以一个 core 插件 Subagents(与 Todo 同级,~300 行,零新依赖)提供"派生后台子 agent + 结果回送"能力;web 层通过两个窄回调复用现有 RunSupervisor/Scheduler 基础设施。应用层今天就能用纯公开 API 自建(文末附 recipe 证明)。不建议引入 ADK 式"工具挂起、结果后补"机制——它解决的是另一个问题,且与 lovia 的 append-only 不变量冲突。

一、外部调研:两类产品,收敛于两种模式

产品原语父阻塞?结果送达实现层
Claude CodeAgent 工具(v2.1.198 起默认后台)、后台 Bash、Monitor:完成通知在后续 turn 重新唤起模型;SendMessage 续聊harness 核心
Codexspawn_agent/send_input/wait/close_agent否,但靠阻塞 wait 拉取Rust 核心
opencodetask(background=true, task_id):child 完成后把结果作为合成消息注入父会话(ops.prompt());task_status 可轮询服务端核心
Amp子 agent 即工具;跨线程 agent(2026)回合内等汇总工具结果 / 线程间消息闭源应用层
Gemini CLI每个子 agent 注册为一个工具(委托-等待)工具结果core 包
OpenAI Agents SDK——官方示例直接用 asyncio.create_task/gatherto_input_list() / Session留给宿主
Pydantic AI后台原语;有 CallDeferred(见下)resume 时按 tool_call_id 配对留给宿主
LangGraphOSS (一切工作在 super-step 汇合);后台 run/cron/webhook 是平台服务端功能webhook / runs.join服务端
Google ADK进程内无;LongRunningFunctionTool暂停 runrun 暂停客户端带原 FunctionCall.id 重新注入

三个关键收敛点:

  1. 纯库不做后台原语,harness 都做。 OpenAI Agents SDK、Pydantic AI、LangGraph OSS 的 core 一致选择"交给宿主 asyncio / 服务端";而所有交互式 harness(Claude Code、Codex、opencode)都内置了 spawn 工具。lovia 两者都是——库 core + web harness——所以答案天然分层。

  2. 送达只有两条路,且不互斥:推(完成时注入消息/通知,Claude Code、opencode)与拉(阻塞 wait 工具,Codex)。最好的设计两者都给:推保证不遗漏,拉让模型能主动汇合。

  3. "deferred tool result"是另一个问题。 ADK 的 LongRunningFunctionTool 和 Pydantic AI 的 CallDeferred 都会暂停/结束当前 run,等外部完成后带着配对 id 重新进入——那是 human-in-the-loop/外部完成场景,不是"父 agent 继续干活"。没有任何框架在 run 内部把 tool_result 留空、事后回填,因为 provider 协议(tool_call 后必须紧跟配对 result)不允许。

技能生态的预期(mattpocock 的 research skill 等)是:harness 能把一个任务甩到后台、主对话继续,结果以文件+通知落地——即 Claude Code 模式。

二、core 要不要支持?——分层回答

Runtime loop:不需要、也不应该改。 检查过全部相关 seam 后,结论是"后台子 agent"在 lovia 里不需要任何运行时新原语:

反面明确排除:不要做 ADK 式 deferred tool result。 在 run 内让工具返回 pending、事后改写结果,会同时违反 lovia 四条硬不变量:Session append-only("Don't add a replace",AGENTS.md)、Checkpointer append-only、safe_window() 的 call/result 配对、compaction 的 byte-stable prefix(缓存)。而它服务的场景——工具需外部完成、run 暂停等待——lovia 已有等价物:approval 通道(环内暂停)+ checkpoint resume(跨进程续跑)。

能力本身:建议以 core 插件提供。 理由:(a) 技能生态把"background agent"当作 harness 标配,lovia 的 Skills 插件已经在吃这些 SKILL.md;(b) 裸 recipe 有真实尖角——final-turn 送达丢失、任务泄漏、等待语义、教模型协议的 instructions 文本——每个用户都会重新踩一遍,这正是 Todo(242 行)级别的"电池";(c) 默认实现零 web 依赖(mailbox 送达),放 lovia/plugins/ 完全干净。这符合"minimal general seams over core coupling":能力在插件里,loop 一行不动。

三、实现方案:Subagents 插件

@dataclass
class Subagents:                      # lovia/plugins/subagents.py
    agents: dict[str, Agent[Any]]     # 可派生的子 agent 目录(名字 → Agent)
    max_concurrent: int = 5           # 超出即 spawn 返回"稍后再试"
    max_turns: int = 25               # 转发给子 run,同 agent_as_tool 的绑定理由
    budget: RunBudget | None = None   # 每次 spawn 用 replace() 复制
    deliver: DeliverFn | None = None  # seam:None = 推入 ctx.mailbox(run-scoped)
    name: str = "subagents"

setup() 建 run-scoped 注册表 {tid: _TaskRecord(handle, token, status, result, delivered)},贡献三个工具 + instructions(+ 可选一个 view injector,像 Todo 一样每 turn 重现"运行中任务"清单,防模型遗忘):

送达去重:mailbox 推送与 join 拉取共用 delivered 标记,结果只进入上下文一次。

收尾策略(aclose):默认(deliver=None,run-scoped 语义)取消所有仍在跑的 child——库用户得到干净的"任务不出 run"承诺;设置了 deliver 的(web/自定义宿主)child 脱离 run 存活,由 deliver 负责后续送达。两种语义都显式、无静默泄漏。

child 上下文:同 agent_as_tool——全新上下文,只见 prompt,只回传 final output。不做 fork/继承历史(handoff 已覆盖"带全history换人";上下文靠父在 prompt 里自述,Claude Code 亦如此)。嵌套:不主动阻止(child 的 Agent 配置是开发者给的),文档建议 child 不带 Subagents(外部先例都限深:CC=3、Codex=1)。

尺寸预估 ~300 行 + 测试,与 todo.py/scheduling.py 同量级。ScriptedProvider 可全流程无网络测试。

四、若不进 core:应用层今天就能自建

全部所需接口已经公开,~25 行:

def spawn_tool(researcher: Agent) -> Tool:
    @tool
    async def spawn_research(ctx: RunContext[Any], prompt: str) -> str:
        tid = uuid4().hex[:8]
        token = CancelToken()
        async def _go() -> None:
            try:
                r = await Runner.run(researcher, prompt, cancel_token=token, max_turns=25)
                ctx.usage.add(r.usage)                       # 用量折叠,公开 API
                ctx.mailbox.push(f"[任务 {tid} 完成]\n{r.output}")
            except Exception as exc:
                ctx.mailbox.push(f"[任务 {tid} 失败] {exc}")
        asyncio.create_task(_go())                           # 引用管理略
        return f"任务 {tid} 已在后台启动,结果将以消息送达。"
    return spawn_research

core 不缺任何接口:Runner 可并发驱动、ctx.mailbox 恒在、ctx.usage.add 公开、CancelToken 公开、插件可打包。仅有的两个内部便利(_parent_usagectx._tracer)都有公开等价物(完成时 usage.add;tracer 显式传入)。缺的只是策略(final-turn 空窗、泄漏、去重、教模型的文本)——这正是插件的价值,而非新接口的理由。

五、web UI:core 插件 + web 注入两个窄 seam,基础设施全复用

web 已经拥有后台任务需要的全部重资产,新代码只是接线:

  1. child 怎么跑:复用 Scheduler._fire 的 clientless 模式(scheduler.py:257-277)——为每个 spawn 建隐藏 child session,supervisor.start(..., autostart=True, source=f"subagent:{parent_sid}")。child 由此免费获得:运行记录(RunRow/用量)、检查点与断点续跑、max_background_runs 并发上限、审批超时自动拒绝(_await_approval)、SSE 可观察可取消。

  2. 结果怎么回:复用 scheduler 已实现的 inject-or-start(scheduler.py:239-248)——父 session 有活跃 run 就 live.inject()(mailbox 注入,下个 turn 可见),没有就 supervisor.start 新起一条 run 消费结果。注意 supervisor 的 auto-chain(supervisor.py:538-545)已兜住"父正在收尾"的空窗:run 成功结束时 mailbox 有剩余会自动追加一个 leg。所以 web 里推送永不丢失

  3. UI 呈现:/api/events 生命周期总线已有 run_started/run_finished;child session 可在侧栏隐藏或按 source 标记,点开即看子任务全过程(opencode 的 child-session 导航同款体验)。

接线方式:web 在装配 agent 时给 Subagentsdeliver=(用 ctx.session_id + supervisor 做 inject-or-start)和(可选)spawn=(改走 supervisor 而非裸 Runner.run)。core 默认语义与 web 语义只差这两个回调——web 对齐 core,没有 serving 层私货。

六、开放决策点(建议 grill 的地方)

  1. 命名:插件 Subagents vs Tasks vs Background;工具 spawn_subagent/join_subagents/cancel_subagent vs opencode 式 task/task_status。我倾向 Subagents——与 agent_as_tool(同步委托)、handoff(移交)构成清晰的多 agent 三件套。

  2. aclose 默认策略:默认 cancel(run-scoped,干净)是否正确?还是跟随 Claude Code 语义(session 内存活)?我倾向 cancel——库语境里 run 是生命周期单位,web 语境由 deliver seam 升级。

  3. web child 是否为独立(隐藏)session:换来可观察/可恢复,代价是 store 里多行记录与 UI 隐藏逻辑。我倾向是。

  4. spawn seam 要不要(web 让 child 走 supervisor):不加则 web 的并发上限/记录对 child 失效。倾向加,但可以 Phase 2 再加——Phase 1 纯 core 插件已完整可用。

  5. 是否值得进 core:如果你认为技能生态驱动不足,退路是:core 只在文档里放上面的 recipe,插件整个放 web(像 Scheduling)。代价是库用户(CLI/脚本/评测)拿不到这能力。

调研报告全文来自两个后台 agent 的一手来源核查(Claude Code/Codex/Amp/opencode/Gemini CLI + OpenAI SDK/LangGraph/ADK/MAF/AutoGen/Pydantic AI/Claude Agent SDK/Temporal,均附 URL),需要原始细节可以随时展开某一家。若方向认可,下一步我可以先出 Subagents 插件的完整设计稿(含 instructions 文本与测试用例清单)再动手。