gepa-spo
Version:
Genetic-Pareto prompt optimizer to evolve system prompts from a few rollouts with modular support and intelligent crossover
117 lines (116 loc) • 4.58 kB
JavaScript
import * as fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
export const nowId = () => new Date().toISOString().replace(/[:.]/g, '-');
export const slug = (s, n = 24) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, n).replace(/-+$/, '') || 'run';
export const sha = (s) => crypto.createHash('sha256').update(s).digest('hex').slice(0, 16);
async function ensureDir(p) { await fs.mkdir(p, { recursive: true }); }
async function atomicWrite(file, data) {
const dir = path.dirname(file);
await ensureDir(dir);
const base = path.basename(file);
const tmp = path.join(dir, `.${base}.tmp-${Math.random().toString(36).slice(2)}`);
await fs.writeFile(tmp, data, 'utf8');
let lastErr = null;
for (let attempt = 0; attempt < 8; attempt++) {
try {
await fs.rename(tmp, file); // atomic replace on same filesystem (POSIX rename)
return;
}
catch (e) {
lastErr = e;
if (e?.code === 'ENOENT' || e?.code === 'EXDEV' || e?.code === 'EBUSY') {
// Rare transient on some filesystems; brief retry and re-ensure dir
await ensureDir(dir);
await new Promise((r) => setTimeout(r, 10 * (attempt + 1)));
continue;
}
throw e;
}
}
// Final attempt or throw last error
try {
await ensureDir(dir);
await fs.rename(tmp, file);
}
catch {
// Fallback: non-atomic direct write to target to avoid test flakiness on some FS
try {
await fs.writeFile(file, data, 'utf8');
return;
}
catch {
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}
}
}
export async function writeJsonAtomic(file, obj) {
await atomicWrite(file, JSON.stringify(obj, null, 2));
}
export async function readJson(file, fallback) {
try {
return JSON.parse(await fs.readFile(file, 'utf8'));
}
catch {
return fallback;
}
}
export function buildRunDir(runsRoot, inputObj) {
const id = `${nowId()}-${slug(inputObj.system ?? 'system')}-${sha(inputObj.system ?? '')}`;
return path.join(runsRoot, id);
}
export async function acquireLock(dir) {
await ensureDir(dir);
const lock = path.join(dir, '.lock');
const h = await fs.open(lock, 'wx').catch(e => { throw new Error(`Lock exists at ${dir}. Another process running? (${e.message})`); });
await h.write(String(process.pid));
await h.close();
return async () => { await fs.rm(lock, { force: true }); };
}
export async function initRun(args) {
const runDir = buildRunDir(args.runsRoot, args.inputObj);
const iterationsDir = path.join(runDir, 'iterations');
await ensureDir(iterationsDir);
await writeJsonAtomic(path.join(runDir, 'input.json'), args.inputObj);
await writeJsonAtomic(path.join(runDir, 'config.json'), args.configObj);
if (args.outFile)
await atomicWrite(path.join(runDir, '.outpath'), args.outFile);
const budget = Number(args.configObj['budget'] ?? 100);
const state = {
version: 2,
budgetLeft: budget,
iter: 0,
Psystems: [String(args.inputObj['system'] ?? '')],
S: [],
DparetoIdx: [],
DfbIdx: [],
DholdIdx: [],
bestIdx: 0,
seeded: false,
bandit: null
};
await writeJsonAtomic(path.join(runDir, 'state.json'), state);
return { runDir, iterationsDir, state, inputObj: args.inputObj, configObj: args.configObj };
}
export async function resumeRun(runDir) {
const iterationsDir = path.join(runDir, 'iterations');
await ensureDir(iterationsDir);
const state = await readJson(path.join(runDir, 'state.json'), null);
if (!state)
throw new Error(`state.json missing in ${runDir}`);
const inputObj = await readJson(path.join(runDir, 'input.json'), {});
const configObj = await readJson(path.join(runDir, 'config.json'), {});
let outPath = null;
try {
outPath = (await fs.readFile(path.join(runDir, '.outpath'))).toString();
}
catch { /* ignore */ }
return { runDir, iterationsDir, state, inputObj, configObj, outPath };
}
export async function saveState(runDir, state) {
await writeJsonAtomic(path.join(runDir, 'state.json'), state);
}
export async function saveIteration(iterationsDir, iterNo, payload) {
const file = path.join(iterationsDir, `iter-${String(iterNo).padStart(4, '0')}.json`);
await writeJsonAtomic(file, payload);
}