75d7cffd27
按任务书 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>
83 lines
4.1 KiB
TypeScript
83 lines
4.1 KiB
TypeScript
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);
|
||
}
|
||
}
|