第 13 章 第二部分:架构原理

Runner 与批量执行

Single Runner 7 步编排、run_agent 工厂、Batch Runner 的 SQLite 后端、并发控制、Resume/Retry

Nexus Runner Batch SQLite 编排

Single Runner:7 步编排

nexus/runner/single.py(546 行)是框架的入口——把配置变成执行。

async def run(run_input: RunInput) -> RunOutput:
    # 0. 加载 imports 中声明的模块
    for module_path in run_input.imports:
        Registry.import_module(module_path)

    # 1. 创建 RuntimeProvider
    runtime_provider = _create_runtime_provider(run_input)

    # 2. 创建 OpenAI Clients
    openai_clients = {key: create_client(spec) for key, spec in run_input.openai_clients.items()}

    # 3. 构建 run_agent 工厂函数
    run_agent, agent_capture = _build_run_agent_factory(run_input, task_needs_agent)

    # 4. 构建 Task Input
    task_input = _build_input(task_input_type, {**task_config, run_agent=run_agent})

    # 5. 创建 Span Processors
    processors = _create_processors(run_input.processors)

    # 6. 加载 Cache
    cache_dict = load_cache(cache_config.file_path) if cache_enabled else None

    # 7. 执行
    async with ExecutionContext(...) as ctx:
        with processor_context(processors):
            with cache_context(cache_dict):
                task_output = await task_fn(task_input)

run_agent 工厂

Runner 不是直接把 Agent 函数传给 Task,而是构建一个工厂函数。工厂函数封装了 Agent 的基础配置(从 YAML),Task 只需要传业务参数:

async def run_agent(**task_kwargs) -> AgentRunOutput:
    merged_kwargs = {**run_input.agent_config, **task_kwargs}
    agent_input = _build_input(agent_input_type, merged_kwargs)
    result = await agent_fn(agent_input)
    capture.output = result  # 捕获 Agent 输出供 RunOutput 使用
    return result

RunInput / RunOutput

RunInput 是完全可序列化的(Pydantic BaseModel)——这使得分布式调度成为可能。

RunOutput 包含:

  • task_output:Task 函数的返回值
  • agent_output:Agent 函数的返回值(如果调用了 run_agent)
  • processor_results:各 Processor 的结果
  • cache_stats:缓存统计
  • success + error:执行状态

OmegaConf 配置解析

RunInput.from_file() 使用 OmegaConf 解析 YAML,支持:

  • 环境变量插值:${oc.env:API_KEY}
  • 自由文本保护:task_config.instance、agent_config.user_prompt 等字段的 ${...} 不会被解析

Batch Runner

nexus/runner/batch/(7 个文件,~95KB)实现了批量执行。

两阶段架构

Phase 1: Generation
  generator.py → generate_configs() → [BatchItem, ...]
  → 写入 SQLite database

Phase 2: Execution
  从 database 读取 pending/failed 的 runs
  → asyncio.Semaphore(max_concurrent)
  → 并行执行 single.run()
  → 写回 database

SQLite 后端

所有数据存在一个 SQLite 文件中(BatchDatabase,19.8KB):

表内容
runs配置 + 状态 + 输出 + 错误 + 时间戳
trace_eventsSpan 事件(如果 enable_trace_db=True)
log_chunks日志块(如果 enable_log_capture=True)
metadata批次元数据

SQLite 的好处:单文件、无服务器、支持并发读写(WAL 模式)、天然支持 resume。

一处缺口值得点出:这四张表都没有 schema 版本字段。跨版本读写同一个 results.db 时无从判断兼容性,§scp 一文件迁移调试 的便利性会被版本错配抵消——OpenAI Agents SDK 的 RunState 就显式维护了 CURRENT_SCHEMA_VERSION 来兜这件事(见下文佐证段)。建议在 metadata 表补一个版本号。📌 Nexus 内部是否已存在该字段,本章未体现,需对照 BatchDatabase 实际 schema 复核。

