Files
InfoRSI/src/vocab/runs.ts
T
iomgaa cf6bc4c844 feat: 任务书 05,runner 与 tasks 两个模块
runner 用 Docker 跑一条命令并交回结局和资源用量;tasks 读 task.json、准备运行目录、给出正式运行/校验器/轻量检查的运行定义,并从原始产物独立判分。任务目录从参考项目搬到 tasks/circle_packing/,新增 task.json;vocab 的 VERDICTS 删掉"判不了",ProcessResult 的 cancelled 改为"容器从外面被杀了"。

全新上下文审阅后的两处修复:计量 shell 以容器内 root 运行,命令经 setpriv 降权,结果写在容器自己的文件层、结束后 docker cp 取出,并加 no-new-privileges,候选无法伪造计量结果把超时记成正常退出;harness 开跑前删掉旧的 solution.json、metrics.json,旧产物不会被当成本次的值。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:55:47 -04:00

262 lines
9.7 KiB
TypeScript
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.
import { Type, type Static } from "typebox";
import { DIRECTIONS, Id, Measured, type Json } from "./basics.ts";
/** 不认识的字段、不合格式的键一律不收。 */
const CLOSED = { additionalProperties: false } as const;
const REASON = Type.String({ minLength: 1 });
// ---------------------------------------------------------------------------
// 指标(方法.md 第五部分"指标")
// ---------------------------------------------------------------------------
/** 指标层次:最终指标由任务规定;常规辅助指标由程序统一采集;假设专用指标由研究者登记。 */
export const METRIC_LAYERS = ["final", "auxiliary", "hypothesis"] as const;
export type MetricLayer = (typeof METRIC_LAYERS)[number];
/** "测不到时怎么办"只有两档。"只记缺失"排在前面:它是诊断量的常规选择。 */
export const MISSING_POLICIES = ["recordMissing", "invalidateRun"] as const;
export type MissingPolicy = (typeof MISSING_POLICIES)[number];
/** 每一档的后果,字段说明和给模型看的材料都用这一份。 */
export const MISSING_POLICY_EFFECTS: Readonly<Record<MissingPolicy, string>> = {
recordMissing: "缺了只记缺失:这次运行的成绩照常算。诊断用的量一般选这一种。",
invalidateRun:
"缺了整次作废:这次运行不能被采纳,运行次数照扣。只有没有这个量成绩就不可信时,才选这一种。",
};
/** 一条指标除编号和层次以外的内容。研究者登记假设专用指标时填的就是这些,所以说明写给模型看。 */
const METRIC_FIELDS = {
meaning: Type.String({
minLength: 1,
description: "含义:这个量测的是什么",
}),
unit: Type.String({
minLength: 1,
description: "单位;没有单位(计数、比例)写\"无\"",
}),
direction: Type.Enum(DIRECTIONS, {
type: "string",
description: "maximize:越大越好;minimize:越小越好",
}),
howMeasured: Type.String({
minLength: 1,
description:
"怎么采样和汇总:在什么时刻测、测几次、怎么合成一个数,写到换一个人照着写代码能测出同一个数",
}),
missing: Type.Enum(MISSING_POLICIES, {
type: "string",
description:
`测不到时怎么办。recordMissing——${MISSING_POLICY_EFFECTS.recordMissing}` +
`invalidateRun——${MISSING_POLICY_EFFECTS.invalidateRun}` +
"消融格里一律只记缺失。引用别人登记过的指标时,沿用原登记的选法",
}),
};
/** 研究者在协议里登记的一条假设专用指标。编号由研究者起,候选代码按这个编号写出取值。 */
export const HypothesisMetric = Type.Object(
{
id: Type.String({
...Id,
description: "指标短名,只用字母、数字和 ._-,候选代码按它写出取值",
}),
...METRIC_FIELDS,
},
CLOSED,
);
export type HypothesisMetric = Static<typeof HypothesisMetric>;
/** 一条完整的指标定义:任务给的最终指标、常规辅助指标,以及冻结后的假设专用指标。 */
export const MetricDefinition = Type.Object(
{
id: Id,
layer: Type.Enum(METRIC_LAYERS, { type: "string" }),
...METRIC_FIELDS,
},
CLOSED,
);
export type MetricDefinition = Static<typeof MetricDefinition>;
// ---------------------------------------------------------------------------
// 运行定义:tasks 交给 runner,告诉它怎么起这个进程
// ---------------------------------------------------------------------------
export const RunDefinition = Type.Object(
{
/** 命令行,按参数切好,不经过 shell。 */
command: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
/** 进程在容器里的工作目录,绝对路径。 */
workdir: Type.String({ pattern: "^/" }),
/**
* 镜像按 sha256 钉死,不接受 tag:tag 可以被重新指向另一份内容。
* digest 填本机镜像 ID,即 `docker image inspect -f '{{.Id}}'` 的值;
* 不是仓库清单摘要(RepoDigests),本地构建的镜像没有那个值。
* 运行时必须写明:本机 Docker 的默认运行时不一定是 runc,跟着默认走,换台机器环境就悄悄变了。
*/
image: Type.Object(
{
digest: Type.String({ pattern: "^sha256:[0-9a-f]{64}$" }),
runtime: Type.String({ minLength: 1 }),
},
CLOSED,
),
/** 单次运行的墙钟时限(秒)和核数,由任务规定,是同批候选共用的比较条件。 */
wallSeconds: Type.Number({ exclusiveMinimum: 0 }),
cores: Type.Integer({ minimum: 1 }),
/**
* 候选进程不继承宿主的环境变量,类型上只允许 false。参考项目实测过:宿主环境被污染时,
* 同一份代码的分数从 0.9598 变成 0.8849,退出码是 0,没有任何报错。
*/
inheritEnv: Type.Literal(false),
/** 进程拿到的全部环境变量。 */
env: Type.Record(Type.String({ pattern: "^[A-Za-z_][A-Za-z0-9_]*$" }), Type.String(), CLOSED),
},
CLOSED,
);
export type RunDefinition = Static<typeof RunDefinition>;
// ---------------------------------------------------------------------------
// 运行结果第一层:runner 交出的进程层面结果
// ---------------------------------------------------------------------------
/** 资源用量,每一项都可能测不到。 */
const RESOURCE_USAGE = {
cpuSeconds: Measured,
wallSeconds: Measured,
peakMemoryBytes: Measured,
};
/**
* 以什么方式结束。有没有启动由它决定:只有 notStarted 是没启动,
* 其余三种都启动过、要扣次数(方法.md 第七部分)。
* 启动前就取消的记 notStarted;cancelled 是容器从外面被杀了(关机、手动 docker kill、
* 计量的 shell 自己被杀),没有留下计量结果,三项资源都记测不到。
* 退出码只有正常退出才有,资源用量只有启动过才有,所以分成四种形状。
*/
export const ProcessResult = Type.Union([
Type.Object(
{
ending: Type.Literal("notStarted"),
reason: REASON,
},
CLOSED,
),
Type.Object(
{
ending: Type.Literal("exited"),
exitCode: Type.Integer(),
...RESOURCE_USAGE,
},
CLOSED,
),
Type.Object(
{
ending: Type.Literal("timeout"),
...RESOURCE_USAGE,
},
CLOSED,
),
Type.Object(
{
ending: Type.Literal("cancelled"),
...RESOURCE_USAGE,
},
CLOSED,
),
]);
export type ProcessResult = Static<typeof ProcessResult>;
// ---------------------------------------------------------------------------
// 运行结果第二层:tasks 从原始产物独立判分
// ---------------------------------------------------------------------------
/** 判定二选一:算数、不算数。 */
export const VERDICTS = ["valid", "invalid"] as const;
export type Verdict = (typeof VERDICTS)[number];
/**
* 候选在写最终指标的同一个文件里多写的两个保留键(方法.md 第五部分"答案路径")。
* 实现的字段说明(tasks 一侧写)和给模型的材料(materials 一侧读)都用这两个常量。
*/
export const ANSWER_PATH_KEY = "answer-path";
export const SAFEGUARD_TRIGGERS_KEY = "safeguard-triggers";
/**
* 答案路径:候选写的最终答案出自哪里(main、某个因子编号或某条保障的短名)。
* tasks 只记读到的原始内容:读到了就原样存字符串;没写或写的不是字符串,记读不到并附原因。
* 取值在不在位置表里不在这里判断,由以后拿得到位置表的模块现算。
*/
export const AnswerPath = Type.Union([
Type.Object(
{
status: Type.Literal("read"),
value: Type.String(),
},
CLOSED,
),
Type.Object(
{
status: Type.Literal("unreadable"),
reason: REASON,
},
CLOSED,
),
]);
export type AnswerPath = Static<typeof AnswerPath>;
/**
* 各条保障这次动手的次数。tasks 只记读到的原始内容:读到"名字 → 非负整数"的对象就原样存;
* 没写,或者写的不是对象、值不是非负整数,记读不到并附原因。
* 名字和位置表对不对得上不在这里判断,由以后拿得到位置表的模块现算。
*/
export const SafeguardTriggers = Type.Union([
Type.Object(
{
status: Type.Literal("read"),
counts: Type.Record(Type.String(), Type.Integer({ minimum: 0 })),
},
CLOSED,
),
Type.Object(
{
status: Type.Literal("unreadable"),
reason: REASON,
},
CLOSED,
),
]);
export type SafeguardTriggers = Static<typeof SafeguardTriggers>;
export const JudgedResult = Type.Object(
{
verdict: Type.Enum(VERDICTS, { type: "string" }),
verdictReason: REASON,
/** 最终指标的分数(合法化之后的)。 */
score: Measured,
/** 各指标的取值,键是指标编号。 */
metrics: Type.Record(Id, Measured, CLOSED),
answerPath: AnswerPath,
safeguardTriggers: SafeguardTriggers,
/** 任务自己的附加记录,任务想记什么就记什么,例如填圆的合法化。 */
extra: Type.Unsafe<Json>({}),
},
CLOSED,
);
export type JudgedResult = Static<typeof JudgedResult>;
// ---------------------------------------------------------------------------
// 模型用量(方法.md 第七部分:拿不到用量时记"未知",不记 0)
// ---------------------------------------------------------------------------
/** 一次模型调用的用量。四种 token 互不重叠;价格单位为美元,与 Pi 的用量报告一致。 */
export const ModelUsage = Type.Object(
{
inputTokens: Measured,
cacheReadTokens: Measured,
cacheWriteTokens: Measured,
outputTokens: Measured,
cost: Measured,
},
CLOSED,
);
export type ModelUsage = Static<typeof ModelUsage>;