Files
InfoRSI/eval/lib/study.ts
T
iomgaa 75d7cffd27 feat: flow 第 1 部分(骨架、研究开始、第零轮)与第零轮检验
按任务书 12 实现 src/flow:从账本现算下一步的纯函数、主循环、恢复入口、派模型任务和跑正式运行两个共用动作、冻结配置、基线、第零轮三个研究者阶段和入口分配,走到第 1 轮时停下。materials 导出 finalEntries、加 researcherId 和 StudyRecord。

审阅采纳两条:研究编号本机已用过时冻结前报错;docker run 没启动时写完结果就停下,不在本进程里无限起新编号。

eval/ 新增第零轮检验:起研究命令行、回放假模型、检验用 Jev 客户端、六问质量评估、16-real-1 回放、杀进程与两份同时跑。tsc 与 depcruise 也检查 eval/,新增 eval 只经 index.ts 用正式代码的规则。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:58:52 -04:00

83 lines
4.1 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 { execFileSync, spawn, type ChildProcess } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
// 检验脚本共用的起研究工具:检验目录放在仓库外,配置照用户的写法写一份,研究在子进程里跑(要能被 SIGKILL)。
/** 检验输出一律放在仓库外(任务书 12 第四部分第 4 条),不提交。 */
export const EVAL_ROOT = "/home/iomgaa/inforsi-eval";
/** 新建一个检验目录 <日期-名字>;已经有了就报错,不把两次检验的产物混在一起。 */
export function newEvalDir(name: string): string {
const dir = join(EVAL_ROOT, `${new Date().toISOString().slice(0, 10)}-${name}`);
if (existsSync(dir)) throw new Error(`检验目录 ${dir} 已经有了,换个名字或先挪走`);
mkdirSync(dir, { recursive: true });
return dir;
}
/**
* 照一份底稿写这次检验用的 research.json:换研究编号(容器名、镜像标签都带它),需要时换各角色模型;
* 任务目录、提示词目录写成绝对路径(底稿里相对仓库根目录),这样配置文件放在仓库外也指得对。
*/
export function writeConfig(base: string, path: string, studyId: string, models: unknown): string {
const config = JSON.parse(readFileSync(base, "utf8")) as Record<string, unknown>;
config["studyId"] = studyId;
config["task"] = resolve(config["task"] as string);
config["promptDir"] = resolve(config["promptDir"] as string);
if (models !== null) config["models"] = models;
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
return path;
}
export interface StudyProcess {
child: ChildProcess;
/** 子进程退出:退出码、信号,以及它打印的结局(被杀掉时没有,是 null)。 */
done: Promise<{ code: number | null; signal: NodeJS.Signals | null; outcome: unknown }>;
}
/** 在子进程里起 eval/lib/cli.ts。config 为 null 是接着跑。子进程的输出原样转到本进程,并记进 <研究目录>.log。 */
export function launch(config: string | null, studyDir: string, env: Record<string, string>): StudyProcess {
const args = ["eval/lib/cli.ts", ...(config === null ? ["--resume"] : ["--config", config]), "--dir", studyDir];
const child = spawn(process.execPath, args, { env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] });
const log: string[] = [];
let stdout = "";
child.stdout!.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
log.push(chunk.toString());
process.stdout.write(chunk);
});
child.stderr!.on("data", (chunk: Buffer) => {
log.push(chunk.toString());
process.stderr.write(chunk);
});
const done = new Promise<{ code: number | null; signal: NodeJS.Signals | null; outcome: unknown }>((ok) =>
child.on("close", (code, signal) => {
writeFileSync(`${studyDir}.log`, log.join(""), { flag: "a" });
const line = stdout.split("\n").find((l) => l.startsWith("RESULT "));
ok({ code, signal, outcome: line === undefined ? null : JSON.parse(line.slice("RESULT ".length)) });
}),
);
return { child, done };
}
export function sleep(ms: number): Promise<void> {
return new Promise((ok) => setTimeout(ok, ms));
}
/** 本机名字以 prefix 开头的容器(含已停止的)。检验直接问 Docker,不经正式代码。 */
export function containers(prefix: string): string[] {
const names = execFileSync("docker", ["ps", "-a", "--format", "{{.Names}}"], { encoding: "utf8" });
return names.split("\n").filter((n) => n.startsWith(prefix));
}
/**
* 等 flow 把账本的表建好,检验才第一次打开账本:Ledger.open 遇到空文件会自己建表,和 flow 同时建会冲突。
* 信号是研究目录里出现 repo.git:flow 先 Ledger.open(建表),冻结配置时才建裸仓库(src/flow/loop.ts、study.ts 的 freezeConfig)。
*/
export async function waitForLedger(studyDir: string, child: ChildProcess): Promise<void> {
while (!existsSync(join(studyDir, "repo.git"))) {
if (child.exitCode !== null) throw new Error(`研究进程没建出 repo.git 就退出了,退出码 ${child.exitCode}`);
await sleep(50);
}
}