更准确地说,该补的是三件套而不是一个字段。 OpenAI Agents SDK 的 run_state.py 顶部写明了四条 schema 策略(2026-10-06 回源核对,逐字):① 已发布版本保持可读;② 未发布版本可在发布前重编号或合并;③ to_json() 始终写 CURRENT_SCHEMA_VERSION;④ “Forward compatibility is intentionally fail-fast (older SDKs reject newer or unsupported versions)“。落到实现上是三样东西:

  1. metadata 表加 schema 版本号;
  2. 维护一张 version → 一行摘要的登记表(源码注释原文要求 “Keep this mapping in chronological order. Every schema bump must add a one-line summary here.”),并用启动期断言强制——CURRENT_SCHEMA_VERSION 必须在摘要表内有对应项,且每个受支持版本必须有非空摘要;
  3. 读取时遇到未知版本直接 fail-fast 拒绝,而不是”尽力解析”。

只做第 (1) 条的后果是:跨版本读到旧库时不报错,但读到的数据结构含义已经变了——这比直接拒绝更难排查。

现在是四件套:还应加第 (4) 条——按能力设 *_MIN_SCHEMA_VERSION 常量。 上面三条解决的是”整个库能不能读”,官方源码里还有一层更细的机制(2026-10-24 实读 openai/openai-agents-python v0.24.0 的 src/agents/run_state.py)。除第 (2) 条提到的 CURRENT_SCHEMA_VERSION 外,该文件顶部并列了六个按能力划分的门槛常量:

常量版本对应能力
_PROGRAMMATIC_TOOL_CALLING_MIN_SCHEMA_VERSION1.13程序化工具调用
_HOSTED_MCP_APPROVALS_MIN_SCHEMA_VERSION1.14托管 MCP 审批
_CURRENT_RESPONSE_OWNERSHIP_MIN_SCHEMA_VERSION1.17当前响应归属
_PENDING_WRITE_COMPACTION_MIN_SCHEMA_VERSION1.18待定压缩
_HELD_PENDING_SESSION_WRITE_MIN_SCHEMA_VERSION1.19被扣下的 Session 待写入
_TOOL_OUTPUT_GUARDRAIL_BOUNDARY_MIN_SCHEMA_VERSION1.20工具输出护栏结果边界

它的价值是把”整库拒绝”降级为”这一项不可恢复”:读到 1.18 的库时,1.19 的 held pending write 无法恢复,其余照常可用,而不是整库读不了。源码注释里另有两条同形态的限定——“Hook instance attributes do not identify agents in schema 1.21 or later.” 与 “Executing pending local MCP calls requires recipient bindings written by schema 1.18 or later.”

这与上面”只做第 (1) 条的后果”是同一个问题的更细解法:第 (1)(3) 条只能给出”这个库太新,读不了”,第 (4) 条能给出”这个库缺 1.19 才有的能力,所以这一项不能恢复”。对 Nexus 的落点:每个依赖 schema 的特性各带一个最小版本号常量,读取时逐项判定,并把不可恢复的项列进 summary.json。

SQLite-for-Agent 设计决策论证

Nexus 选择 SQLite 而非分布式中间件(如 Kafka + Redis + PostgreSQL)作为 Agent 批量执行的后端,这一决策背后有系统性的工程论证:

  1. Agent 的 Single-Tenant Focus 特性:Agent 批量执行通常是单用户/单项目场景,分布式中间件的多租户能力是伪需求。SQLite 的单文件模型天然匹配单租户场景
  2. 延迟比吞吐量更关键:LLM API 调用已是毫秒-秒级延迟,额外的网络跳转(如 Redis 查询任务状态)是纯粹的惩罚。SQLite 的进程内访问将任务状态查询延迟降至微秒级
  3. WAL 读写分离可支撑数百并行子 Agent:WAL 模式允许多个读连接与一个写连接并行工作,足以覆盖 Nexus Batch Runner 的并发规模
  4. RETURNING 子句实现原子任务申领:UPDATE runs SET status='running' WHERE run_id=? RETURNING * 一条 SQL 完成状态变更和数据读取,无需分布式锁
  5. Savepoint 支持精确断点恢复:SAVEPOINT batch_checkpoint + ROLLBACK TO batch_checkpoint 实现子任务粒度的断点恢复,比分布式事务更轻量
  6. scp 一文件迁移调试:scp results.db remote:debug/ 即可迁移全部执行数据,而分布式方案需要同步多个数据存储

