sandboxReuse: true,
多条 Attempt 依次共用同一个 Sandbox:Sandbox 创建和公共准备每个 Sandbox 只付一次,
每道题之间 NiceEval 自动把工作目录 git reset 回公共准备完成时的状态。
本页讲 Experiment 级的
sandboxReuse。只有几组兼容的评估需要各自的复用边界时,使用
评估组:同组串行复用,不同组继续并行。选中评估组的
Experiment 不能再声明 sandboxReuse: true。- 复用运行的结果照常进缓存。 pair 具备稳定 carry identity 时,终态结果指纹匹配就直接
沿用,不创建 Sandbox。Sandbox Plugin 的 attachment owner、name、instance key、behavior
revision、声明 identity、顺序与 setup/teardown 形状都属于 carry identity;任一项变化都会
让 slot 变为 fresh。Callback 函数体仍是 opaque;行为变化时需同步修改声明 identity 或使用
--rerun all。只有未沿用的 Attempt 才会在本次共用 Sandbox 中执行。 - 中断不等于外部状态回滚。 只在跨 Attempt 状态能回到最后一个终态提交边界时,续跑才是 同一条实验轨迹。否则应换干净 cohort 从头重建。
- 工作目录之外的状态会留下。
$HOME、/tmp、全局安装、后台进程都活过题间重置。 评估的准备代码必须能接受这一点(下面「把准备代码写对」)。 - 与
--keep-sandbox、localSandbox()互斥。
--rerun 的选择。
开启:Experiment 三件套
timeoutMs 和 lifetimeMs 是两个时钟,量的是两个对象:前者限一条 Attempt 跑多久,
后者限一个 Sandbox 活多久。想让 Sandbox 活得久一点,调 lifetimeMs,
不要调大 timeoutMs——那会同时放宽对卡死 Agent 的保护。lifetimeMs 的上限
由 Provider 账号档位决定(例如 e2b 免费档是 1 小时),超了会在创建时报 Provider 的原话。
多开终端时 Sandbox 不共享
Sandbox 复用只发生在一次 Invocation 里。两个终端同时跑一个实验时,两边有各自的 Run 和 Sandbox 池。NiceEval 不把运行中的 Sandbox handle 交给另一个进程。同一个 Record root 同时只允许一项操作;第二个终端指向同一个 root 会立即得到record-root-busy,不会和第一个终端并行写入。
确实需要并行时,给两个终端不同的 Record root:
setup() 恢复同一 checkpoint,并在 teardown() 回存时,再给实验声明稳定的非密 key:
teardown、Provider finalizer 和实验 teardown 后才释放。等待方不创建 Sandbox。租约释放后,等待方继续自己的既定计划;它不读取或沿用另一 Record 的结果。
key 会进入结果的配置身份。换 key 表示换了状态 cohort,旧结果不会混入。这个租约只按 sharedState.key 保护外部 checkpoint,不替代 Record root 的独占规则。不同机器或工作副本需要外部分布式互斥。
生命周期:谁跑几次
每条 Attempt 结束后,NiceEval 对
workdir 执行 git reset --hard + git clean
回到重置点,再开始下一条。这个 reset 只管 workdir。/opt、$HOME、/tmp、
全局安装、包缓存和后台进程不会因此消失。它们要么由评估用例的 teardown 自己收,
要么就是作者明确留给下一条用的持久状态。
大型持久 build/cache 不能只依赖「一直增长」。作者应明确容量上限和达到上限前的阈值。
正常大小和命中情况用 facts 记录,达到风险阈值才用 diagnostic 告警,并提供清理、
轮换或退休 Sandbox 的策略。
把准备代码写对:按「随什么变化」分层
- 所有实验都要的重依赖(Agent CLI、语言运行时)→ 烘进 Provider 的
image / template / snapshot,不进任何
setup。 - 整批评估共用的准备(装工具链、clone 共同仓库、预热构建缓存)→ Sandbox 级
.setup()。它每个 Sandbox 只跑一次,产物成为题间重置点的一部分,每道题白拿。 - 只有这道题要的素材(它自己的仓库、数据、依赖)→ 评估用例自己的
setup或test(t)。每题重置后重放,所以必须是重放一遍还对的代码。每题各自 clone 时, 临时 clone 目录整个实验统一用一个名字、加进diff.ignore,clone 前先rm -rf .git <临时目录>清掉上一题的残留——.git会活过题间重置。 - 起了后台进程、占了端口 → 评估用例的
teardown自己收,题间重置不杀进程。
幂等是硬要求:一反一正
Sandbox 级和评估用例级的准备代码都可能面对「上一次留下的半截状态」。两种写法:Agent 原生 Plugin:安装由 Adapter 收敛
codexAgent({ plugins: [...] })、claudeCodeAgent({ plugins: [...] }) 和
sandboxReuse 可以同时声明。Agent 原生 Plugin 装在 $HOME 里,$HOME 活过题间重置,
但这不用你处理:每条 Attempt 开始前,Adapter 把 Plugin 安装收敛到你声明的配置——
上一条 Attempt 留下的同名 marketplace 注册和插件,被替换成按声明 source 和 ref
的全新安装。插件自带脚本改写 marketplace 注册(比如换成托管源)也被同一条规则吸收。
这里说的是 Agent factory 里的原生 Plugin。评估用例、实验和评估组顶层的 plugins 字段
承载 NiceEval 条件,两者的分工见用 Plugin 复用完整评估条件。
两件事仍归你:
postSetup脚本必须幂等。它每条 Attempt 都在残留的$HOME上重跑, 往全局配置登记 hook 的脚本,重跑一遍要收敛到同一份配置。开复用前先拿两三道题 压一条 Sandbox 泳道跑一轮——第二题起才是真正的判据。- 插件数据放哪。插件安装目录每条 Attempt 都被重装覆盖。插件运行时要留到下一题的数据, 只能写在安装目录之外。留下的数据会不会污染下一题,是你的实验设计要回答的问题。
setup。marketplace 拉取太慢时用 sparse 只拉插件路径,或把插件烘进 template
并从 plugins 里拿掉声明——代价是安装 manifest 里就没有插件与解析版本的记录了。
一批里混了不同环境
选中的评估用例用environment 解析到不同预制产物时,不需要拆命令。Runner 按解析后的
环境 profile 分组:同一个 Sandbox 只承接同组 Attempt,每组建立自己的题间重置点,
组之间不共享 Sandbox 也不共享 Sandbox 级 setup 的产物。实验的 maxConcurrency
约束的是所有组加起来的同时执行数。
典型节奏
--keep-sandbox 与 sandboxReuse 互斥,而且复用的 Sandbox 被整批共享,
留下来对任何一道题都不忠实。「串起来挂、单独跑过」出现时同样这么办——不要在共用批次里
排查这种问题。流程见保留 Sandbox 现场排查问题。
做不到?用并发,别复用
准备代码改不成幂等、或者评估依赖全新的$HOME(记忆类被测对象、有状态服务),
不要声明 sandboxReuse。提速走并发就够了:
相关阅读
- 评估组——显式声明组内成员和顺序,同时保留组间并发。
- 重跑与沿用——复用 Sandbox 如何沿用终态结果,以及何时使用
--rerun。 - 并发与执行顺序——不复用时怎么用并发拿到同一份提速。
- Sandbox Provider 配置——各 Provider 的 template / image / snapshot 怎么做。