Files
InfoRSI/任务书/05-tasks-runner.md
T

253 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 05:tasks(任务)与 runner(运行器)
写两个模块:
- **runner**:用 Docker 把一条命令跑完,交回它怎么结束的、用了多少资源;
- **tasks**:任务目录里的事实和文件、怎么准备一次运行、怎么从原始产物独立判分。
顺带改 vocab 的两处小地方。
本任务书已由用户和监督者在 2026-10-02 一起确定。下面说的"监督者",是在参考项目目录里和用户讨论设计的另一个会话。
## 开工前先读
- `CLAUDE.md`:全部,尤其是"罕见情况怎么办"(什么算系统边界)、"不设默认值"、"写代码的标准"。
- `docs/结构.md`:
- 第二部分:模块表里 runner、tasks 两行,以及"几条边界"里"runner 只管怎么跑""依赖跟着任务走"两条;
- 第六部分:只有 runner 和 workspace 能起子进程,tasks 不许引用 runner;
- 第七部分:行数;
- 第八部分:vocab 已导出的 `RunDefinition`、`ProcessResult`、`JudgedResult`、`AnswerPath`、`SafeguardTriggers`、指标定义。
- `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 以后按同样的形状加。
## 四、顺带改 vocab
- `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 对外的函数和类型,每项一句话;vocab 一节里 `VERDICTS` 和 `ProcessResult` 的说法同步;
- `WORK_LOG.md`:在"已完成"补一条。
- 不新建其他 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 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。