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> = { 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; /** 一条完整的指标定义:任务给的最终指标、常规辅助指标,以及冻结后的假设专用指标。 */ export const MetricDefinition = Type.Object( { id: Id, layer: Type.Enum(METRIC_LAYERS, { type: "string" }), ...METRIC_FIELDS, }, CLOSED, ); export type MetricDefinition = Static; // --------------------------------------------------------------------------- // 运行定义: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; // --------------------------------------------------------------------------- // 运行结果第一层: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; // --------------------------------------------------------------------------- // 运行结果第二层: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; /** * 各条保障这次动手的次数。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; 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({}), }, CLOSED, ); export type JudgedResult = Static; // --------------------------------------------------------------------------- // 模型用量(方法.md 第七部分:拿不到用量时记"未知",不记 0) // --------------------------------------------------------------------------- /** 一次模型调用的用量。四种 token 互不重叠;价格单位为美元,与 Pi 的用量报告一致。 */ export const ModelUsage = Type.Object( { inputTokens: Measured, cacheReadTokens: Measured, cacheWriteTokens: Measured, outputTokens: Measured, cost: Measured, }, CLOSED, ); export type ModelUsage = Static;