241 lines
6.9 KiB
TypeScript
241 lines
6.9 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
export interface EvalTraceCommand {
|
|
command: string;
|
|
status: number | null;
|
|
}
|
|
|
|
export interface EvalTraceReceipt {
|
|
task: string;
|
|
repo: string;
|
|
status: "passed" | "failed";
|
|
commands: EvalTraceCommand[];
|
|
gitHead?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface AgentRunTraceBudget {
|
|
tokens?: number;
|
|
wallClockMs?: number;
|
|
toolCalls?: number;
|
|
}
|
|
|
|
export interface AgentRunTraceEnvironment {
|
|
internet: boolean;
|
|
repoAccess: boolean;
|
|
secretsAccess: false;
|
|
deployAuthority: "none" | "gated";
|
|
}
|
|
|
|
export interface AgentRunTraceSafetyPosture {
|
|
cyber: "not-run" | "read-only" | "approved-active";
|
|
securityScan?: "not-run" | "passed" | "failed";
|
|
approval?: "not-required" | "required" | "granted";
|
|
}
|
|
|
|
export interface AgentRunTraceReceipt {
|
|
schema: "fable.agent_run.trace.v1";
|
|
runId: string;
|
|
task: string;
|
|
startedAt: string;
|
|
endedAt: string;
|
|
toolsCalled: string[];
|
|
receiptsRead: string[];
|
|
receiptsWritten: string[];
|
|
tokensIn?: number;
|
|
tokensOut?: number;
|
|
latencyMs: number;
|
|
budget: AgentRunTraceBudget;
|
|
environment: AgentRunTraceEnvironment;
|
|
safetyPosture: AgentRunTraceSafetyPosture;
|
|
decision: "ok" | "needs-human" | "blocked" | "failed";
|
|
reasons: string[];
|
|
deployAttempted: false;
|
|
}
|
|
|
|
export interface AgentRunTraceOptions {
|
|
runId: string;
|
|
task: string;
|
|
startedAt: Date;
|
|
endedAt?: Date;
|
|
toolsCalled?: string[];
|
|
receiptsRead?: string[];
|
|
receiptsWritten?: string[];
|
|
tokensIn?: number;
|
|
tokensOut?: number;
|
|
budget?: Partial<AgentRunTraceBudget>;
|
|
environment?: Partial<AgentRunTraceEnvironment>;
|
|
safetyPosture?: Partial<AgentRunTraceSafetyPosture>;
|
|
decision?: AgentRunTraceReceipt["decision"];
|
|
reasons?: string[];
|
|
}
|
|
|
|
export interface DuelEvalReceipt {
|
|
schema: "fable.eval.duel.v1";
|
|
task: string;
|
|
contenders: { a: string; b: string };
|
|
winner: "a" | "b" | "tie";
|
|
reason: string;
|
|
reviewer: "human" | "agent";
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface DuelTally {
|
|
total: number;
|
|
wins: Record<string, number>;
|
|
ties: number;
|
|
}
|
|
|
|
export interface SetGradeReceipt {
|
|
schema: "fable.benchmark.set_grade.v1";
|
|
expected: string[];
|
|
actual: string[];
|
|
intersection: string[];
|
|
jaccard: number;
|
|
passed: boolean;
|
|
threshold: number;
|
|
}
|
|
|
|
export interface VerifiableRewardReceipt {
|
|
schema: "fable.eval.verifiable_reward.v1";
|
|
prompt: string;
|
|
expected: string;
|
|
actual: string;
|
|
verifier: "exact-normalized";
|
|
reward: 0 | 1;
|
|
passed: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export function createEvalTraceReceipt(task: string, repo: string, commands: EvalTraceCommand[], gitHead?: string, now = new Date()): EvalTraceReceipt {
|
|
return {
|
|
task,
|
|
repo,
|
|
status: commands.every((cmd) => cmd.status === 0) ? "passed" : "failed",
|
|
commands,
|
|
gitHead,
|
|
createdAt: now.toISOString(),
|
|
};
|
|
}
|
|
|
|
export function createAgentRunTraceReceipt(opts: AgentRunTraceOptions): AgentRunTraceReceipt {
|
|
const endedAt = opts.endedAt ?? new Date();
|
|
const latencyMs = Math.max(0, endedAt.getTime() - opts.startedAt.getTime());
|
|
return {
|
|
schema: "fable.agent_run.trace.v1",
|
|
runId: opts.runId,
|
|
task: opts.task,
|
|
startedAt: opts.startedAt.toISOString(),
|
|
endedAt: endedAt.toISOString(),
|
|
toolsCalled: opts.toolsCalled ?? [],
|
|
receiptsRead: opts.receiptsRead ?? [],
|
|
receiptsWritten: opts.receiptsWritten ?? [],
|
|
tokensIn: opts.tokensIn,
|
|
tokensOut: opts.tokensOut,
|
|
latencyMs,
|
|
budget: {
|
|
tokens: opts.budget?.tokens,
|
|
wallClockMs: opts.budget?.wallClockMs,
|
|
toolCalls: opts.budget?.toolCalls,
|
|
},
|
|
environment: {
|
|
internet: opts.environment?.internet ?? false,
|
|
repoAccess: opts.environment?.repoAccess ?? true,
|
|
secretsAccess: false,
|
|
deployAuthority: opts.environment?.deployAuthority ?? "none",
|
|
},
|
|
safetyPosture: {
|
|
cyber: opts.safetyPosture?.cyber ?? "not-run",
|
|
securityScan: opts.safetyPosture?.securityScan ?? "not-run",
|
|
approval: opts.safetyPosture?.approval ?? "not-required",
|
|
},
|
|
decision: opts.decision ?? "ok",
|
|
reasons: opts.reasons ?? [],
|
|
deployAttempted: false,
|
|
};
|
|
}
|
|
|
|
export function writeAgentRunTraceReceipt(file: string, receipt: AgentRunTraceReceipt): string {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}
|
|
`);
|
|
return file;
|
|
}
|
|
|
|
export function writeEvalTraceReceipt(file: string, receipt: EvalTraceReceipt): string {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
return file;
|
|
}
|
|
|
|
export function createDuelEvalReceipt(opts: {
|
|
task: string;
|
|
a: string;
|
|
b: string;
|
|
winner: "a" | "b" | "tie";
|
|
reason: string;
|
|
reviewer?: "human" | "agent";
|
|
now?: Date;
|
|
}): DuelEvalReceipt {
|
|
return {
|
|
schema: "fable.eval.duel.v1",
|
|
task: opts.task,
|
|
contenders: { a: opts.a, b: opts.b },
|
|
winner: opts.winner,
|
|
reason: opts.reason,
|
|
reviewer: opts.reviewer ?? "human",
|
|
createdAt: (opts.now ?? new Date()).toISOString(),
|
|
};
|
|
}
|
|
|
|
export function writeDuelEvalReceipt(file: string, receipt: DuelEvalReceipt): string {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
return file;
|
|
}
|
|
|
|
export function tallyDuelReceipts(dir: string): DuelTally {
|
|
const tally: DuelTally = { total: 0, wins: {}, ties: 0 };
|
|
if (!fs.existsSync(dir)) return tally;
|
|
for (const file of fs.readdirSync(dir)) {
|
|
if (!file.endsWith(".json")) continue;
|
|
const receipt = JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8")) as Partial<DuelEvalReceipt>;
|
|
if (receipt.schema !== "fable.eval.duel.v1" || !receipt.contenders || !receipt.winner) continue;
|
|
tally.total += 1;
|
|
if (receipt.winner === "tie") {
|
|
tally.ties += 1;
|
|
continue;
|
|
}
|
|
const label = receipt.contenders[receipt.winner];
|
|
tally.wins[label] = (tally.wins[label] ?? 0) + 1;
|
|
}
|
|
return tally;
|
|
}
|
|
|
|
export function gradeSetRecovery(expected: string[], actual: string[], threshold = 0.8): SetGradeReceipt {
|
|
const norm = (xs: string[]) => [...new Set(xs.map((x) => x.trim().toLowerCase()).filter(Boolean))].sort();
|
|
const e = norm(expected);
|
|
const a = norm(actual);
|
|
const aSet = new Set(a);
|
|
const intersection = e.filter((x) => aSet.has(x));
|
|
const union = new Set([...e, ...a]);
|
|
const jaccard = union.size === 0 ? 1 : intersection.length / union.size;
|
|
return { schema: "fable.benchmark.set_grade.v1", expected: e, actual: a, intersection, jaccard, passed: jaccard >= threshold, threshold };
|
|
}
|
|
|
|
export function gradeVerifiableReward(prompt: string, expected: string, actual: string, now = new Date()): VerifiableRewardReceipt {
|
|
const normalize = (s: string) => s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
const passed = normalize(expected) === normalize(actual);
|
|
return {
|
|
schema: "fable.eval.verifiable_reward.v1",
|
|
prompt,
|
|
expected,
|
|
actual,
|
|
verifier: "exact-normalized",
|
|
reward: passed ? 1 : 0,
|
|
passed,
|
|
createdAt: now.toISOString(),
|
|
};
|
|
}
|