UNPKG

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
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); }