Something went wrong. Try again.
Parametric learning in your code harness.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104// Runs and conversations are addressed by readable names: a machine name// (nested machines use "/", e.g. "chatbot/triage") plus a run id or session// name. Every write is an upsert. The disk store keeps JSON files under one// directory; a cloud volume can implement the same interface later.
import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";import path from "node:path";import type { Message, Run } from "./types.ts";
export type Session = { machine: string; name: string; messages: Message[]; updatedAt: string;};
export interface Store { putRun(run: Run): Promise<void>; getRun(machine: string, id: string): Promise<Run | undefined>; /** Newest first. */ listRuns(machine: string, options?: { limit?: number }): Promise<Run[]>; putSession(session: Session): Promise<void>; getSession(machine: string, name: string): Promise<Session | undefined>;}
const SEGMENT = /^[a-z0-9][a-z0-9._-]*$/;
/** Validate a machine name, session name, or run id and return its path segments. */export function nameSegments(name: string, what = "name"): string[] { const segments = name.split("/"); for (const segment of segments) { if (!SEGMENT.test(segment)) { throw new Error(`Invalid ${what} "${name}": use lowercase letters, digits, ".", "_" and "-", with "/" between nested names.`); } } return segments;}
/** JSON files under `root`. Files are private to the user: runs hold prompts and outputs. */export function diskStore(root: string): Store { const runDir = (machine: string) => path.join(root, "runs", ...nameSegments(machine, "machine name")); const sessionFile = (machine: string, name: string) => path.join(root, "sessions", ...nameSegments(machine, "machine name"), `${nameSegments(name, "session name").join("--")}.json`);
return { async putRun(run) { nameSegments(run.id, "run id"); await writeJson(path.join(runDir(run.machine), `${run.id}.json`), run); }, async getRun(machine, id) { nameSegments(id, "run id"); return readJson<Run>(path.join(runDir(machine), `${id}.json`)); }, async listRuns(machine, { limit = 50 } = {}) { let files: string[]; try { files = await readdir(runDir(machine)); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; } const newest = files.filter((f) => f.endsWith(".json")).sort().reverse().slice(0, limit); const runs = await Promise.all(newest.map((f) => readJson<Run>(path.join(runDir(machine), f)))); return runs.filter((r): r is Run => r !== undefined); }, async putSession(session) { await writeJson(sessionFile(session.machine, session.name), session); }, async getSession(machine, name) { return readJson<Session>(sessionFile(machine, name)); }, };}
let lastRunMs = 0;
/** * Sortable, readable run id: UTC timestamp plus a short random suffix. Ids * from one process strictly increase, even for runs started in the same * millisecond, so sorting ids sorts runs by start. */export function newRunId(now = new Date()): string { const ms = Math.max(now.getTime(), lastRunMs + 1); lastRunMs = ms; const stamp = new Date(ms).toISOString().replace(/[:.]/g, "-").replace("T", "t").replace("Z", "z"); return `${stamp}-${Math.random().toString(36).slice(2, 6)}`;}
async function writeJson(file: string, value: unknown): Promise<void> { await mkdir(path.dirname(file), { recursive: true, mode: 0o700 }); const tmp = `${file}.${process.pid}.tmp`; await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(tmp, file);}
async function readJson<T>(file: string): Promise<T | undefined> { try { return JSON.parse(await readFile(file, "utf8")) as T; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw error; }}