Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
任务书 05:tasks(任务)与 runner(运行器)
写两个模块:
- runner:用 Docker 把一条命令跑完,交回它怎么结束的、用了多少资源;
- tasks:任务目录里的事实和文件、怎么准备一次运行、怎么从原始产物独立判分。
顺带改 config 和 vocab 的几处小地方。
本任务书已由用户和监督者在 2026-10-02 一起确定。下面说的"监督者",是在参考项目目录里和用户讨论设计的另一个会话。
开工前先读
CLAUDE.md:全部,尤其是"罕见情况怎么办"(什么算系统边界)、"不设默认值"、"写代码的标准"。docs/结构.md:- 第二部分:模块表里 runner、tasks 两行,以及"几条边界"里"runner 只管怎么跑""依赖跟着任务走"两条;
- 第六部分:只有 runner 和 workspace 能起子进程,tasks 不许引用 runner;
- 第七部分:行数;
- 第八部分:vocab 已导出的
RunDefinition、ProcessResult、JudgedResult、AnswerPath、SafeguardTriggers、指标定义,以及 config 一节。
docs/方法.md:- 4.3 ⑨ ⑩(实现、正式运行);
- 第五部分"答案路径";
- 第七部分(扣次、轻量检查);
- 第八部分(公平比较、合法化、防作弊靠结构);
- 第九部分(先登记、再执行、再登记结果);
- 第十部分(任务需要提供什么);
- 第十一部分(对照组暂不实现)。
WORK_LOG.md。- 参考项目(只读,不要改):
/home/iomgaa/Projects/RivalBits/tasks/:任务目录和轻量检查脚本;/home/iomgaa/Projects/RivalBits/src/tasks/:只看circle-packing.ts里的protocolMetrics、validateSolution、readLegalization、readMetricsFile,其余文件看第七部分的删除表;/home/iomgaa/Projects/RivalBits/src/runner/container.ts:docker run的参数和每个参数的理由;/home/iomgaa/Projects/RivalBits/tests/circle-validator.test.ts:手算的布局,可以直接借用。
一、原则
-
系统边界:
task.json是用户提供的东西,严格检查,一次列出全部错误;- Docker 的返回是外部服务的返回,读不懂就报错停下。
模块之间的调用不是边界:flow 传进来的参数不另做检查(CLAUDE.md"罕见情况怎么办")。
-
不设默认值:
task.json每个字段必填,可以没有的写null;函数参数不写默认值。 -
每加一道检查、一个分支之前,先问"不写会怎样"。 如果答案是"会报错停下"或者"需要有人用 shell 搞破坏才会发生",就不写。参考项目这两个目录里大量代码属于后一种,第七部分已列出。
-
凡是要执行任务代码的事,都由 runner 在任务镜像里跑,包括正式运行、判分时跑校验器、轻量检查。tasks 不起子进程,只读写文件、给出运行定义、解读产物。宿主机不一定装了 numpy,三件事在同一个环境里做才一致。
-
只有正式运行在账本里登记、扣次数。判分和轻量检查只是借 runner 跑一下,不登记。登记、扣次数是 flow 的事,runner 和 tasks 都不碰账本。
二、runner
对外提供什么
经 src/runner/index.ts 导出,函数名由你定:
-
建镜像:给一个目录(里面有 Dockerfile 和依赖清单)和一个镜像名,执行
docker build,交回镜像编号(docker image inspect -f '{{.Id}}'的值,见 vocabRunDefinition的说明)。 -
查镜像在不在:给一个镜像编号,回答本机有没有。
-
启动:给一个名字、一份
RunDefinition、一个宿主上的运行目录,起一个容器。交回"启动了",或者"没启动 + 原因"。 -
等结束:给名字,等这个容器结束,交回 vocab 的
ProcessResult。要能接上之前另一个进程启动的容器,因为中断恢复时就是这种情况。 -
查状态:给名字,回答这几种之一:
- 没有这个容器;
- 建了但没启动;
- 在跑;
- 已结束;
- 问不到(Docker 不应答)。
恢复时 flow 用它决定写"确认启动"还是"确认没启动";问不到时,flow 记"状态未知"并停下(方法.md 第七、第九部分)。
-
删容器:flow 写完结果后调用。
容器名由调用方给,必须是确定的,不用时间戳或随机串:恢复时要按名字去问。runner 只检查名字符合 Docker 的要求。
不做"取消"函数:方法里没有程序主动取消一次已启动运行的场合。用户中途停下研究时,只是不再等待;容器照常跑完,恢复时接着等。
实现要点
docker run 的参数照参考项目 container.ts,理由在那里的注释里:
- 显式写运行时;
- 用当前用户的 uid:gid 运行;
- 固定主机名;
- 只传
RunDefinition.env里的环境变量; --cpus=核数,再加PYTHON_CPU_COUNT=核数;- 不加
--rm,否则中断恢复时容器已经没了; - 把运行目录挂到
RunDefinition.workdir。
另外加 --network none:方法要求运行时不联网。
资源用量在容器里面读,不用参考项目的 --cgroup-parent。 参考项目每次运行建一个系统级的 cgroup 分组,这个分组要 root 才能删,每跑一次就留一个。新办法:
- 容器里的第一个进程是一小段 shell。它用
timeout包着真正的命令;命令结束后(包括超时被杀),读容器自己的/sys/fs/cgroup/cpu.stat(usage_usec)和/sys/fs/cgroup/memory.peak,再加上墙钟时间,写进一个结果文件。 - 监督者在本机实测过(Python 3.13 slim 镜像,普通用户,
--cpus=1):一个 2 秒超时的 numpy 循环,timeout退出码 124,读到usage_usec 2007945、memory.peak 177795072。 - 结果文件放在运行目录以外、单独挂进容器的一个目录里,不和候选的产物混在一起。
- 怎么判"超时":命令自己也可能以 124 退出,所以超时要由 shell 自己确认,例如对比用时和时限,不能只看退出码。
四种结局怎么定:
| 情况 | 结局 |
|---|---|
docker run 失败,按名字再查一次,容器不存在或建了没启动(顺手删掉) |
notStarted,原因写 Docker 的报错原文 |
| 结果文件在,命令自己结束 | exited,带退出码和资源用量 |
| 结果文件在,到时限被杀 | timeout,带资源用量 |
容器已结束,但没有结果文件:从外面被杀了(关机、手动 docker kill、shell 自己被杀) |
cancelled;三项资源都记测不到,写明原因 |
docker run 失败后按名字查到容器在跑或已结束的,按"启动了"处理。
日志:容器的标准输出和标准错误,结束后存成两个文件,存到调用方给的目录里。以后给模型看失败原因时要用。
三、tasks
任务目录
把参考项目的 tasks/circle_packing/ 和 tasks/check-candidate.py 搬到新项目的 tasks/circle_packing/。
新增 task.json:写任务本身的事实,字段名由你定。要有:
- 任务种类(现在只有填圆一种);
- 最终指标的编号和方向;
- 精度 ε;
- 已知最好,可以写
null; - 单次时限、核数、Docker 运行时;
- 不许改的文件名单。
填圆的值:
- 最终指标
sum-of-radii,越大越好; - ε = 0.01,已知最好 2.635983(方法.md 6.11);
- 120 秒、1 核(方法.md 第八部分);
- 运行时
runc。
常规辅助指标的定义写在填圆的代码里,和算这些值的代码放在一起,不写进 task.json。
改:
TASK.md:这是给实验员看的说明。- 删掉"什么会被拦"一节:那是 Jev 审查运行观测量的说明,方法里已删除(方法.md 第八部分);
- "点"一律改成"因子";
- 删掉
RIVALBITS字样、"运行清单"、"调度器"这些参考项目的说法; - 写清依赖只有镜像里那几个包,容器断网;
- 其余内容和方法.md 对不上的地方,按方法.md 改。
harness.py:- 环境变量名改成
INFORSI_开头; - 假设专用指标的说明
metrics-spec.json由 tasks 在准备运行目录时直接写好,harness 不再从环境变量生成它。
- 环境变量名改成
validate.py:加一个参数,把判定写进指定的文件,tasks 读那个文件;不再靠标准输出传。baseline/SOURCES.md:去掉参考项目测试文件的说法,改成本项目里对应的测试。baseline/下其余文件逐字节不动。
删:
research.template.json、research.template.md:配置对话归 plugin;__pycache__。
Dockerfile 里拉基础镜像走 dockerproxy.net,原样保留;在 TASK.md 或 task.json 旁边写一句:换机器时可能要改这一行。
对外提供什么
经 src/tasks/index.ts 导出,函数名由你定:
- 读任务:读
task.json,严格检查,交回任务事实;同时交回 Dockerfile 所在目录(给 runner 建镜像)和TASK.md的全文(以后 materials 给实验员)。 - 准备运行目录:给候选代码目录、一个空的运行目录、这次要测的假设专用指标,把候选代码、
harness.py、metrics-spec.json放进运行目录。 - 正式运行的运行定义:给任务、镜像编号、种子,交回
RunDefinition。- 种子由调用方给。同一份研究的全部运行用同一个种子:方法.md 第十部分要求评测确定,消融不重跑基线也靠这一点。
- 种子用环境变量传给 harness。
- 轻量检查:
- 给出轻量检查的运行定义:在镜像里跑
check-candidate.py,时长由调用方给(来自配置lightChecks.seconds); - 解读检查结果:通过,或者不通过及原因。
- 次数上限归以后的 agents 或 flow 管,tasks 不数次数。
- 给出轻量检查的运行定义:在镜像里跑
- 判分:分两步。
- 第一步:把运行目录里的
solution.json和宿主保管的validate.py复制到一个单独的判分目录,交回校验器的运行定义。没有solution.json时不交运行定义,直接说明"没有产物"。 - 第二步:读校验器写的判定和运行目录里的
metrics.json,加上正式运行的ProcessResult,交回 vocab 的JudgedResult。
- 第一步:把运行目录里的
- 查不许改的文件:给改动过的文件名单,交回其中属于
task.json里"不许改"的那些。
判定怎么定
| 情况 | 判定 |
|---|---|
正式运行 timeout、cancelled,或者 exited 但退出码不是 0 |
不算数,写明原因 |
没有 solution.json |
不算数:"没有产物" |
| 校验器判不合法:形状不对、非有限数、圆心出界、修不了 | 不算数,带校验器的原因 |
| 校验器判合法(含合法化后合法) | 算数,分数取校验器算的 |
| 某条假设专用指标测不到,且它的选法是"缺了整次作废" | 不算数 |
另外几条:
- 消融格里假设专用指标一律只记缺失(vocab 指标说明)。调用方传进来的指标已经是按这条改好的,tasks 不需要知道这是不是消融格。
- 校验器自己没正常跑完(结局不是
exited,或者退出码、判定文件和参考项目约定的对不上):这是宿主这一侧的工具坏了,报错停下,不当成候选的失败。 - 合法化的三个数(原违反量、原分数、扣掉的分)放进
JudgedResult.extra,同时作为常规辅助指标的取值。修后分数高于原分数直接报错(方法.md 第八部分第 3 条)。 - 答案路径、保障动手次数:按 vocab 的
AnswerPath、SafeguardTriggers只记读到的原样内容,或者读不到及原因。 - 常规辅助指标照参考项目
protocolMetrics的那几项:墙钟、CPU 秒、峰值内存来自ProcessResult;最小间隙、原分数、原违反量、扣掉的分来自校验器。测不到就记测不到并附原因,不记 0。
vocab 的 VERDICTS 删掉"判不了"(undecidable)。上表没有哪种情况会产生它,方法.md 里也没有这种结局。结构.md 第八部分 VERDICTS 那一行同步改。
留给以后
多个任务的接口:现在只写填圆。task.json 里的任务种类对应一份实现,用一个普通的对照表,不写登记函数。nanochat、AI4AI 以后按同样的形状加。
四、顺带改 config 和 vocab
这些已写进 WORK_LOG.md"未完成",背景是用户 2026-10-02 定"对照组先不做"(方法.md 第十一部分、结构.md 第五部分)。
config:
- 删掉朴素循环,以及四个步骤开关:第零轮开关、第一次预测、挑刺与回应、消融。
- 保留第零轮讨论次数(1 或 2)、选批规则名、选格规则名。
- u、衰减挪到选格规则旁边:它们只在选格时用。λ 的位置你看着放。
stepList跟着改:不再有"完整流程还是朴素循环",按配置跳过的只剩结构.md 第三部分说的两种(只讨论 1 次时的第 2 次讨论、只有一位研究者时的讨论)。- 测试跟着改。
vocab:
- 解释的
cells改回必交,至少一格,字段说明里删掉"关掉消融时不交"的说法。 ProcessResult说明里"启动后取消的记 cancelled"改成第二部分表里的含义:容器从外面被杀了。VERDICTS删掉undecidable,见上一部分。
五、测试
runner 和 tasks 的测试用真实 Docker,放进 npm test。没有 Docker 时测试失败,不跳过。
- 镜像:用任务目录的 Dockerfile 建。本机 Docker 有缓存,参考项目的镜像编号是
7ed52793365c…,同样的 Dockerfile 加依赖清单应当命中缓存。测试里的镜像名、容器名都用inforsi-test-开头的固定名字,跑完自己删。这台机器上还有别的项目在建、删容器,盘点时只看自己的前缀。 - runner:
- 正常退出,带退出码;
- 超时:结局
timeout,CPU 秒接近时限; - 命令自己以 124 退出:不能判成超时;
- 从外面
docker kill:结局cancelled,三项资源测不到; - 镜像编号不存在:
notStarted; - 断网:容器里连不上外网;
- 核数:容器里
os.cpu_count()报 1; - 查状态:五种回答各一个,"问不到"可以用一个不存在的 Docker 地址来造;
- 等结束能接上另一个 runner 实例启动的容器。
- tasks:
task.json:缺字段、多字段、类型不对都报错,带字段路径;- 判分:借参考项目
circle-validator.test.ts里手算的布局(宽松布局半径和 0.203125、相切布局 0.40625 等),以及差一点不合法、修不了的例子;判定表里每一行至少一个例子; - 合法化:修后的分数不高于原分数,扣掉的分等于原分数减最终分数。
- 一条从头到尾的测试:用基线
initial.py准备运行目录、正式运行、跑校验器、判分。- 分数应当是
0.9597642169962064:参考项目多次真实运行得到的基线分数,不是从新代码里推出来的。 - 同一份代码跑两次,分数逐位相同。
- 分数应当是
- 轻量检查:一份语法错的、一份没有入口函数的、一份正常的。
审阅:runner 的启动判断、四种结局、资源用量,以及判分的判定表,关系到扣次数和恢复(CLAUDE.md"工作方式")。写完后派一个全新上下文的子 Agent 审阅一轮。
告诉审阅者:
- 每条意见先回答"不写会怎样";
- 模块之间传错参数这类问题只列出来,不改;
- 需要有人用 shell 搞破坏才会发生的不防。
审阅意见你逐条判断,采纳或不采纳都在报告里写理由。tasks 的其余部分按原型标准,不另审。
六、规模与文档
- 预期:runner 250–450 行,tasks 的 TypeScript 400–700 行,都不含测试;Python 文件另算。超过上限就停下来检查。
- 完成后更新:
docs/结构.md第八部分:补 runner、tasks 对外的函数和类型,每项一句话;config 一节按第四部分改;vocab 一节里VERDICTS和ProcessResult的说法同步;WORK_LOG.md:在"已完成"补一条;从"未完成"里删掉已做的 config、vocab 两条;把 submissions 跨字段规则第 5 条改成"每条解释交cells,恰好覆盖材料列出的格"。
- 不新建其他 md 文件,
TASK.md和SOURCES.md是任务目录里本来就有的。
七、参考项目的保留与删除
| 处理 | 内容 |
|---|---|
| 借鉴 | container.ts 的 docker run 参数和理由;circle-packing.ts 的 protocolMetrics、validateSolution 里解读校验器输出的部分、readLegalization、readMetricsFile;任务目录的 Python 文件 |
| 删除 | subprocess.ts 整个文件:没有 Docker 时的备用路线,方法只用 Docker |
| 删除 | audit.ts、audit-questions.ts,以及 circle-packing.ts 里的观测量表、硬规则、校准记录、代码差异、审查接进判定:Jev 审查运行观测量已删除(方法.md 第八部分) |
| 删除 | --cgroup-parent 和父分组、leftoverRunSlices、cleanupCommand:改成在容器里读资源 |
| 删除 | service.ts 里的登记、预算、核销、宿主计时器:登记和扣次数归 flow;时限由容器里的 timeout 管 |
| 删除 | measurement.ts 里测量前后核对校验器和解文件字节:防的是有人用 shell 改文件 |
| 删除 | environment-definition.ts、probeEnvironment:镜像编号已经钉住环境;pip 源地址和凭据检查随"一份研究自己的依赖清单"一起删 |
| 删除 | image-build.ts 里"一份研究一个镜像名"、依赖清单哈希核对、建镜像记录文件:依赖跟着任务走 |
| 删除 | TaskAdapter 接口那 14 个方法、适配器登记表、describeReproduction(复现脚本留到写 export 时再说)、researchBrief、formalRunProfile |
八、验收与报告
做完后本地提交,然后在回复里交一份报告,监督者会对着实际文件逐项核对:
- 提交号,新增和修改的文件列表,每个文件的行数,runner、tasks 各自的总行数。
npm run check和npm test的完整输出,以及 Docker 测试总共花了多久。- 从头到尾那条测试的实际输出:分数、两次是否逐位相同、四项资源用量。
task.json全文,TASK.md改动的摘要。- 审阅意见逐条:内容、采纳或不采纳、理由。
- 你和这份任务书、结构.md、方法.md 不一致的地方,以及为什么。
- 规则不清楚的地方:读 CLAUDE.md、结构.md、方法.md 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。