# 任务书 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` 导出,函数名由你定: 1. **建镜像**:给一个目录(里面有 Dockerfile 和依赖清单)和一个镜像名,执行 `docker build`,交回镜像编号(`docker image inspect -f '{{.Id}}'` 的值,见 vocab `RunDefinition` 的说明)。 2. **查镜像在不在**:给一个镜像编号,回答本机有没有。 3. **启动**:给一个名字、一份 `RunDefinition`、一个宿主上的运行目录,起一个容器。交回"启动了",或者"没启动 + 原因"。 4. **等结束**:给名字,等这个容器结束,交回 vocab 的 `ProcessResult`。要能接上之前另一个进程启动的容器,因为中断恢复时就是这种情况。 5. **查状态**:给名字,回答这几种之一: - 没有这个容器; - 建了但没启动; - 在跑; - 已结束; - 问不到(Docker 不应答)。 恢复时 flow 用它决定写"确认启动"还是"确认没启动";问不到时,flow 记"状态未知"并停下(方法.md 第七、第九部分)。 6. **删容器**: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` 导出,函数名由你定: 1. **读任务**:读 `task.json`,严格检查,交回任务事实;同时交回 Dockerfile 所在目录(给 runner 建镜像)和 `TASK.md` 的全文(以后 materials 给实验员)。 2. **准备运行目录**:给候选代码目录、一个空的运行目录、这次要测的假设专用指标,把候选代码、`harness.py`、`metrics-spec.json` 放进运行目录。 3. **正式运行的运行定义**:给任务、镜像编号、种子,交回 `RunDefinition`。 - 种子由调用方给。同一份研究的全部运行用同一个种子:方法.md 第十部分要求评测确定,消融不重跑基线也靠这一点。 - 种子用环境变量传给 harness。 4. **轻量检查**: - 给出轻量检查的运行定义:在镜像里跑 `check-candidate.py`,时长由调用方给(来自配置 `lightChecks.seconds`); - 解读检查结果:通过,或者不通过及原因。 - 次数上限归以后的 agents 或 flow 管,tasks 不数次数。 5. **判分**:分两步。 - 第一步:把运行目录里的 `solution.json` 和宿主保管的 `validate.py` 复制到一个单独的判分目录,交回校验器的运行定义。没有 `solution.json` 时不交运行定义,直接说明"没有产物"。 - 第二步:读校验器写的判定和运行目录里的 `metrics.json`,加上正式运行的 `ProcessResult`,交回 vocab 的 `JudgedResult`。 6. **查不许改的文件**:给改动过的文件名单,交回其中属于 `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` 准备运行目录、正式运行、跑校验器、判分。判为算数、有分数;同一份代码跑两次,分数逐位相同。分数的对错由上面手算的布局保证,这条只证明整条链接得通。 - 轻量检查:一份语法错的、一份没有入口函数的、一份正常的。 **审阅**: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` | ## 八、验收与报告 做完后本地提交,然后在回复里交一份报告,监督者会对着实际文件逐项核对: 1. 提交号,新增和修改的文件列表,每个文件的行数,runner、tasks 各自的总行数。 2. `npm run check` 和 `npm test` 的完整输出,以及 Docker 测试总共花了多久。 3. 从头到尾那条测试的实际输出:分数、两次是否逐位相同、四项资源用量。 4. `task.json` 全文,`TASK.md` 改动的摘要。 5. 审阅意见逐条:内容、采纳或不采纳、理由。 6. 你和这份任务书、结构.md、方法.md 不一致的地方,以及为什么。 7. **规则不清楚的地方**:读 CLAUDE.md、结构.md、方法.md 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。