这一设计哲学在多个生产级 Agent 框架中得到验证——OpenClaw 将 ACP/subagent/cron/后台 CLI 四种执行体统一到 SQLite-backed 任务账本,Hermes Kanban 用 ~/.hermes/kanban.db 做多 Agent 看板编排。SQLite-for-Agent 已从”简单场景的简单选择”演变为”Agent 基础设施的有意识设计决策”。

第三条佐证来自沙箱编排层:K8s agent-sandbox 在 v1.0.5(2026-10-01)集成了 OpenAI Agents(#1388,含 workspace hydration 与 session resumption,并给出 Ray RLlib PPO 端到端示例),v1.0.4(2026-09-24)增强了 RL SDK 的并发隔离。(2026-10-24 复核:agent-sandbox 已推进到 v1.0.8 / 2026-10-22,且 patch 节奏稳定在每周一版——v1.0.6 10-08 / v1.0.7 10-15 / v1.0.8 10-22;本轮的引用只需更新版本号,本章的论证不依赖它。📌 v1.0.6–v1.0.8 是否改动了 #1388 引入的 OpenAI Agents 集成,本轮未读 release notes。)沙箱编排与 Agent SDK 两个方向同时向 RL 批量执行场景收敛,与本章”SQLite 足以覆盖 Agent 批量执行规模”的判断相互印证(此为本书分析推论 📌)。

这一判断如今有了官方 SDK 级的佐证:OpenAI Agents SDK 把 SQLite 列为 Session 层的一等实现——agents.memory.SQLiteSession,extensions/memory 下另有 AdvancedSQLiteSession、AsyncSQLiteSession、RedisSession、SQLAlchemySession、MongoDBSession、DaprSession、EncryptedSession;持久化边界则由可序列化的 RunState 承担,to_json() 快照带 CURRENT_SCHEMA_VERSION(2026-10-24 查 src/agents/run_state.py 得 1.21,对应 openai-agents v0.24.0 / 2026-10-15;此前 2026-10-06 查得 1.18 / v0.23.1,2026-09-16 查得 1.17——13 天内三次 bump),用于对接 Temporal / Restate / DBOS。本章的选型论证由此从社区实践升到官方 SDK 背书。

持久化的边界:resume 不等于不重复副作用

上面六条讲的是 SQLite 为什么够用,还差一条反向的边界。本章的状态机(pending → running → success/failed/timeout/cancelled)恢复的是 run 级状态,不是工具副作用:--retry-only 重试一个”工具已执行成功、但结果写回 DB 前崩溃”的 run 时,整个 run 从头重跑;若该 run 的 Task 含写操作(写文件、发请求、拉镜像),副作用就重复一次。

对照 Zylos Research 2026-04 调研给出的七条持久化原语,可以看清 Nexus 覆盖到哪里:

原语Batch Runner说明
事件历史 / 步骤日志❌trace_events 默认关闭(enable_trace_db=True 才落盘),log_chunks 同理
持久化步骤结果✅ 部分由 run 级 success/failed 体现,非步骤级
持久化定时器 / sleep❌无
外部事件等待❌无
重试策略✅--retry-only --retry-failed 3
幂等键❌本章 runs 表为「配置 + 状态 + 输出 + 错误 + 时间戳」,未见 attempt 计数与幂等键字段 📌
子工作流 / 子任务❌无

同一份调研对”会话记忆 ≠ 持久化执行”的区分正好点破这里的位置:保存聊天历史能让 Agent “记得”它在做什么,证明不了哪条命令真的跑过、哪次重试会不会重复某个副作用。results.db 落在两者之间——比纯会话记忆强(有 run 级状态与时间戳),比持久化执行弱(无步骤日志与幂等键)。

还有一类对象值得单列:执行环境的身份与状态。 上面七原语讲的都是”步骤与副作用”,而 OpenAI Agents SDK 的 RunState 在 1.16 / 1.17 / 1.18 三个版本里,依次把 Docker 网络隔离状态、Docker 容器标签与当前响应生成项归属、compaction 元数据(含确认与模型交换证据,供压缩重试用)以及具名 Vercel 沙箱与精确执行 ID 纳入持久化快照。也就是说,在这条产品线上”持久化执行”不仅包含步骤与幂等,还包含这次跑在哪个环境上。

对 Nexus 的含义:除了”重试会不会重复副作用”,还缺一条”这次跑在哪个沙箱上”——本章前述 runs 表的字段描述(配置 + 状态 + 输出 + 错误 + 时间戳)中未见 provider / 镜像 / 容器标识一类记录 📌(Nexus 无公开源可核,需对照 BatchDatabase 实际 schema)。对 RL 训练数据采集这类需要复现的场景,后者与幂等键同等重要。同一对象的另一面见 Ch14 §Compaction 作为一等可观测事件——那里主张把 compaction 状态做成可观测事件,这里主张把它放进持久化快照,合起来才是完整主张。

1.19–1.21 走的是第三个方向:把”什么还没落盘”也持久化。 上面讲的两类——步骤与副作用(七原语)、执行环境身份(1.16–1.18)——记的都是已经发生的事。2026-10-24 实读 v0.24.0 的 run_state.py,最近三次 bump 的摘要原文是:

  • 1.19:“Persists the interrupted turn’s withheld Session write as a held pending write, with the conversion policy its items were registered under and the current-response boundary, so an approval resume can settle it under the output-guardrail gate.”
  • 1.20:“Persists the pending tool output guardrail result boundary so a resumed run withholds current-response diagnostics until its terminal guardrails complete.”
  • 1.21:“Makes duplicate-agent identity independent of mutable hook instance state.”

三条的共同点是:持久化的对象不是”做了什么”,而是悬而未决的东西——被中断那一轮被扣下来的 Session 写入、还没跑完的护栏边界、以及一个不再依附于可变 hook 实例状态的身份标识。这恰好是本节”重试会不会重复副作用”论点的延长线:光记录已发生的还不够,还得记录”上一次到底写到哪一步”。

对 Nexus 的含义因此可以更具体:runs 表除了 attempt 计数与幂等键,还应持久化”本次有哪些写入尚未落地”(pending writes 及其转换策略)。否则 --retry-only 重跑时无从判断上一次停在哪——这比”工具是否幂等”更基础,因为连待落地项都不知道,就谈不上判断它是否已被执行过。

实践含义只有一句:重试前确认该 Task 的工具是幂等的。要往前走,成本最低的一步是在 runs 表加 attempt 计数字段;加幂等键要求工具本身支持,改动大得多。

实践含义其实是两句:还有第三类会丢的东西——累积量。 上面区分了”run 级状态”与”工具副作用”两类,还缺一类:成本、token 用量、调用次数这类累积计数。实证:客户端侧 v2.1.302(2026-10-16)修的是 “Fixed a cloud session’s cost and usage restarting from zero after the session resumed in a new process”——会话在新进程中 resume 之后,成本与用量从零重新开始,即累积量在恢复时丢失,而不是被读出来。

这类丢失比前两类更难发现,原因是它不留外部痕迹:

丢失类型恢复后表现可发现性
run 级状态run 退回 pending,重跑高——状态可见
工具副作用写操作/请求重复一次中——有外部效应
累积量计数归零,只表现为”这次好像便宜了”低——无外部痕迹,且与”跑得少”不可区分

对 Nexus 的直接含义:runs 表除 attempt 计数外,还应有跨 attempt 的累积计数(token、墙钟耗时、LLM 调用次数、工具调用次数),并且这些字段在重试时必须走”读取并累加”而非”重新计数”。本章开头列的 runs 表字段(配置 + 状态 + 输出 + 错误 + 时间戳)里,时间戳是每次覆盖语义,正与上表第三行同形。

本节的实践含义因此扩为两句:重试前确认该 Task 的工具是幂等的;重试后确认累积量是叠加而非重置。 后者成本极低——加几个字段并在重试路径上改成累加——但漏掉它会让 Batch Runner 的成本归因与吞吐统计在长跑批次上系统性偏低。

本节”比纯会话记忆强、比持久化执行弱”的定位也可以据此再细分一格:会话记忆连累积量都不记,持久化执行则把它当状态的一部分——results.db 目前与前者相同,补上跨 attempt 累积字段才迈进后者。

📌 待核实:上表”缺哪几项”依据本章对 runs 表字段的描述推断,Nexus 无公开源可核,未对照 BatchDatabase 实际 schema。

结果契约:跑完之后交出去的是什么

上面讲的都是”结果存在哪”(SQLite 四表)。还有一问没答:跑完之后交出去的那个东西是什么格式? 对”结果只给自己看”的场景这不是问题;对要跟别人对比、要进论文、要给下游训练管线消费的场景,它就是问题。

Harbor 把每个 trial 的产物固定成一组路径(models/trial/paths.py,v0.24.0 实读,核于 2026-10-24):

jobs/<job>/<trial>/
├── trial.log                 # 执行日志
├── config.json               # Trial configuration for reproducibility.
├── lock.json                 # Resolved trial inputs for reproducibility.
├── results.json              # JSON representation of TrialResult.
├── agent/  user-agent/  verifier/  artifacts/(含 manifest.json)
└── verifier/
    ├── reward.txt            # 0 / 1(或连续值)
    ├── reward.json
    └── ctrf.json             # CTRF 标准测试报告(由 viewer 读取,见下)

trial.log 与 reward.txt 不稀奇,稀奇的是 ctrf.json——CTRF(Common Test Report Format)是跨语言、跨测试框架的通用测试报告 JSON schema。一次 run 的结果可以直接喂给任何吃 CTRF 的工具(CI 报告、聚合看板、可视化),中间不需要再写一层转换。

⚠️ 一处归属需要校正,且校正后本章的论点反而更强。 原先写”ctrf.json 是 paths.py 固定的产物路径之一”并不准确:v0.24.0 的 paths.py 全文没有 ctrf 字样,EnvironmentPaths 里只有 reward_text_path(reward.txt)与 reward_json_path(reward.json)。ctrf.json 出现在 viewer 侧——src/harbor/viewer/server.py 读取 verifier_dir / "ctrf.json"(同段原文:“Returns test-stdout.txt, test-stderr.txt, ctrf.json as text, plus reward.json”)。

也就是说:CTRF 是 viewer 消费的一个外部接入格式,不在 Harbor 自己的产物契约里。 这条差异对本章论点有利——Nexus 要补的适配器因此比”照抄 Harbor 的产物结构”更轻:不必把 CTRF 塞进产物 schema,只要在导出层多写一个 target 即可(与本节结论一致)。📌 ctrf.json 由哪个组件产出(测试运行器还是 verifier)本轮未核。

补回两个上一轮漏掉、却更贴合本章论点的文件。 TrialPaths 的文档串在 trial 根级还列了三样:config.json(trial 配置,用于复现)、lock.json(“Resolved trial inputs for reproducibility”)、results.json(“JSON representation of TrialResult”)。后两者对 Nexus 的含义比对 CTRF 更直接:

  1. results.json 才是”跑完之后交出去的那个东西”的正解——CTRF 是测试报告格式,而 TrialResult 的 JSON 表示才是结果本体。本节后面建议”把 summary.json 同时写成 CTRF”,更贴切的目标是再写一份 results.json 式的结构化结果。
  2. lock.json 直击本章 §持久化的边界 的复现需求——“解析后的 trial 输入”正是复现所需的那一半,而上面花了一整节论证 runs 表里缺 provider / 镜像 / 容器标识。补一份”解析后输入快照”的成本比接 CTRF 低得多,却直接回答了本章自己提出的问题。

因此本节给 Nexus 的建议从”多一个标准格式的 target”扩成两份文件:一份 results.json 式的结果表示(对下游消费)+ 一份 lock.json 式的输入快照(对复现)。另可参照 Harbor 的 multi-step 目录结构 steps/{step_name}/{agent,verifier,artifacts}/(根级目录在每步后被迁入、结束时清理)——这为本章七原语表中”子工作流 / 子任务”那条 ❌ 提供了一个现成的目录范式。

对照 Nexus:导出是 nexus export --format jsonl | csv | full,full 目录为 config.json / output.json / metadata.json / processors/*.json / summary.json——全是自定义格式。差异的后果有三条:

  1. 跨框架对比要靠人肉对齐。 同一 Agent 在 Nexus 与 Harbor 各跑一遍,两边结果文件没有公共 schema,比较只能靠手动搬数字。
  2. 结果进不了现成的 CI / 报告工具链。 summary.json 不是任何标准报告格式,想挂上去就得自己写转换器。
  3. 外部难以复核。 第 10 章提出过”若要向外部证明 RL 训练数据的采集过程,签名化的执行历史是比单个 SQLite 文件更强的证据链”——但那讲的是证明,本节讲的是出示:CTRF 解决的是”结果能被别人用标准工具直接读走”。

需要说明的是:这不等于 Nexus 该把存储换成 CTRF。 SQLite 作为执行后端(WAL / resume / 并发申领)与导出格式是两个层次的决定,前者没有理由动。要补的只是导出层多一个标准格式的 target——把 summary.json 同时写成 CTRF 是一层薄适配器,不动四表结构,也不影响现有三种导出。

⚠️ 格式可交换的前提是值本身没被截断。 上面的建议都假设”导出文件里有什么,就交出去什么”。客户端侧 v2.1.296(2026-10-11)修的是 “Fixed workflow scripts silently truncating results, agent options and other values nested deeper than about 20,000 levels, and deeply nested results writing huge task output files”——嵌套超过约 20,000 层的结果与 agent 选项会被静默截断,且深层嵌套结果会写出超大任务输出文件。

这是导出层的上游风险:Nexus 的 processors/*.json 若含深层嵌套结构,同样会在到达导出层之前被截断,且不报错——表现为导出文件结构完整但内容不全,恰恰是上面第 3 条”外部难以复核”最需要防的形态。建议导出前对嵌套深度与单条体积各设一个显式上限,并把超限次数记进 summary.json。

⚠️ 本节 Harbor 产物路径核于 v0.24.0(发布于 2026-10-05,核于 2026-10-24;仓库 star 6,424 / fork 2,076,最后 push 2026-10-23)。另需分清:CTRF 解决的是结果可交换,不是结果可信——它同样不带签名、不带环境指纹。要证明”这个分数是在这个环境里跑出来的”仍要靠第 10 章那条签名化执行历史的路线。两者互补,不可互相替代。

并发控制

semaphore = asyncio.Semaphore(batch_config.max_concurrent)

async def _run_one(item):
    async with semaphore:
        result = await run_single(item.config)
        db.save_result(item.run_id, result)

Conflict Groups

batch_config.enable_conflict_groups=True 时,metadata.conflict_groups 中相同组的任务串行执行。用于共享资源的场景(如同一个 Docker 镜像不能并行拉取)。

DAG 依赖编排方向

当前 Batch Runner 仅支持两种编排模式——Semaphore 并行 + Conflict Groups 串行。然而,Agent 工作流中常见更复杂的依赖关系:任务 B 依赖任务 A 的输出(而非仅资源冲突),需要 fan-out/fan-in 编排。

SilkLoom Core 的 add_node(..., depends_on=[...]) 模式提供了 DAG 依赖编排的参考:每个节点声明其依赖的前置节点,调度器自动计算拓扑序并行执行无依赖节点。与 Conflict Groups 的关键区别在于:

维度Conflict GroupsDAG 依赖
解决的问题资源冲突(同镜像不并行)逻辑依赖(B 需要 A 的输出)
调度方式同组串行,异组并行拓扑排序,无依赖节点并行
数据传递无(各 run 独立)前置节点输出 → 后继节点输入

在 Nexus 中扩展 DAG 支持,可在 generator.py 的 config schema 中增加 depends_on 字段,Batch Runner 在 Phase 2 执行时按拓扑序调度。但需注意:DAG 编排引入的复杂性(循环检测、部分失败回滚、中间结果传递)可能偏离 Nexus “简单可靠” 的设计哲学,需权衡必要性。

这道权衡题在过去一个季度已经有了现成答案——durable execution 赛道把上面三件”复杂事”做成了标准件:

本章担心的复杂性业界现成解法代表实现
循环检测编译期 / 注册期图校验Temporal:工作流定义即代码,重放时命令序列与已记录历史不符即报 non-determinism error(规则 TMPRL1100)
部分失败回滚日志化步骤 + 已完成步骤重放Restate journal、Inngest / Hatchet step memoization
中间结果传递持久化步骤结果 + KV 状态Restate Virtual Object、DBOS @step 返回值落 Postgres
长时间等待人工 / 外部事件持久化定时器 + awakeableRestate awakeable、Inngest step.waitForEvent(零计算挂起)

Restate 官方把这类引擎收敛为四条一致原则:日志化步骤(外部交互的结果在返回给应用代码之前先落盘)、带幂等的自动重试(已完成步骤不重跑,重放已记录结果)、持久化定时器与信号(sleep、定时任务、人工审批等待可在崩溃与重启后存活,且不占用活进程)、可恢复性(任何在途执行可被任意健康 worker 透明接管)。

由此多出一条比自建 DAG 更轻的路径:不把 DAG 做进 Batch Runner,而是在需要 DAG 的场景把单个 run 交给外部 durable engine 托管,Nexus 只保留 RunInput 的可序列化契约——本章开头那句”RunInput 是完全可序列化的……这使得分布式调度成为可能”恰好就是现成的接口边界。是否接受这层外部依赖属编辑判断,需与 Nexus 团队确认 📌。

再往前一步是可验证执行:Dapr 1.18(2026-06)以工作流历史签名的形式把它引入运行时,Diagrid Catalyst 2.0(2026-07)推广到十个主流 Agent 框架。对 Nexus 的意义在于,批量执行的结果目前只能靠 results.db 自证;若将来需要向外部证明 RL 训练数据的采集过程,签名化的执行历史是比单个 SQLite 文件更强的证据链。📌 该条的时间与能力描述来自单一中文综述转述(2026-10-24 复测 gh api repos/dapr/dapr/releases:v1.19.0 已于 2026-10-22 GA,prerelease=false;1.19 线在 10-21 连发 rc.5 / rc.6 / rc.7,10-22 转正式,同日 1.18 线发 v1.18.9、1.17 线发 v1.17.18、1.16 线发 v1.16.21;Dapr 官方 release notes 仍未逐条核对),属”值得知道”而非”应该做”。

⚠️ “1.19 尚未 GA”这条前提已于 2026-10-22 失效,但本段的定性不宜顺势上调。 本段把可验证执行归为”值得知道而非应该做”,当时有两条理由:一是 1.19 未 GA(成熟度),二是能力描述来自单一中文综述转述、官方 release notes 未逐条核对(可靠性)。GA 只解除第一条——版本状态不再是障碍;第二条本轮仍未核,因此”值得知道”的定性保持不变。

要把定性提升到”应该做”,先要回 v1.19.0 的 release notes 确认工作流历史签名是否真的落地、以什么形式落地(是内置能力还是示例/扩展)。GA 不等于本章所说的那个能力已实现。

Resume / Retry

# 续跑:跳过已成功的
nexus run-batch -d results.db

# 只重试失败的
nexus run-batch -d results.db --retry-only --retry-failed 3

数据库中每个 run 有状态:pending → running → success/failed/timeout/cancelled。

⚠️ 停止条件必须由调度器独占,不能由可扩展点注入。 上面 §并发控制 的 Semaphore(max_concurrent)、§Conflict Groups 的串行化、本节的 --retry-failed 3,三处都假定”什么时候停由调度器决定”。客户端侧 v2.1.299(2026-10-14)修的是这个假定的一次破口(官方 changelog 原文):

“Fixed a mod that submits a prompt at the end of every turn keeping a headless run going past --max-turns, --max-budget-usd, an interrupt, or the end of the host’s conversation”

一个在每轮末尾自动提交 prompt 的 mod,能让 headless 运行越过全部四道停止条件——轮次上限、美元上限、中断信号、宿主会话结束。注意破口的位置不在任何一道闸门内部,而在“能往循环里塞新输入”这个能力上:四道闸门都判定正确,但每一次判定之后都被注入了新一轮。

Nexus 的 Processor 与 hook 正是同形态的可扩展点——本章开头第 5 步 _create_processors(run_input.processors) 把它们插在主执行路径上。若允许 Processor 在轮次末尾追加输入,max_concurrent 与 --retry-failed 两道闸门同样形同虚设。建议:Batch Runner 的轮次与预算判定放在 Processor 管道的外侧,Processor 只可声明”建议停止”,不可自行续跑。

Inspector

batch/inspector.py(32.7KB)提供了丰富的批次检视能力——nexus inspect-batch 可以查看每个 run 的状态、输出、日志、Trace。