Files
InfoRSI/任务书/05-tasks-runner.md
T
2026-10-02 12:03:23 -04:00

18 KiB
Raw Blame History

任务书 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 时,哪里让你犹豫、哪里两份文件说法不一致、哪里你是靠猜的。没有就写"没有"。