@leostera/evalkit 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -2
- package/README.md +5 -4
- package/dist/cli.mjs +510 -379
- package/dist/dashboard/assets/index-DNR4shrM.js +67 -0
- package/dist/dashboard/assets/index-TPYprg75.css +2 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/{files-C-U0-PWW.mjs → files-DkDQs2JL.mjs} +3 -0
- package/dist/{index-db-vbqEGFaU.mjs → index-db-B1FeXzmS.mjs} +333 -51
- package/dist/index.d.mts +529 -183
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +447 -173
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +10 -3
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +6 -3
- package/dist/share.d.mts +49 -7
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +4 -4
- package/dist/skills/evalkit/SKILL.md +5 -5
- package/dist/skills/evalkit-agents/SKILL.md +5 -3
- package/dist/skills/evalkit-datasets/SKILL.md +5 -5
- package/dist/skills/evalkit-evals/SKILL.md +5 -5
- package/dist/skills/evalkit-judges/SKILL.md +8 -6
- package/dist/skills/evalkit-matrices/SKILL.md +2 -2
- package/dist/skills/evalkit-results/SKILL.md +6 -6
- package/dist/{src-LeGSyDD2.mjs → src-B14fXAGp.mjs} +334 -73
- package/dist/{src-CZnDXP1K.mjs → src-Bccv9igQ.mjs} +8 -3
- package/dist/{src-mm8L_IxM.mjs → src-BfSQKOJP.mjs} +676 -321
- package/dist/upgrade-G-uFPJKg.mjs +111 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-C12IeG2y.css +0 -2
- package/dist/dashboard/assets/index-xlVsW6ZR.js +0 -37
package/dist/cli.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
':' //; exec "${EVALKIT_RUNTIME:-$(command -v bun || command -v node)}" "$0" "$@"
|
|
3
|
-
import {
|
|
4
|
-
import { a as
|
|
5
|
-
import {
|
|
6
|
-
import { t as createDirectoryShareApp } from "./src-
|
|
3
|
+
import { H as defineEvalMatrix, U as canonicalParameters, h as registerEvals, i as agentRuntimes, l as defineSuite, m as recordError, o as authoringId, tt as passMark } from "./src-B14fXAGp.mjs";
|
|
4
|
+
import { a as readRunEvents, c as readRunSummary, d as staticDatasetFile, f as staticDatasetFiles, i as ownerIsGone, l as localReportStore, n as upgradeReports, o as readRunManifest, p as globMatcher, r as runMatrix, t as runEval, u as runWorkspacePath } from "./src-BfSQKOJP.mjs";
|
|
5
|
+
import { f as encodeBundle, i as normalizeBasePath, l as dashboardIndex, s as repoKey } from "./index-db-B1FeXzmS.mjs";
|
|
6
|
+
import { t as createDirectoryShareApp } from "./src-Bccv9igQ.mjs";
|
|
7
7
|
import { Effect, LogLevel, Logger } from "effect";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { cp, lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
10
12
|
import fs, { existsSync } from "node:fs";
|
|
11
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
14
|
import { Hono } from "hono";
|
|
@@ -166,63 +168,18 @@ function shortSource(source) {
|
|
|
166
168
|
//#endregion
|
|
167
169
|
//#region ../cli/src/dashboard-reports.ts
|
|
168
170
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
entry.count += 1;
|
|
182
|
-
entry.sum += result.value;
|
|
183
|
-
}
|
|
184
|
-
byName.set(result.name, entry);
|
|
185
|
-
}
|
|
186
|
-
return [...byName.values()].map(({ sum, ...metric }) => ({
|
|
187
|
-
...metric,
|
|
188
|
-
...metric.count ? { mean: sum / metric.count } : {}
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
function runScorers(trials) {
|
|
192
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
193
|
-
for (const trial of trials) for (const score of trial?.scoring?.results ?? []) {
|
|
194
|
-
const kind = score.kind;
|
|
195
|
-
const key = `${kind}\0${score.name}`;
|
|
196
|
-
const entry = byKey.get(key) ?? {
|
|
197
|
-
name: score.name,
|
|
198
|
-
kind,
|
|
199
|
-
count: 0,
|
|
200
|
-
skipped: 0,
|
|
201
|
-
sum: 0
|
|
202
|
-
};
|
|
203
|
-
if (score.skipped) entry.skipped += 1;
|
|
204
|
-
else if (score.value !== void 0) {
|
|
205
|
-
entry.count += 1;
|
|
206
|
-
entry.sum += score.value;
|
|
207
|
-
}
|
|
208
|
-
byKey.set(key, entry);
|
|
209
|
-
}
|
|
210
|
-
return [...byKey.values()].map(({ sum, ...scorer }) => ({
|
|
211
|
-
...scorer,
|
|
212
|
-
...scorer.count ? { mean: sum / scorer.count } : {}
|
|
213
|
-
}));
|
|
214
|
-
}
|
|
215
|
-
function runDatasets(manifests) {
|
|
216
|
-
const byId = /* @__PURE__ */ new Map();
|
|
217
|
-
for (const manifest of manifests) for (const dataset of manifest?.datasets ?? []) {
|
|
218
|
-
const digests = byId.get(dataset.id) ?? /* @__PURE__ */ new Set();
|
|
219
|
-
digests.add(dataset.digest);
|
|
220
|
-
byId.set(dataset.id, digests);
|
|
221
|
-
}
|
|
222
|
-
return [...byId].map(([id, digests]) => ({
|
|
223
|
-
id,
|
|
224
|
-
digests: [...digests].sort()
|
|
225
|
-
}));
|
|
171
|
+
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
|
|
172
|
+
const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f]/g;
|
|
173
|
+
/**
|
|
174
|
+
* The first non-empty line of a message, without terminal colours, cut to
|
|
175
|
+
* `max` characters: a scorer may explain itself with 300 lines of stderr,
|
|
176
|
+
* and a summary line must stay one line. The run page has the rest.
|
|
177
|
+
*/
|
|
178
|
+
function firstLine(text, max = 100) {
|
|
179
|
+
const lines = (text ?? "").replace(ANSI, "").split(/\r?\n/).map((line) => line.replace(CONTROL, "").trim()).filter(Boolean);
|
|
180
|
+
const line = lines[0] ?? "";
|
|
181
|
+
const more = lines.length > 1 ? " …" : "";
|
|
182
|
+
return line.length > max ? `${line.slice(0, max - 1)}…` : `${line}${more}`;
|
|
226
183
|
}
|
|
227
184
|
/** Written into a run directory by `evalkit tidy`; deleting it restores the run. */
|
|
228
185
|
const ARCHIVE_MARKER = "archived.json";
|
|
@@ -240,23 +197,19 @@ async function ifPresent(operation) {
|
|
|
240
197
|
function assertUuid(id, label) {
|
|
241
198
|
if (!UUID_PATTERN.test(id)) throw new Error(`Invalid ${label}`);
|
|
242
199
|
}
|
|
243
|
-
function
|
|
200
|
+
function runPath(root, runId) {
|
|
244
201
|
assertUuid(runId, "run ID");
|
|
245
|
-
|
|
246
|
-
return resolve(root, runId, "trials", trialId);
|
|
247
|
-
}
|
|
248
|
-
function runStatus(summary) {
|
|
249
|
-
if (summary.status === "running") return "running";
|
|
250
|
-
if (summary.status === "cancelled") return "cancelled";
|
|
251
|
-
if (summary.status !== "completed") return "errored";
|
|
252
|
-
return summary.failed === 0 && summary.passed === summary.trialCount ? "passed" : "failed";
|
|
253
|
-
}
|
|
254
|
-
function trialStatus(summary) {
|
|
255
|
-
if (summary.status === "running") return "running";
|
|
256
|
-
if (summary.status === "cancelled") return "cancelled";
|
|
257
|
-
if (summary.status !== "completed") return "errored";
|
|
258
|
-
return summary.scoring?.passed === true ? "passed" : "failed";
|
|
202
|
+
return resolve(root, runId);
|
|
259
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Headers for files an agent, a dataset or an adapter produced: untrusted
|
|
206
|
+
* content on the dashboard's origin, so never rendered or sniffed as HTML.
|
|
207
|
+
*/
|
|
208
|
+
const UNTRUSTED_FILE = {
|
|
209
|
+
"content-type": "application/octet-stream",
|
|
210
|
+
"x-content-type-options": "nosniff",
|
|
211
|
+
"content-security-policy": "sandbox"
|
|
212
|
+
};
|
|
260
213
|
function fileContentType(file) {
|
|
261
214
|
return {
|
|
262
215
|
".css": "text/css",
|
|
@@ -265,15 +218,17 @@ function fileContentType(file) {
|
|
|
265
218
|
}[extname(file)] ?? "application/octet-stream";
|
|
266
219
|
}
|
|
267
220
|
/**
|
|
268
|
-
* Read saved local reports. A missing summary means "running"; malformed
|
|
269
|
-
* Runs
|
|
221
|
+
* Read saved local reports. A missing summary means "running"; malformed
|
|
222
|
+
* reports are errors. Runs from an older report format (before schema
|
|
223
|
+
* version 6) are skipped and counted, for `evalkit upgrade` to convert.
|
|
270
224
|
*/
|
|
271
|
-
function createDashboardReportReader(root
|
|
225
|
+
function createDashboardReportReader(root) {
|
|
272
226
|
const legacyRuns = /* @__PURE__ */ new Set();
|
|
273
227
|
async function listRuns() {
|
|
274
228
|
const entries = await ifPresent(() => readdir(root, { withFileTypes: true }));
|
|
275
229
|
if (!entries) return [];
|
|
276
230
|
const ids = entries.filter((entry) => entry.isDirectory() && UUID_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
231
|
+
legacyRuns.clear();
|
|
277
232
|
return (await Promise.all(ids.map(readRun))).filter((run) => run !== void 0);
|
|
278
233
|
}
|
|
279
234
|
/** True for a run from a report format this version cannot read; never for corrupt JSON. */
|
|
@@ -286,7 +241,7 @@ function createDashboardReportReader(root, registry) {
|
|
|
286
241
|
} catch {
|
|
287
242
|
return false;
|
|
288
243
|
}
|
|
289
|
-
return typeof version === "number" && version <
|
|
244
|
+
return typeof version === "number" && version < 6;
|
|
290
245
|
}
|
|
291
246
|
async function readRun(id) {
|
|
292
247
|
if (await isLegacyRun(id)) {
|
|
@@ -296,15 +251,9 @@ function createDashboardReportReader(root, registry) {
|
|
|
296
251
|
const manifest = await ifPresent(() => readRunManifest(root, id));
|
|
297
252
|
if (!manifest) return void 0;
|
|
298
253
|
const summary = await ifPresent(() => readRunSummary(root, id));
|
|
299
|
-
const trialIds = await ifPresent(() => readdir(resolve(root, id, "trials")));
|
|
300
|
-
const trials = await Promise.all((trialIds ?? []).map((trialId) => ifPresent(() => readTrialSummary(root, id, trialId))));
|
|
301
|
-
const trialManifests = await Promise.all((trialIds ?? []).map((trialId) => ifPresent(() => readTrialManifest(root, id, trialId))));
|
|
302
|
-
const scores = trials.flatMap((trial) => trial?.scoring?.overall === void 0 ? [] : [trial.scoring.overall]);
|
|
303
|
-
const scorers = runScorers(trials);
|
|
304
|
-
const metrics = runMetrics(trials);
|
|
305
|
-
const datasets = runDatasets(trialManifests);
|
|
306
254
|
const archived = await ifPresent(() => stat(resolve(root, id, ARCHIVE_MARKER)));
|
|
307
255
|
const interrupted = !summary && ownerIsGone(manifest.process);
|
|
256
|
+
const scoring = summary?.scoring;
|
|
308
257
|
return {
|
|
309
258
|
id,
|
|
310
259
|
...archived ? { archived: true } : {},
|
|
@@ -321,76 +270,81 @@ function createDashboardReportReader(root, registry) {
|
|
|
321
270
|
manifest.aut.id,
|
|
322
271
|
manifest.aut.version
|
|
323
272
|
].filter(Boolean).join(" / ") } : {},
|
|
324
|
-
status: summary ?
|
|
273
|
+
status: summary ? summary.status : interrupted ? "cancelled" : "running",
|
|
325
274
|
...interrupted ? { interrupted: true } : {},
|
|
326
275
|
startedAt: manifest.startedAt,
|
|
327
276
|
...summary?.endedAt ? { completedAt: summary.endedAt } : {},
|
|
328
277
|
...summary?.durationMs !== void 0 ? { durationMs: summary.durationMs } : {},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
...
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
278
|
+
...manifest.batch ? { batch: manifest.batch } : {},
|
|
279
|
+
...scoring?.overall === void 0 ? {} : { score: scoring.overall },
|
|
280
|
+
...scoring?.results.length ? { scorers: scoring.results.map((result) => ({
|
|
281
|
+
name: result.name,
|
|
282
|
+
kind: result.kind,
|
|
283
|
+
...result.skipped ? { skipped: true } : {},
|
|
284
|
+
...result.value === void 0 || result.skipped ? {} : { value: result.value },
|
|
285
|
+
...result.passed === void 0 || result.skipped ? {} : { passed: result.passed },
|
|
286
|
+
...result.error ? { errored: true } : {},
|
|
287
|
+
...result.choice ? { choice: result.choice } : {},
|
|
288
|
+
...result.explanation ? { explanation: firstLine(result.explanation, 200) } : {},
|
|
289
|
+
...result.judge?.model ? { model: result.judge.model } : {}
|
|
290
|
+
})) } : {},
|
|
291
|
+
...scoring?.metrics?.length ? { metrics: scoring.metrics.map((metric) => ({
|
|
292
|
+
name: metric.name,
|
|
293
|
+
...metric.unit ? { unit: metric.unit } : {},
|
|
294
|
+
...metric.better ? { better: metric.better } : {},
|
|
295
|
+
...metric.builtIn ? { builtIn: true } : {},
|
|
296
|
+
...metric.value === void 0 ? {} : { value: metric.value }
|
|
297
|
+
})) } : {},
|
|
298
|
+
...manifest.datasets?.length ? { datasets: manifest.datasets.map(({ id, digest }) => ({
|
|
347
299
|
id,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
...summary?.endedAt ? { completedAt: summary.endedAt } : {},
|
|
352
|
-
...summary?.durationMs !== void 0 ? { durationMs: summary.durationMs } : {},
|
|
353
|
-
...summary?.scoring?.overall === void 0 ? {} : { score: summary.scoring.overall },
|
|
354
|
-
scores: summary?.scoring?.results ?? [],
|
|
355
|
-
...summary?.scoring?.metrics ? { metrics: summary.scoring.metrics } : {},
|
|
356
|
-
checkpoints: summary?.scoring?.checkpoints ?? [],
|
|
357
|
-
...summary?.scoring?.skippedScorers ? { skippedScorers: summary.scoring.skippedScorers } : {}
|
|
358
|
-
};
|
|
359
|
-
}))).sort((left, right) => left.index - right.index);
|
|
300
|
+
digest
|
|
301
|
+
})) } : {}
|
|
302
|
+
};
|
|
360
303
|
}
|
|
361
|
-
async function
|
|
362
|
-
|
|
304
|
+
async function runDetail(runId) {
|
|
305
|
+
runPath(root, runId);
|
|
306
|
+
const manifest = await readRunManifest(root, runId);
|
|
307
|
+
const summary = await ifPresent(() => readRunSummary(root, runId));
|
|
308
|
+
const scoring = summary?.scoring;
|
|
363
309
|
return {
|
|
364
|
-
|
|
365
|
-
|
|
310
|
+
scores: scoring?.results ?? [],
|
|
311
|
+
...scoring?.metrics ? { metrics: scoring.metrics } : {},
|
|
312
|
+
checkpoints: scoring?.checkpoints ?? [],
|
|
313
|
+
...scoring?.skippedScorers ? { skippedScorers: scoring.skippedScorers } : {},
|
|
314
|
+
...summary?.attachments?.length ? { attachments: summary.attachments } : {},
|
|
315
|
+
manifest,
|
|
316
|
+
summary: summary ?? { status: "running" }
|
|
366
317
|
};
|
|
367
318
|
}
|
|
368
|
-
async function listArtifacts(runId
|
|
369
|
-
return walkArtifacts(resolve(
|
|
319
|
+
async function listArtifacts(runId) {
|
|
320
|
+
return walkArtifacts(resolve(runPath(root, runId), "artifacts"));
|
|
370
321
|
}
|
|
371
|
-
async function listCandidateWorkspace(runId
|
|
372
|
-
return walkArtifacts(resolve(
|
|
322
|
+
async function listCandidateWorkspace(runId) {
|
|
323
|
+
return walkArtifacts(resolve(runPath(root, runId), "artifacts", "candidate"));
|
|
373
324
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
if (!
|
|
379
|
-
|
|
325
|
+
/** A file below one of a run's artifact trees; never outside it. */
|
|
326
|
+
async function artifactFile(runId, tree, relativePath) {
|
|
327
|
+
const treeRoot = resolve(runPath(root, runId), "artifacts", tree);
|
|
328
|
+
const file = resolve(treeRoot, relativePath);
|
|
329
|
+
if (file === treeRoot || !file.startsWith(`${treeRoot}${sep}`)) throw new Error(`Invalid ${tree === "candidate" ? "workspace" : "attachment"} path`);
|
|
330
|
+
if (!(await lstat(file)).isFile()) throw new Error(`${tree === "candidate" ? "Workspace path" : "Attachment"} is not a file`);
|
|
331
|
+
return new Response(await readFile(file), { headers: UNTRUSTED_FILE });
|
|
380
332
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
333
|
+
const candidateFile = (runId, path) => artifactFile(runId, "candidate", path);
|
|
334
|
+
const attachmentFile = (runId, name) => artifactFile(runId, "attachments", name);
|
|
335
|
+
async function trajectory(runId) {
|
|
336
|
+
runPath(root, runId);
|
|
337
|
+
return await ifPresent(() => readRunEvents(root, runId)) ?? [];
|
|
384
338
|
}
|
|
385
339
|
return {
|
|
386
340
|
listRuns,
|
|
387
|
-
/** Run IDs skipped by the last `listRuns()
|
|
341
|
+
/** Run IDs skipped by the last `listRuns()`: an older EvalKit wrote them; `evalkit upgrade` converts them. */
|
|
388
342
|
legacyRunIds: () => [...legacyRuns],
|
|
389
|
-
|
|
390
|
-
trialDetail,
|
|
343
|
+
runDetail,
|
|
391
344
|
listArtifacts,
|
|
392
345
|
listCandidateWorkspace,
|
|
393
346
|
candidateFile,
|
|
347
|
+
attachmentFile,
|
|
394
348
|
trajectory
|
|
395
349
|
};
|
|
396
350
|
}
|
|
@@ -461,40 +415,32 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
461
415
|
}
|
|
462
416
|
return files;
|
|
463
417
|
}, (files) => count(files, "dataset file"));
|
|
464
|
-
const reports = createDashboardReportReader(reportRoot
|
|
418
|
+
const reports = createDashboardReportReader(reportRoot);
|
|
465
419
|
const snapshot = await step("Run reports", async (progress) => {
|
|
466
420
|
const all = await reports.listRuns();
|
|
467
421
|
const runs = all.filter((run) => !run.archived);
|
|
468
422
|
await write("runs.json", { runs });
|
|
469
|
-
let trials = 0;
|
|
470
423
|
let files = 0;
|
|
471
|
-
const runTrials = [];
|
|
472
424
|
for (const [index, run] of runs.entries()) {
|
|
473
425
|
progress(`run ${index + 1}/${runs.length} · ${run.id.slice(0, 8)}`);
|
|
474
426
|
const written = await snapshotRun(reports, run.id, directorySink(resolve(data, "runs", run.id)));
|
|
475
|
-
trials += written.trialIds.length;
|
|
476
427
|
files += written.files;
|
|
477
|
-
runTrials.push({
|
|
478
|
-
id: run.id,
|
|
479
|
-
trials: written.trialIds
|
|
480
|
-
});
|
|
481
428
|
}
|
|
482
429
|
return {
|
|
483
430
|
runs: runs.length,
|
|
484
|
-
trials,
|
|
485
431
|
files,
|
|
486
432
|
skipped: reports.legacyRunIds().length,
|
|
487
433
|
archived: all.length - runs.length,
|
|
488
|
-
|
|
434
|
+
runIds: runs.map((run) => run.id)
|
|
489
435
|
};
|
|
490
|
-
}, ({ runs,
|
|
436
|
+
}, ({ runs, files, skipped, archived }) => `${count(runs, "run")} · ${count(files, "candidate file")}` + (archived ? ` · ${archived} archived excluded` : "") + (skipped ? ` · ${skipped} older-format skipped (run \`evalkit upgrade\`)` : ""));
|
|
491
437
|
await step("Route pages", async () => {
|
|
492
438
|
const catalog = registry.catalog();
|
|
493
439
|
const routes = staticRoutes({
|
|
494
440
|
scenarios: catalog.map((entry) => entry.id),
|
|
495
441
|
agents: catalog.flatMap((entry) => entry.agent.id ? [entry.agent.id] : []),
|
|
496
442
|
datasets: catalog.flatMap((entry) => entry.datasets.map((dataset) => dataset.id)),
|
|
497
|
-
runs: snapshot.
|
|
443
|
+
runs: snapshot.runIds
|
|
498
444
|
});
|
|
499
445
|
for (const route of routes) {
|
|
500
446
|
const file = resolve(outDir, `${route}.html`);
|
|
@@ -504,7 +450,7 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
504
450
|
await writeFile(resolve(outDir, "404.html"), dashboardIndex(template, { base: "/" }));
|
|
505
451
|
return routes.length;
|
|
506
452
|
}, (pages) => `${count(pages, "page")} with clean /path URLs`);
|
|
507
|
-
const {
|
|
453
|
+
const { runIds: _runs, ...summary } = snapshot;
|
|
508
454
|
return {
|
|
509
455
|
outDir,
|
|
510
456
|
...summary
|
|
@@ -522,7 +468,6 @@ function staticRoutes(ids) {
|
|
|
522
468
|
return [
|
|
523
469
|
"scoreboard",
|
|
524
470
|
"runs",
|
|
525
|
-
"trials",
|
|
526
471
|
"workspaces",
|
|
527
472
|
"library/agents",
|
|
528
473
|
"library/judges",
|
|
@@ -531,9 +476,22 @@ function staticRoutes(ids) {
|
|
|
531
476
|
...under("library/scenarios", ids.scenarios),
|
|
532
477
|
...under("library/agents", ids.agents),
|
|
533
478
|
...under("library/datasets", ids.datasets),
|
|
534
|
-
...ids.runs.filter(
|
|
479
|
+
...ids.runs.filter(safe).flatMap((id) => [`runs/${id}`, ...RUN_SECTIONS.map((section) => `runs/${id}/${section}`)])
|
|
535
480
|
];
|
|
536
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* A run's sections, each its own page; `attachments` is the logs' URL from
|
|
484
|
+
* before 0.10, kept so old links redirect.
|
|
485
|
+
*/
|
|
486
|
+
const RUN_SECTIONS = [
|
|
487
|
+
"judging",
|
|
488
|
+
"metrics",
|
|
489
|
+
"workspace",
|
|
490
|
+
"logs",
|
|
491
|
+
"datasets",
|
|
492
|
+
"raw",
|
|
493
|
+
"attachments"
|
|
494
|
+
];
|
|
537
495
|
/** `runs/abc.html` is served at `<mount>/runs/abc`; its base is one level up. */
|
|
538
496
|
function baseFor(route) {
|
|
539
497
|
const depth = route.split("/").length - 1;
|
|
@@ -564,33 +522,33 @@ function directorySink(root) {
|
|
|
564
522
|
}
|
|
565
523
|
/**
|
|
566
524
|
* One run's snapshot, shared by `evalkit build` (files on disk) and
|
|
567
|
-
* `evalkit share` (an upload bundle): `
|
|
568
|
-
* `
|
|
569
|
-
*
|
|
525
|
+
* `evalkit share` (an upload bundle): `detail.json`, `events.json`,
|
|
526
|
+
* `artifacts.json` and `workspace.json`, the candidate files under
|
|
527
|
+
* `workspace/`, and attachments under `attachments/` (both left out with
|
|
528
|
+
* `workspaceFiles: false`; they stay listed). Evaluator workspaces are never
|
|
529
|
+
* read.
|
|
570
530
|
*/
|
|
571
531
|
async function snapshotRun(reports, runId, sink, options = {}) {
|
|
572
|
-
const
|
|
573
|
-
await sink.json("
|
|
532
|
+
const detail = await reports.runDetail(runId);
|
|
533
|
+
await sink.json("detail.json", detail);
|
|
534
|
+
await sink.json("events.json", { events: await reports.trajectory(runId) });
|
|
535
|
+
await sink.json("artifacts.json", { artifacts: await reports.listArtifacts(runId) });
|
|
536
|
+
const workspace = await reports.listCandidateWorkspace(runId);
|
|
537
|
+
await sink.json("workspace.json", workspace);
|
|
538
|
+
if (options.workspaceFiles === false) return { files: 0 };
|
|
574
539
|
let files = 0;
|
|
575
|
-
for (const
|
|
576
|
-
const
|
|
577
|
-
await sink.
|
|
578
|
-
|
|
579
|
-
await sink.json(`${base}/artifacts.json`, { artifacts: await reports.listArtifacts(runId, trial.id) });
|
|
580
|
-
const workspace = await reports.listCandidateWorkspace(runId, trial.id);
|
|
581
|
-
await sink.json(`${base}/workspace.json`, workspace);
|
|
582
|
-
if (options.workspaceFiles === false) continue;
|
|
583
|
-
for (const entry of workspace) {
|
|
584
|
-
if (entry.kind !== "file") continue;
|
|
585
|
-
const response = await reports.candidateFile(runId, trial.id, entry.path);
|
|
586
|
-
await sink.bytes(`${base}/workspace/${entry.path}`, new Uint8Array(await response.arrayBuffer()));
|
|
587
|
-
files += 1;
|
|
588
|
-
}
|
|
540
|
+
for (const attachment of detail.attachments ?? []) {
|
|
541
|
+
const response = await reports.attachmentFile(runId, attachment.name);
|
|
542
|
+
await sink.bytes(`attachments/${attachment.name}`, new Uint8Array(await response.arrayBuffer()));
|
|
543
|
+
files += 1;
|
|
589
544
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
545
|
+
for (const entry of workspace) {
|
|
546
|
+
if (entry.kind !== "file") continue;
|
|
547
|
+
const response = await reports.candidateFile(runId, entry.path);
|
|
548
|
+
await sink.bytes(`workspace/${entry.path}`, new Uint8Array(await response.arrayBuffer()));
|
|
549
|
+
files += 1;
|
|
550
|
+
}
|
|
551
|
+
return { files };
|
|
594
552
|
}
|
|
595
553
|
/** Total size and file count of a directory, for the build summary. */
|
|
596
554
|
async function directorySize(root) {
|
|
@@ -639,7 +597,7 @@ function listMatrixCells(matrix, input) {
|
|
|
639
597
|
if (![
|
|
640
598
|
"eval",
|
|
641
599
|
"agent",
|
|
642
|
-
"
|
|
600
|
+
"repeat",
|
|
643
601
|
...[...Object.keys(matrix.cases?.[0] ?? {}), ...Object.keys(matrix.parameters)].map((key) => `axis:${key}`)
|
|
644
602
|
].includes(input.sort)) throw new Error(`Unknown matrix sort: ${input.sort}`);
|
|
645
603
|
const selection = input.evalIds.length ? { evals: input.evalIds } : {};
|
|
@@ -663,7 +621,7 @@ function listMatrixCells(matrix, input) {
|
|
|
663
621
|
}
|
|
664
622
|
if (input.sort === "eval") return cell.eval.name ?? cell.eval.id;
|
|
665
623
|
if (input.sort === "agent") return cell.eval.agent?.identity?.name ?? cell.eval.agent?.identity?.kind ?? "";
|
|
666
|
-
return cell.eval.
|
|
624
|
+
return cell.eval.repeat ?? 1;
|
|
667
625
|
}
|
|
668
626
|
cells.sort((left, right) => {
|
|
669
627
|
const a = value(left);
|
|
@@ -894,7 +852,12 @@ async function discover(root, config) {
|
|
|
894
852
|
async function isDirectory(path) {
|
|
895
853
|
return stat(path).then((info) => info.isDirectory(), () => false);
|
|
896
854
|
}
|
|
897
|
-
|
|
855
|
+
/**
|
|
856
|
+
* The project's config and root, without importing any scenario: enough for
|
|
857
|
+
* commands that only touch results (`evalkit upgrade`), and that must work
|
|
858
|
+
* while scenarios from an older EvalKit still fail to load.
|
|
859
|
+
*/
|
|
860
|
+
async function loadConfig(cwd, configPath) {
|
|
898
861
|
const candidates = configPath ? [resolve(cwd, configPath)] : [
|
|
899
862
|
"evalkit.config.ts",
|
|
900
863
|
"evalkit.config.js",
|
|
@@ -908,7 +871,17 @@ async function loadProject(cwd, configPath) {
|
|
|
908
871
|
const root = file ? dirname(file) : resolve(cwd);
|
|
909
872
|
const config = file ? (await importProjectModule(file)).default : {};
|
|
910
873
|
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Config must default-export defineConfig({...})");
|
|
874
|
+
if (config.execution && "trials" in config.execution) throw new Error("execution.trials was renamed to execution.repeat in EvalKit 0.9");
|
|
875
|
+
if (config.judge !== void 0 && typeof config.judge?.start !== "function") throw new Error("judge in the config must be an agent made with defineAgent()");
|
|
911
876
|
for (const [key, value] of Object.entries(config.execution ?? {})) if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new Error(`execution.${key} must be a positive integer`);
|
|
877
|
+
return {
|
|
878
|
+
root,
|
|
879
|
+
...file ? { file } : {},
|
|
880
|
+
config
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
async function loadProject(cwd, configPath) {
|
|
884
|
+
const { root, file, config } = await loadConfig(cwd, configPath);
|
|
912
885
|
let registry = config.registry;
|
|
913
886
|
if (!file && !configPath && await exists$1(resolve(root, "src/registry.ts"))) registry = (await importProjectModule(resolve(root, "src/registry.ts"))).default;
|
|
914
887
|
let loadErrors = [];
|
|
@@ -937,6 +910,17 @@ async function loadProject(cwd, configPath) {
|
|
|
937
910
|
matrices: [...registry.matrices, matrix]
|
|
938
911
|
};
|
|
939
912
|
}
|
|
913
|
+
const judge = config.judge;
|
|
914
|
+
if (judge) {
|
|
915
|
+
const catalog = registry.catalog;
|
|
916
|
+
registry = {
|
|
917
|
+
...registry,
|
|
918
|
+
catalog: (options) => catalog({
|
|
919
|
+
judge,
|
|
920
|
+
...options
|
|
921
|
+
})
|
|
922
|
+
};
|
|
923
|
+
}
|
|
940
924
|
return {
|
|
941
925
|
root,
|
|
942
926
|
config,
|
|
@@ -944,6 +928,34 @@ async function loadProject(cwd, configPath) {
|
|
|
944
928
|
loadErrors
|
|
945
929
|
};
|
|
946
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* Where a project's run workspaces live. By default outside the project, in
|
|
933
|
+
* the system temp directory (one directory per project, so `tidy` and `dev`
|
|
934
|
+
* find the ones it kept): an agent that looks around its workspace finds no
|
|
935
|
+
* project source, no `AGENTS.md` or skills above it, and no results.
|
|
936
|
+
* `sandboxDir` overrides it; `inside` says when that puts them back in the
|
|
937
|
+
* project, which `run` warns about.
|
|
938
|
+
*/
|
|
939
|
+
function sandboxLocation(project) {
|
|
940
|
+
if (project.config.sandboxDir === void 0) return {
|
|
941
|
+
path: defaultSandboxDir(project.root),
|
|
942
|
+
inside: false
|
|
943
|
+
};
|
|
944
|
+
const path = resolve(project.root, project.config.sandboxDir);
|
|
945
|
+
const fromRoot = relative(project.root, path);
|
|
946
|
+
return {
|
|
947
|
+
path,
|
|
948
|
+
inside: !fromRoot.startsWith("..") && !isAbsolute(fromRoot)
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/** `<tmp>/evalkit/<project name>-<hash of its path>`: stable per checkout. */
|
|
952
|
+
function defaultSandboxDir(root) {
|
|
953
|
+
const hash = createHash("sha256").update(root).digest("hex").slice(0, 10);
|
|
954
|
+
const name = basename(root).replace(/[^A-Za-z0-9._-]/g, "-") || "project";
|
|
955
|
+
return join(tmpdir(), "evalkit", `${name}-${hash}`);
|
|
956
|
+
}
|
|
957
|
+
/** The warning `run` prints when workspaces sit inside the project. */
|
|
958
|
+
const SANDBOX_INSIDE_WARNING = "sandboxDir is inside the project: agents can read its source, its AGENTS.md and skills, its results and other runs’ workspaces from there. Leave sandboxDir unset to keep workspaces in the system temp directory.";
|
|
947
959
|
//#endregion
|
|
948
960
|
//#region ../cli/src/terminal.ts
|
|
949
961
|
/** Human CLI output. Colors and live progress only appear on an interactive terminal. */
|
|
@@ -996,25 +1008,23 @@ function cellLabel(name, parameters) {
|
|
|
996
1008
|
const shown = Object.entries(parameters).map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`).join(" ");
|
|
997
1009
|
return shown ? `${bold(name)} ${dim(shown)}` : bold(name);
|
|
998
1010
|
}
|
|
999
|
-
function passed(result) {
|
|
1000
|
-
return result.status === "completed" && (result.aggregateScoring?.passRate === 1 || Boolean(result.scoring?.passed));
|
|
1001
|
-
}
|
|
1002
1011
|
/** Live log for runs launched from the `evalkit dev` dashboard. */
|
|
1003
1012
|
const runLog = {
|
|
1004
1013
|
started(name, parameters = {}, suiteId) {
|
|
1005
1014
|
const suite = suiteId ? dim(` (suite ${suiteId})`) : "";
|
|
1006
1015
|
console.log(`${clock()} ${cyan("▶ run ")} ${cellLabel(name, parameters)}${suite}`);
|
|
1007
1016
|
},
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1017
|
+
/** One line per run; a batch's runs say which one they are. */
|
|
1018
|
+
finished(name, parameters, runs, ms) {
|
|
1019
|
+
for (const result of runs) {
|
|
1020
|
+
const status = result.status === "passed" ? green("✓ pass") : result.status === "failed" ? red("✗ fail") : red(`✗ ${result.status}`);
|
|
1021
|
+
const batch = result.batch ? ` #${result.batch.index + 1}/${result.batch.size}` : "";
|
|
1022
|
+
console.log(`${clock()} ${status} ${cellLabel(name, parameters)}${dim(`${batch} · ${duration(result.durationMs ?? ms)}`)}`);
|
|
1023
|
+
console.log(` ${dim(`report ${result.reportLocation}`)}`);
|
|
1024
|
+
}
|
|
1015
1025
|
},
|
|
1016
|
-
cell(cell,
|
|
1017
|
-
runLog.finished(cell.eval.name ?? authoringId(cell.eval), cell.parameters,
|
|
1026
|
+
cell(cell, runs, ms) {
|
|
1027
|
+
runLog.finished(cell.eval.name ?? authoringId(cell.eval), cell.parameters, runs, ms);
|
|
1018
1028
|
},
|
|
1019
1029
|
failed(name, error) {
|
|
1020
1030
|
console.log(`${clock()} ${red("✗ error")} ${bold(name)} ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1066,72 +1076,88 @@ async function confirm(question) {
|
|
|
1066
1076
|
}
|
|
1067
1077
|
//#endregion
|
|
1068
1078
|
//#region ../cli/src/run-reporter.ts
|
|
1069
|
-
function
|
|
1079
|
+
function runOutcome(result) {
|
|
1070
1080
|
if (result.status === "cancelled") return "cancelled";
|
|
1071
|
-
return result.status === "
|
|
1072
|
-
}
|
|
1073
|
-
function passAtFor(definition, name) {
|
|
1074
|
-
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "predicate" && step.name === name);
|
|
1075
|
-
return rule?.kind === "predicate" ? rule.passAt : void 0;
|
|
1081
|
+
return result.status === "passed" ? "passed" : "failed";
|
|
1076
1082
|
}
|
|
1077
1083
|
function value(score) {
|
|
1078
1084
|
return score.value === void 0 ? "" : score.value.toFixed(1);
|
|
1079
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* One failed verdict in words: `history 0.0 (passes at 1): got 2 entries`,
|
|
1088
|
+
* or for a choices judge `Correctness C (passes at A or B): wrong product`.
|
|
1089
|
+
*/
|
|
1090
|
+
function verdictReason(definition, verdict) {
|
|
1091
|
+
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "judge" && step.name === verdict.name);
|
|
1092
|
+
const answer = rule?.kind === "judge" ? rule.answer : void 0;
|
|
1093
|
+
const mark = verdict.passAt === void 0 ? "" : answer?.kind === "choices" ? ` (passes at ${passMark(verdict.passAt, answer)})` : ` (passes at ${verdict.passAt})`;
|
|
1094
|
+
const why = [criteriaReason(verdict.criteria), firstLine(verdict.explanation)].filter(Boolean).join(" · ");
|
|
1095
|
+
return `${verdict.name} ${verdict.choice ?? value(verdict)}${mark}${why ? `: ${why}` : ""}`;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Which criteria let the score down, so seven checks the app never reached
|
|
1099
|
+
* read as one cause (`not reached: saves, history`), not seven failures.
|
|
1100
|
+
*/
|
|
1101
|
+
function criteriaReason(criteria) {
|
|
1102
|
+
if (!criteria?.length) return "";
|
|
1103
|
+
const failed = criteria.filter((criterion) => "value" in criterion && criterion.value < 1);
|
|
1104
|
+
const unreached = criteria.filter((criterion) => !("value" in criterion));
|
|
1105
|
+
return [failed.length ? `failed ${failed.map((criterion) => {
|
|
1106
|
+
const why = firstLine(criterion.explanation, 60);
|
|
1107
|
+
return why ? `${criterion.name} (${why})` : criterion.name;
|
|
1108
|
+
}).join(", ")}` : "", unreached.length ? `not reached ${unreached.map((criterion) => criterion.name).join(", ")}` : ""].filter(Boolean).join(" · ");
|
|
1109
|
+
}
|
|
1080
1110
|
function scoreReason(definition, score) {
|
|
1081
|
-
if (score.error) return `${score.name} error: ${score.error.message}`;
|
|
1082
|
-
|
|
1083
|
-
return `${score.name} ${value(score)}${passAt === void 0 ? "" : ` (passes at ${passAt})`}`;
|
|
1111
|
+
if (score.error) return `${score.name} error: ${firstLine(score.error.message)}`;
|
|
1112
|
+
return verdictReason(definition, score);
|
|
1084
1113
|
}
|
|
1085
1114
|
function checkpointReason(definition, checkpoint) {
|
|
1086
|
-
if (checkpoint.status === "error") return `step ${checkpoint.step} ${checkpoint.name} error: ${checkpoint.error?.message
|
|
1087
|
-
|
|
1088
|
-
return `step ${checkpoint.step} ${
|
|
1115
|
+
if (checkpoint.status === "error") return `step ${checkpoint.step} ${checkpoint.name} error: ${firstLine(checkpoint.error?.message)}`;
|
|
1116
|
+
if (checkpoint.value === void 0 && !checkpoint.choice) return `step ${checkpoint.step} ${checkpoint.name} ${checkpoint.status}`;
|
|
1117
|
+
return `step ${checkpoint.step} ${verdictReason(definition, checkpoint)}`;
|
|
1089
1118
|
}
|
|
1090
|
-
/** What went wrong in a
|
|
1091
|
-
function reasonsFor(definition,
|
|
1119
|
+
/** What went wrong in a run. */
|
|
1120
|
+
function reasonsFor(definition, run) {
|
|
1092
1121
|
const reasons = /* @__PURE__ */ new Set();
|
|
1093
1122
|
const skipped = /* @__PURE__ */ new Set();
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
for (const name of trial.scoring?.skippedScorers ?? []) if (trial.status !== "cancelled") reasons.add(`${name} not run`);
|
|
1100
|
-
}
|
|
1123
|
+
if (run.status === "errored" && run.error) reasons.add(`error: ${firstLine(run.error.message, 160)}`);
|
|
1124
|
+
for (const checkpoint of run.scoring?.checkpoints ?? []) if (checkpoint.status === "failed" || checkpoint.status === "error") reasons.add(checkpointReason(definition, checkpoint));
|
|
1125
|
+
for (const score of run.scoring?.results ?? []) if (score.skipped) skipped.add(score.name);
|
|
1126
|
+
else if (!score.passed) reasons.add(scoreReason(definition, score));
|
|
1127
|
+
for (const name of run.scoring?.skippedScorers ?? []) if (run.status !== "cancelled") reasons.add(`${name} not run`);
|
|
1101
1128
|
return {
|
|
1102
1129
|
reasons: [...reasons],
|
|
1103
1130
|
skipped: [...skipped]
|
|
1104
1131
|
};
|
|
1105
1132
|
}
|
|
1106
|
-
function
|
|
1133
|
+
function runReport(cell, result, suiteId) {
|
|
1107
1134
|
const evalId = authoringId(cell.eval);
|
|
1108
|
-
const trials = result.trials ?? [result];
|
|
1109
1135
|
return {
|
|
1110
1136
|
label: suiteId ? `${suiteId}/${evalId}` : evalId,
|
|
1111
1137
|
evalId,
|
|
1112
1138
|
...suiteId ? { suiteId } : {},
|
|
1113
1139
|
parameters: cell.parameters,
|
|
1114
|
-
outcome:
|
|
1140
|
+
outcome: runOutcome(result),
|
|
1115
1141
|
...result.durationMs !== void 0 ? { durationMs: result.durationMs } : {},
|
|
1116
1142
|
result,
|
|
1117
|
-
...reasonsFor(cell.eval,
|
|
1143
|
+
...reasonsFor(cell.eval, result)
|
|
1118
1144
|
};
|
|
1119
1145
|
}
|
|
1120
1146
|
function parameterLabel(parameters) {
|
|
1121
1147
|
return Object.entries(parameters).map(([key, item]) => `${key}=${typeof item === "string" ? item : JSON.stringify(item)}`).join(" ");
|
|
1122
1148
|
}
|
|
1123
|
-
/** One printed line for a finished
|
|
1124
|
-
function
|
|
1149
|
+
/** One printed line for a finished run; failing reasons on the same line. */
|
|
1150
|
+
function runLine(scenario) {
|
|
1125
1151
|
const mark = scenario.outcome === "passed" ? green("✓") : scenario.outcome === "cancelled" ? yellow("◌") : red("✗");
|
|
1126
1152
|
const parameters = parameterLabel(scenario.parameters);
|
|
1127
|
-
const
|
|
1128
|
-
const
|
|
1153
|
+
const batch = scenario.result.batch;
|
|
1154
|
+
const position = batch ? dim(` #${batch.index + 1}/${batch.size}`) : "";
|
|
1129
1155
|
const details = [
|
|
1130
1156
|
...scenario.outcome === "failed" ? scenario.reasons : [],
|
|
1131
1157
|
...scenario.outcome === "cancelled" ? ["cancelled"] : [],
|
|
1132
1158
|
...scenario.skipped.map((name) => `${name} skipped`)
|
|
1133
1159
|
];
|
|
1134
|
-
return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${
|
|
1160
|
+
return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${position}${details.length ? ` ${dim(details.join(" · "))}` : ""}`;
|
|
1135
1161
|
}
|
|
1136
1162
|
/**
|
|
1137
1163
|
* Prints run progress. On a terminal, failures print as they happen and a live
|
|
@@ -1177,29 +1203,29 @@ function createRunReporter(options) {
|
|
|
1177
1203
|
timer.unref?.();
|
|
1178
1204
|
}
|
|
1179
1205
|
},
|
|
1180
|
-
onEvent(
|
|
1181
|
-
if (event.source === "runner" && event.kind === "
|
|
1182
|
-
running.set(
|
|
1183
|
-
label: options.labelFor(
|
|
1206
|
+
onEvent(run, event) {
|
|
1207
|
+
if (event.source === "runner" && event.kind === "run-started") {
|
|
1208
|
+
running.set(run.runId, {
|
|
1209
|
+
label: options.labelFor(run),
|
|
1184
1210
|
startedAt: now()
|
|
1185
1211
|
});
|
|
1186
1212
|
return;
|
|
1187
1213
|
}
|
|
1188
|
-
const activity = running.get(
|
|
1214
|
+
const activity = running.get(run.runId);
|
|
1189
1215
|
if (!activity) return;
|
|
1190
|
-
if (event.source === "runner" && (event.kind === "
|
|
1216
|
+
if (event.source === "runner" && (event.kind === "run-completed" || event.kind === "error")) running.delete(run.runId);
|
|
1191
1217
|
else if (event.source === "aut" && event.kind === "tool-call") activity.doing = `tool ${event.name}`;
|
|
1192
1218
|
else if (event.source === "runner" && event.kind === "scorer-started") activity.doing = `scoring ${event.scorer}`;
|
|
1193
1219
|
else if (event.source === "runner" && event.kind === "checkpoint-started") activity.doing = `check ${event.name}`;
|
|
1194
1220
|
},
|
|
1195
|
-
|
|
1221
|
+
onRun(scenario) {
|
|
1196
1222
|
scenarios.push(scenario);
|
|
1197
|
-
|
|
1198
|
-
if (!options.interactive || options.verbose || scenario.outcome !== "passed") print(
|
|
1223
|
+
running.delete(scenario.result.runId);
|
|
1224
|
+
if (!options.interactive || options.verbose || scenario.outcome !== "passed") print(runLine(scenario));
|
|
1199
1225
|
},
|
|
1200
1226
|
/** Called on the first Ctrl+C. */
|
|
1201
1227
|
cancelling() {
|
|
1202
|
-
print(`\n ${yellow("◌")} Cancelling: finishing running
|
|
1228
|
+
print(`\n ${yellow("◌")} Cancelling: finishing running runs. Press Ctrl+C again to exit now.`);
|
|
1203
1229
|
},
|
|
1204
1230
|
finish(durationMs) {
|
|
1205
1231
|
if (timer) clearInterval(timer);
|
|
@@ -1208,11 +1234,12 @@ function createRunReporter(options) {
|
|
|
1208
1234
|
passed: scenarios.filter((s) => s.outcome === "passed").length,
|
|
1209
1235
|
failed: scenarios.filter((s) => s.outcome === "failed").length,
|
|
1210
1236
|
cancelled: scenarios.filter((s) => s.outcome === "cancelled").length,
|
|
1211
|
-
|
|
1237
|
+
runs: scenarios.length,
|
|
1238
|
+
scenarios: new Set(scenarios.map((s) => `${s.label} ${parameterLabel(s.parameters)}`)).size,
|
|
1212
1239
|
durationMs
|
|
1213
1240
|
};
|
|
1214
1241
|
},
|
|
1215
|
-
|
|
1242
|
+
runs: () => scenarios
|
|
1216
1243
|
};
|
|
1217
1244
|
}
|
|
1218
1245
|
function totalsLine(totals) {
|
|
@@ -1221,7 +1248,8 @@ function totalsLine(totals) {
|
|
|
1221
1248
|
totals.failed ? red(`${totals.failed} failed`) : "",
|
|
1222
1249
|
totals.cancelled ? yellow(`${totals.cancelled} cancelled`) : ""
|
|
1223
1250
|
].filter(Boolean);
|
|
1224
|
-
|
|
1251
|
+
const coverage = totals.runs === totals.scenarios ? plural(totals.runs, "run") : `${plural(totals.runs, "run")} of ${plural(totals.scenarios, "scenario")}`;
|
|
1252
|
+
return ` ${bold(parts.join(" · "))} ${dim(`· ${coverage} in ${duration(totals.durationMs)}`)}`;
|
|
1225
1253
|
}
|
|
1226
1254
|
//#endregion
|
|
1227
1255
|
//#region ../cli/src/junit.ts
|
|
@@ -1231,8 +1259,8 @@ function escape(text) {
|
|
|
1231
1259
|
const seconds = (ms) => ((ms ?? 0) / 1e3).toFixed(3);
|
|
1232
1260
|
/**
|
|
1233
1261
|
* A JUnit XML report: one test suite per EvalKit suite (`default` for
|
|
1234
|
-
* scenarios without one) and one test case per
|
|
1235
|
-
*
|
|
1262
|
+
* scenarios without one) and one test case per run, named after its scenario
|
|
1263
|
+
* and parameters, and its place in a batch (` #2`) when repeated.
|
|
1236
1264
|
*/
|
|
1237
1265
|
function junitReport(scenarios) {
|
|
1238
1266
|
const bySuite = /* @__PURE__ */ new Map();
|
|
@@ -1246,7 +1274,8 @@ function junitReport(scenarios) {
|
|
|
1246
1274
|
const time = cases.reduce((sum, c) => sum + (c.durationMs ?? 0), 0);
|
|
1247
1275
|
const testcases = cases.map((scenario) => {
|
|
1248
1276
|
const parameters = parameterLabel(scenario.parameters);
|
|
1249
|
-
const
|
|
1277
|
+
const batch = scenario.result.batch;
|
|
1278
|
+
const attributes = `name="${escape((parameters ? `${scenario.evalId} [${parameters}]` : scenario.evalId) + (batch ? ` #${batch.index + 1}` : ""))}" classname="${escape(suite)}" time="${seconds(scenario.durationMs)}"`;
|
|
1250
1279
|
if (scenario.outcome === "failed") return ` <testcase ${attributes}>\n <failure message="${escape(scenario.reasons.join("; ") || "failed")}">${escape(scenario.reasons.join("\n"))}</failure>\n </testcase>`;
|
|
1251
1280
|
if (scenario.outcome === "cancelled") return ` <testcase ${attributes}>\n <skipped message="cancelled"/>\n </testcase>`;
|
|
1252
1281
|
if (allSkipped(scenario)) return ` <testcase ${attributes}>\n <skipped message="${escape(`all scorers skipped: ${scenario.skipped.join(", ")}`)}"/>\n </testcase>`;
|
|
@@ -1256,15 +1285,15 @@ function junitReport(scenarios) {
|
|
|
1256
1285
|
});
|
|
1257
1286
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites name="evalkit" tests="${scenarios.length}" failures="${scenarios.filter((c) => c.outcome === "failed").length}">\n${suites.join("\n")}\n</testsuites>\n`;
|
|
1258
1287
|
}
|
|
1259
|
-
/** True when every score
|
|
1288
|
+
/** True when every score was skip()ped. */
|
|
1260
1289
|
function allSkipped(scenario) {
|
|
1261
|
-
const results =
|
|
1290
|
+
const results = scenario.result.scoring?.results ?? [];
|
|
1262
1291
|
return results.length > 0 && results.every((score) => score.skipped);
|
|
1263
1292
|
}
|
|
1264
1293
|
//#endregion
|
|
1265
1294
|
//#region ../cli/src/version.ts
|
|
1266
1295
|
/** The CLI version; kept in sync with package.json when releasing. */
|
|
1267
|
-
const VERSION = "0.
|
|
1296
|
+
const VERSION = "0.10.0";
|
|
1268
1297
|
//#endregion
|
|
1269
1298
|
//#region ../cli/src/run-command.ts
|
|
1270
1299
|
/** Validates `run` flags into a selection, parameter overrides and limits. */
|
|
@@ -1291,28 +1320,27 @@ function normalizeRunOptions(values, positionals) {
|
|
|
1291
1320
|
const index = value.indexOf("=");
|
|
1292
1321
|
if (index < 1) throw new Error("--select requires axis=value");
|
|
1293
1322
|
const axis = value.slice(0, index);
|
|
1294
|
-
|
|
1323
|
+
const raw = value.slice(index + 1);
|
|
1324
|
+
let choices;
|
|
1295
1325
|
try {
|
|
1296
|
-
|
|
1326
|
+
choices = [JSON.parse(raw)];
|
|
1297
1327
|
} catch {
|
|
1298
|
-
|
|
1328
|
+
choices = raw.split(",").filter(Boolean);
|
|
1299
1329
|
}
|
|
1300
|
-
(selection[axis] ??= []).push(
|
|
1330
|
+
(selection[axis] ??= []).push(...choices);
|
|
1301
1331
|
}
|
|
1302
1332
|
for (const value of values.param ?? []) {
|
|
1303
1333
|
const index = value.indexOf("=");
|
|
1304
1334
|
if (index < 1) throw new Error("--param requires name=JSON");
|
|
1305
1335
|
parameters[value.slice(0, index)] = JSON.parse(value.slice(index + 1));
|
|
1306
1336
|
}
|
|
1307
|
-
if (values.suite && values.matrix) throw new Error("--suite and --matrix cannot be used together");
|
|
1308
|
-
if ((values.suite || values.matrix) && positionals.length) throw new Error("Use --eval to select evals with --suite or --matrix");
|
|
1309
1337
|
return {
|
|
1310
1338
|
values,
|
|
1311
1339
|
positionals,
|
|
1312
1340
|
parameters,
|
|
1313
1341
|
selection,
|
|
1314
1342
|
concurrency: integer("concurrency"),
|
|
1315
|
-
|
|
1343
|
+
repeat: integer("repeat")
|
|
1316
1344
|
};
|
|
1317
1345
|
}
|
|
1318
1346
|
/** Load errors that affect what was asked for, so the run must not pretend it covered them. */
|
|
@@ -1333,35 +1361,32 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1333
1361
|
const project = await loadProject(cwd, values.config);
|
|
1334
1362
|
process.chdir(project.root);
|
|
1335
1363
|
const registry = project.registry;
|
|
1336
|
-
const requestedEvals = values.eval
|
|
1364
|
+
const requestedEvals = [...values.eval ?? [], ...positionals].flatMap((value) => value.split(",").filter(Boolean));
|
|
1337
1365
|
let matrix;
|
|
1338
1366
|
let suiteId;
|
|
1339
1367
|
const quiet = Boolean(values.json || values["dry-run"]);
|
|
1340
1368
|
if (!quiet) banner("run", VERSION);
|
|
1369
|
+
const suite = values.suite ? registry.suites.find((item) => item.id === values.suite) : void 0;
|
|
1341
1370
|
if (values.suite) {
|
|
1342
|
-
const suite = registry.suites.find((item) => item.id === values.suite);
|
|
1343
1371
|
const broken = project.loadErrors.filter((error) => error.suiteId === values.suite);
|
|
1344
1372
|
if (!suite && !broken.length) throw new Error(`Unknown suite: ${values.suite}`);
|
|
1345
1373
|
suiteId = values.suite;
|
|
1346
|
-
matrix = defineEvalMatrix({
|
|
1347
|
-
id: "default",
|
|
1348
|
-
evals: suite?.evals ?? [],
|
|
1349
|
-
parameters: project.config.matrix?.parameters ?? {},
|
|
1350
|
-
defaults: project.config.matrix?.defaults,
|
|
1351
|
-
cases: project.config.matrix?.cases,
|
|
1352
|
-
exclude: project.config.matrix?.exclude
|
|
1353
|
-
});
|
|
1354
|
-
} else if (values.matrix) {
|
|
1355
|
-
matrix = registry.matrices.find((item) => item.id === values.matrix);
|
|
1356
|
-
if (!matrix) throw new Error(`Unknown matrix: ${values.matrix}`);
|
|
1357
|
-
} else {
|
|
1358
|
-
requestedEvals.push(...positionals.flatMap((value) => value.split(",")));
|
|
1359
|
-
matrix = project.config.matrix ? registry.matrices.at(-1) : defineEvalMatrix({
|
|
1360
|
-
id: "default",
|
|
1361
|
-
evals: registry.evals,
|
|
1362
|
-
parameters: {}
|
|
1363
|
-
});
|
|
1364
1374
|
}
|
|
1375
|
+
const configured = values.matrix ? registry.matrices.find((item) => item.id === values.matrix) : project.config.matrix ? registry.matrices.at(-1) : void 0;
|
|
1376
|
+
if (values.matrix && !configured) throw new Error(`Unknown matrix: ${values.matrix}`);
|
|
1377
|
+
if (configured && suite) {
|
|
1378
|
+
const inSuite = new Set(suite.evals.map(authoringId));
|
|
1379
|
+
const both = configured.evals.map(authoringId).filter((id) => inSuite.has(id));
|
|
1380
|
+
if (!both.length) throw new Error(`Matrix ${authoringId(configured)} has no scenarios from suite ${suite.id}`);
|
|
1381
|
+
const outside = requestedEvals.filter((id) => !both.includes(id));
|
|
1382
|
+
if (outside.length) throw new Error(`Not in both matrix ${authoringId(configured)} and suite ${suite.id}: ${outside.join(", ")}`);
|
|
1383
|
+
if (!requestedEvals.length) requestedEvals.push(...both);
|
|
1384
|
+
}
|
|
1385
|
+
matrix = configured ?? defineEvalMatrix({
|
|
1386
|
+
id: "default",
|
|
1387
|
+
evals: values.suite ? suite?.evals ?? [] : registry.evals,
|
|
1388
|
+
parameters: {}
|
|
1389
|
+
});
|
|
1365
1390
|
const broken = brokenSelection(project.loadErrors, {
|
|
1366
1391
|
...suiteId ? { suiteId } : {},
|
|
1367
1392
|
evalIds: requestedEvals,
|
|
@@ -1385,14 +1410,18 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1385
1410
|
else throw new Error(`Unknown matrix axis: ${axis}`);
|
|
1386
1411
|
const cells = matrix.count(selection);
|
|
1387
1412
|
if (!cells) throw new Error("Selection contains no eligible cells; check cases and exclusions");
|
|
1388
|
-
const
|
|
1413
|
+
const repeat = parsed.repeat ?? project.config.execution?.repeat;
|
|
1414
|
+
let runs = 0;
|
|
1415
|
+
for (const cell of matrix.cells(selection)) runs += repeat ?? cell.eval.repeat ?? 1;
|
|
1389
1416
|
const plan = {
|
|
1390
1417
|
matrix: authoringId(matrix),
|
|
1391
1418
|
evals: requestedEvals.length ? requestedEvals : matrix.evals.map((definition) => definition.id),
|
|
1392
1419
|
cells,
|
|
1420
|
+
runs,
|
|
1393
1421
|
parameters: selection.parameters,
|
|
1394
1422
|
overrides: selection.overrides,
|
|
1395
|
-
|
|
1423
|
+
repeat: repeat ?? "scenario",
|
|
1424
|
+
scoring: Object.fromEntries(registry.catalog({ root: project.root }).filter((entry) => matrix.evals.some((definition) => definition.id === entry.id)).map((entry) => [entry.id, plannedChecks(entry)]))
|
|
1396
1425
|
};
|
|
1397
1426
|
if (values["dry-run"]) {
|
|
1398
1427
|
console.log(JSON.stringify(plan, null, 2));
|
|
@@ -1401,7 +1430,9 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1401
1430
|
const sweeps = Object.keys(matrix.parameters).length > 0 || Boolean(matrix.cases?.length);
|
|
1402
1431
|
if (sweeps && cells > (project.config.execution?.maxCells ?? 100) && !values.all) throw new Error(`Selected ${cells} cells. Narrow the selection, inspect --dry-run, or pass --all.`);
|
|
1403
1432
|
const reportDir = resolve(project.root, project.config.reportDir ?? "_evalkit-results");
|
|
1404
|
-
const
|
|
1433
|
+
const sandbox = sandboxLocation(project);
|
|
1434
|
+
const sandboxDir = sandbox.path;
|
|
1435
|
+
if (sandbox.inside) console.error(` ${yellow("!")} ${SANDBOX_INSIDE_WARNING}`);
|
|
1405
1436
|
const source = await detectSource(project.root, { outputDirs: [
|
|
1406
1437
|
reportDir,
|
|
1407
1438
|
sandboxDir,
|
|
@@ -1409,14 +1440,15 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1409
1440
|
] });
|
|
1410
1441
|
const suiteOf = new Map(registry.suites.flatMap((suite) => suite.evals.map((evaluation) => [authoringId(evaluation), suite.id])));
|
|
1411
1442
|
const suiteFor = (evalId) => suiteId ?? suiteOf.get(evalId);
|
|
1412
|
-
if (!values.json) console.log(` Running ${plural(cells, "scenario")}${sweeps ? ` from matrix ${plan.matrix}` : ""}${suiteId ? ` from suite ${suiteId}` : ""}` + (source ? ` at ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : " (untracked: not in a git repository)") + "\n");
|
|
1443
|
+
if (!values.json) console.log(` Running ${runs === cells ? plural(cells, "scenario") : `${plural(runs, "run")} of ${plural(cells, "scenario")}`}${sweeps ? ` from matrix ${plan.matrix}` : ""}${suiteId ? ` from suite ${suiteId}` : ""}` + (source ? ` at ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : " (untracked: not in a git repository)") + "\n");
|
|
1413
1444
|
const reporter = createRunReporter({
|
|
1414
1445
|
interactive: interactive && !values.json,
|
|
1415
1446
|
verbose: Boolean(values.verbose),
|
|
1416
|
-
total:
|
|
1417
|
-
labelFor: (
|
|
1418
|
-
const suite = suiteFor(
|
|
1419
|
-
|
|
1447
|
+
total: runs,
|
|
1448
|
+
labelFor: (run) => {
|
|
1449
|
+
const suite = suiteFor(run.evalId);
|
|
1450
|
+
const label = suite ? `${suite}/${run.evalId}` : run.evalId;
|
|
1451
|
+
return run.batch ? `${label} #${run.batch.index + 1}/${run.batch.size}` : label;
|
|
1420
1452
|
},
|
|
1421
1453
|
write: values.json ? () => {} : void 0
|
|
1422
1454
|
});
|
|
@@ -1433,7 +1465,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1433
1465
|
summary = await Effect.runPromise(runnerLogs(runMatrix(matrix, {
|
|
1434
1466
|
selection,
|
|
1435
1467
|
concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
|
|
1436
|
-
|
|
1468
|
+
...repeat ? { repeat } : {},
|
|
1437
1469
|
...suiteId ? { suiteId } : {},
|
|
1438
1470
|
suiteFor: (cell) => suiteFor(authoringId(cell.eval)),
|
|
1439
1471
|
...values.local ? { runtime: "local" } : {},
|
|
@@ -1443,20 +1475,24 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1443
1475
|
workspaceRoot: sandboxDir,
|
|
1444
1476
|
keepWorkspaces: project.config.keepSandboxes,
|
|
1445
1477
|
artifactExclude: project.config.artifacts?.exclude,
|
|
1478
|
+
attachmentLimits: project.config.artifacts?.attachments,
|
|
1479
|
+
judge: project.config.judge,
|
|
1446
1480
|
signal: controller.signal,
|
|
1447
|
-
onEvent: (
|
|
1448
|
-
onResult: (cell,
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1481
|
+
onEvent: (run, event) => reporter.onEvent(run, event),
|
|
1482
|
+
onResult: (cell, results) => {
|
|
1483
|
+
for (const result of results) {
|
|
1484
|
+
const report = runReport(cell, result, suiteFor(authoringId(cell.eval)));
|
|
1485
|
+
if (values.json) console.log(JSON.stringify({
|
|
1486
|
+
cell: {
|
|
1487
|
+
key: cell.key,
|
|
1488
|
+
eval: authoringId(cell.eval),
|
|
1489
|
+
parameters: cell.parameters
|
|
1490
|
+
},
|
|
1491
|
+
outcome: report.outcome,
|
|
1492
|
+
result
|
|
1493
|
+
}));
|
|
1494
|
+
reporter.onRun(report);
|
|
1495
|
+
}
|
|
1460
1496
|
}
|
|
1461
1497
|
})));
|
|
1462
1498
|
} finally {
|
|
@@ -1466,7 +1502,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1466
1502
|
if (values.junit) {
|
|
1467
1503
|
const file = resolve(cwd, values.junit);
|
|
1468
1504
|
await mkdir(dirname(file), { recursive: true });
|
|
1469
|
-
await writeFile(file, junitReport(reporter.
|
|
1505
|
+
await writeFile(file, junitReport(reporter.runs()));
|
|
1470
1506
|
}
|
|
1471
1507
|
if (values.json) console.log(JSON.stringify({ summary: totals }));
|
|
1472
1508
|
else {
|
|
@@ -1479,6 +1515,23 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1479
1515
|
if (controller.signal.aborted) process.exitCode = 130;
|
|
1480
1516
|
else if (summary.failed || broken.length) process.exitCode = 1;
|
|
1481
1517
|
}
|
|
1518
|
+
/** A scenario's checkpoints and final scorers, as `--dry-run` lists them. */
|
|
1519
|
+
function plannedChecks(entry) {
|
|
1520
|
+
return [...entry.transcript.flatMap((step, index) => step.kind === "checkpoint" ? [{
|
|
1521
|
+
scorer: step.scorer,
|
|
1522
|
+
at: `step ${index}`
|
|
1523
|
+
}] : []), ...entry.scorers.map((scorer) => ({
|
|
1524
|
+
scorer,
|
|
1525
|
+
at: "final"
|
|
1526
|
+
}))].map(({ scorer, at }) => ({
|
|
1527
|
+
name: scorer.name,
|
|
1528
|
+
kind: scorer.kind,
|
|
1529
|
+
at,
|
|
1530
|
+
...scorer.description ? { description: scorer.description } : {},
|
|
1531
|
+
passes: passMark(scorer.passAt, scorer.answer),
|
|
1532
|
+
...scorer.agent ? { agent: scorer.agent.name ?? scorer.agent.id ?? scorer.agent.kind } : {}
|
|
1533
|
+
}));
|
|
1534
|
+
}
|
|
1482
1535
|
//#endregion
|
|
1483
1536
|
//#region ../cli/src/tidy.ts
|
|
1484
1537
|
/**
|
|
@@ -1498,7 +1551,7 @@ function projectEvalIds(registry) {
|
|
|
1498
1551
|
*/
|
|
1499
1552
|
async function tidyReports(options) {
|
|
1500
1553
|
const known = projectEvalIds(options.registry);
|
|
1501
|
-
const reports = createDashboardReportReader(options.reportRoot
|
|
1554
|
+
const reports = createDashboardReportReader(options.reportRoot);
|
|
1502
1555
|
const result = {
|
|
1503
1556
|
archived: [],
|
|
1504
1557
|
restored: [],
|
|
@@ -1529,19 +1582,19 @@ async function tidyReports(options) {
|
|
|
1529
1582
|
return result;
|
|
1530
1583
|
}
|
|
1531
1584
|
/**
|
|
1532
|
-
* Remove the
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1585
|
+
* Remove the run workspaces `keep` would not have kept: those of passed runs
|
|
1586
|
+
* by default. Unfinished runs are never touched, so a run in progress keeps
|
|
1587
|
+
* its workspace; workspaces without a report are left alone.
|
|
1535
1588
|
*/
|
|
1536
1589
|
async function tidySandboxes(options) {
|
|
1537
1590
|
const keep = options.keep ?? "failed";
|
|
1538
1591
|
if (keep === "all") return { removed: 0 };
|
|
1539
|
-
const reports = createDashboardReportReader(options.reportRoot
|
|
1592
|
+
const reports = createDashboardReportReader(options.reportRoot);
|
|
1540
1593
|
let removed = 0;
|
|
1541
|
-
for (const run of await reports.listRuns())
|
|
1542
|
-
if (
|
|
1543
|
-
if (keep === "failed" &&
|
|
1544
|
-
const workspace =
|
|
1594
|
+
for (const run of await reports.listRuns()) {
|
|
1595
|
+
if (run.status === "running") continue;
|
|
1596
|
+
if (keep === "failed" && run.status !== "passed") continue;
|
|
1597
|
+
const workspace = runWorkspacePath(options.sandboxRoot, run.id);
|
|
1545
1598
|
if (!await stat(workspace).catch(() => void 0)) continue;
|
|
1546
1599
|
await rm(workspace, {
|
|
1547
1600
|
recursive: true,
|
|
@@ -1601,11 +1654,22 @@ function noRunsMessage(selection, head, options) {
|
|
|
1601
1654
|
if (selection.running) return `No finished runs for ${at}: ${plural(selection.running, "run")} still running. Share again when they finish.`;
|
|
1602
1655
|
return `No runs for ${at}. Run \`evalkit run\` first${options.all ? "" : ", or pass --all to share runs from other commits"}.`;
|
|
1603
1656
|
}
|
|
1604
|
-
/**
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1657
|
+
/**
|
|
1658
|
+
* A share server API URL. The server may live below a path
|
|
1659
|
+
* (`https://example.com/evals`), so paths resolve below `share.url`.
|
|
1660
|
+
*/
|
|
1661
|
+
function shareApiUrl(serverUrl, path) {
|
|
1662
|
+
return new URL(path, serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Where to look after sharing: the commit, or the repository if several
|
|
1666
|
+
* commits were shared. Both come from a run URL the server returned
|
|
1667
|
+
* (`<repo pages>/-/<sha>/runs/<id>`), since only the server knows its layout.
|
|
1668
|
+
*/
|
|
1669
|
+
function shareTarget(serverUrl, runUrl, runs) {
|
|
1670
|
+
const commitUrl = runUrl?.replace(/\/runs\/[^/]+\/?$/, "");
|
|
1671
|
+
if (!commitUrl || commitUrl === runUrl) return serverUrl;
|
|
1672
|
+
return new Set(runs.map((run) => run.source.commit)).size === 1 ? commitUrl : commitUrl.replace(/\/-\/[0-9a-f]{40}$/, "") || serverUrl;
|
|
1609
1673
|
}
|
|
1610
1674
|
/** Build one run's upload bundle: run.json, catalog.json, and its snapshot. */
|
|
1611
1675
|
async function runBundle(options) {
|
|
@@ -1613,7 +1677,10 @@ async function runBundle(options) {
|
|
|
1613
1677
|
const encode = (value) => new TextEncoder().encode(`${JSON.stringify(value)}\n`);
|
|
1614
1678
|
files.push({
|
|
1615
1679
|
path: "run.json",
|
|
1616
|
-
data: encode(
|
|
1680
|
+
data: encode({
|
|
1681
|
+
snapshotVersion: 2,
|
|
1682
|
+
...options.run
|
|
1683
|
+
})
|
|
1617
1684
|
});
|
|
1618
1685
|
const saved = await readFile(resolve(options.reportRoot, options.run.id, "catalog.json")).catch(() => void 0);
|
|
1619
1686
|
files.push({
|
|
@@ -1658,7 +1725,7 @@ async function resolveShareHeaders(headers) {
|
|
|
1658
1725
|
}
|
|
1659
1726
|
/** Check that the URL is an EvalKit share server and the headers get through. */
|
|
1660
1727
|
async function checkShareServer(serverUrl, headers) {
|
|
1661
|
-
const response = await fetch(
|
|
1728
|
+
const response = await fetch(shareApiUrl(serverUrl, "api/v1/repos"), {
|
|
1662
1729
|
headers,
|
|
1663
1730
|
redirect: "manual"
|
|
1664
1731
|
});
|
|
@@ -1681,7 +1748,7 @@ async function uploadBundle(serverUrl, bundle, options = {}) {
|
|
|
1681
1748
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
|
|
1682
1749
|
}
|
|
1683
1750
|
async function postBundle(serverUrl, bundle, headers) {
|
|
1684
|
-
const response = await fetch(
|
|
1751
|
+
const response = await fetch(shareApiUrl(serverUrl, "api/v1/uploads"), {
|
|
1685
1752
|
method: "POST",
|
|
1686
1753
|
headers: {
|
|
1687
1754
|
...headers,
|
|
@@ -1716,7 +1783,7 @@ async function expectShareServer(response, serverUrl) {
|
|
|
1716
1783
|
if (denied || response.ok && !type.includes("json")) throw new ShareServerError(`${new URL(serverUrl).host} did not answer as an EvalKit share server (${response.status}${type ? `, ${type.split(";")[0]}` : ""}). ` + (denied ? "Check share.headers." : "Check share.url."));
|
|
1717
1784
|
}
|
|
1718
1785
|
async function shareRuns(options) {
|
|
1719
|
-
const reports = createDashboardReportReader(options.reportRoot
|
|
1786
|
+
const reports = createDashboardReportReader(options.reportRoot);
|
|
1720
1787
|
return options.step("Uploading", async (progress) => {
|
|
1721
1788
|
const result = {
|
|
1722
1789
|
created: 0,
|
|
@@ -1737,6 +1804,7 @@ async function shareRuns(options) {
|
|
|
1737
1804
|
const uploaded = await uploadBundle(options.serverUrl, bundle, { headers: options.headers });
|
|
1738
1805
|
result.bytes += bundle.byteLength;
|
|
1739
1806
|
result[uploaded.status] += 1;
|
|
1807
|
+
result.url ??= uploaded.url;
|
|
1740
1808
|
} catch (error) {
|
|
1741
1809
|
if (error instanceof ShareServerError) throw error;
|
|
1742
1810
|
result.failed.push({
|
|
@@ -1789,7 +1857,7 @@ export default defineConfig({
|
|
|
1789
1857
|
"datasets/greeting/greeting.txt": "Hello\n",
|
|
1790
1858
|
"datasets/greeting.dataset.ts": `import { defineDataset } from '@leostera/evalkit';
|
|
1791
1859
|
|
|
1792
|
-
// Files the agent can read: copied into each
|
|
1860
|
+
// Files the agent can read: copied into each run's workspace under greeting/.
|
|
1793
1861
|
// Paths are relative to the project root.
|
|
1794
1862
|
export const greeting = defineDataset({
|
|
1795
1863
|
id: 'greeting',
|
|
@@ -1865,18 +1933,17 @@ bun run evalkit build # write a read-only, shareable s
|
|
|
1865
1933
|
Coding agents can use the EvalKit skills that ship with the package, in
|
|
1866
1934
|
\`node_modules/@leostera/evalkit/dist/skills/\` (start with \`evalkit/SKILL.md\`).
|
|
1867
1935
|
|
|
1868
|
-
EvalKit installs from npm. Reports,
|
|
1936
|
+
EvalKit installs from npm. Reports, run workspaces, and dashboard snapshots stay local
|
|
1869
1937
|
and are ignored by Git. A snapshot in \`_evalkit-dashboard/\` contains report messages,
|
|
1870
1938
|
scores, evidence, and candidate files (never evaluator workspaces): review it before sharing.
|
|
1871
1939
|
`;
|
|
1872
1940
|
}
|
|
1873
1941
|
//#endregion
|
|
1874
1942
|
//#region ../cli/src/new-project.ts
|
|
1875
|
-
const npmDependency = "^0.
|
|
1943
|
+
const npmDependency = "^0.10.0";
|
|
1876
1944
|
const ignored = [
|
|
1877
1945
|
"node_modules/",
|
|
1878
1946
|
"_evalkit-results/",
|
|
1879
|
-
"_evalkit-sandbox/",
|
|
1880
1947
|
"_evalkit-dashboard/",
|
|
1881
1948
|
"_evalkit-share/"
|
|
1882
1949
|
];
|
|
@@ -1996,24 +2063,59 @@ const FINISHED = /* @__PURE__ */ new Set([
|
|
|
1996
2063
|
function unitKey(run) {
|
|
1997
2064
|
return `${run.evalId}\0${canonicalParameters(run.parameters ?? {})}`;
|
|
1998
2065
|
}
|
|
1999
|
-
/**
|
|
2066
|
+
/**
|
|
2067
|
+
* What represents each unit: the latest finished run's batch (its finished
|
|
2068
|
+
* runs), or that run alone; else the latest run at all.
|
|
2069
|
+
*/
|
|
2000
2070
|
function latestByUnit(runs) {
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
const ordered = [...runs].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2071
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2072
|
+
const ordered = [...runs].filter((run) => run.status !== "running").sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2004
2073
|
for (const run of ordered) {
|
|
2005
|
-
if (run.status === "running") continue;
|
|
2006
2074
|
const key = unitKey(run);
|
|
2007
|
-
const current =
|
|
2008
|
-
if (!current)
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2075
|
+
const current = latest.get(key);
|
|
2076
|
+
if (!current || !FINISHED.has(current.status) && FINISHED.has(run.status)) latest.set(key, run);
|
|
2077
|
+
}
|
|
2078
|
+
const byUnit = /* @__PURE__ */ new Map();
|
|
2079
|
+
let used = 0;
|
|
2080
|
+
for (const [key, run] of latest) {
|
|
2081
|
+
const members = run.batch ? ordered.filter((other) => other.batch?.id === run.batch.id && FINISHED.has(other.status)) : [run];
|
|
2082
|
+
used += members.length;
|
|
2083
|
+
byUnit.set(key, side(members));
|
|
2013
2084
|
}
|
|
2014
2085
|
return {
|
|
2015
2086
|
byUnit,
|
|
2016
|
-
ignored
|
|
2087
|
+
ignored: ordered.length - used
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
function side(runs) {
|
|
2091
|
+
const scored = runs.filter((run) => run.status !== "errored");
|
|
2092
|
+
const passed = scored.filter((run) => run.status === "passed").length;
|
|
2093
|
+
const values = /* @__PURE__ */ new Map();
|
|
2094
|
+
for (const run of runs) for (const scorer of run.scorers ?? []) {
|
|
2095
|
+
const key = `${scorer.kind}\0${scorer.name}`;
|
|
2096
|
+
const entry = values.get(key) ?? {
|
|
2097
|
+
scorer,
|
|
2098
|
+
sum: 0,
|
|
2099
|
+
n: 0
|
|
2100
|
+
};
|
|
2101
|
+
if (scorer.value !== void 0 && run.status !== "errored") {
|
|
2102
|
+
entry.sum += scorer.value;
|
|
2103
|
+
entry.n += 1;
|
|
2104
|
+
}
|
|
2105
|
+
values.set(key, entry);
|
|
2106
|
+
}
|
|
2107
|
+
const first = runs[0];
|
|
2108
|
+
return {
|
|
2109
|
+
...first,
|
|
2110
|
+
status: !scored.length ? first.status : passed === runs.length ? "passed" : "failed",
|
|
2111
|
+
scorers: [...values.values()].map(({ scorer, sum, n }) => ({
|
|
2112
|
+
name: scorer.name,
|
|
2113
|
+
kind: scorer.kind,
|
|
2114
|
+
...n ? { value: sum / n } : {}
|
|
2115
|
+
})),
|
|
2116
|
+
passed,
|
|
2117
|
+
total: runs.length,
|
|
2118
|
+
runs: [...runs]
|
|
2017
2119
|
};
|
|
2018
2120
|
}
|
|
2019
2121
|
function scorerSignature(run) {
|
|
@@ -2030,8 +2132,9 @@ function classify(base, head) {
|
|
|
2030
2132
|
change: "not-comparable",
|
|
2031
2133
|
reason: "different scorers"
|
|
2032
2134
|
};
|
|
2033
|
-
|
|
2034
|
-
if (
|
|
2135
|
+
const rate = (side) => side.passed / side.total;
|
|
2136
|
+
if (rate(head) < rate(base)) return { change: "regressed" };
|
|
2137
|
+
if (rate(head) > rate(base)) return { change: "fixed" };
|
|
2035
2138
|
return { change: "unchanged" };
|
|
2036
2139
|
}
|
|
2037
2140
|
function label(run, suite) {
|
|
@@ -2056,8 +2159,8 @@ function suiteScorers(units) {
|
|
|
2056
2159
|
name: scorer.name,
|
|
2057
2160
|
kind: scorer.kind
|
|
2058
2161
|
});
|
|
2059
|
-
if (scorer.
|
|
2060
|
-
sides[side].set(key, [...sides[side].get(key) ?? [], scorer.
|
|
2162
|
+
if (scorer.value === void 0) continue;
|
|
2163
|
+
sides[side].set(key, [...sides[side].get(key) ?? [], scorer.value]);
|
|
2061
2164
|
}
|
|
2062
2165
|
}
|
|
2063
2166
|
for (const unit of units) {
|
|
@@ -2111,7 +2214,11 @@ function compareRuns(baseRuns, headRuns) {
|
|
|
2111
2214
|
const changedDatasets = /* @__PURE__ */ new Set();
|
|
2112
2215
|
for (const unit of units) {
|
|
2113
2216
|
if (!unit.base || !unit.head) continue;
|
|
2114
|
-
const digests = (
|
|
2217
|
+
const digests = (side) => {
|
|
2218
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2219
|
+
for (const run of side.runs) for (const dataset of run.datasets ?? []) byId.set(dataset.id, (byId.get(dataset.id) ?? /* @__PURE__ */ new Set()).add(dataset.digest));
|
|
2220
|
+
return new Map([...byId].map(([id, set]) => [id, [...set].sort().join(",")]));
|
|
2221
|
+
};
|
|
2115
2222
|
const before = digests(unit.base);
|
|
2116
2223
|
const after = digests(unit.head);
|
|
2117
2224
|
for (const id of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) if (before.get(id) !== after.get(id)) changedDatasets.add(id);
|
|
@@ -2120,10 +2227,10 @@ function compareRuns(baseRuns, headRuns) {
|
|
|
2120
2227
|
for (const unit of units) bySuite.set(unit.suite, [...bySuite.get(unit.suite) ?? [], unit]);
|
|
2121
2228
|
const suites = [...bySuite].map(([suite, members]) => {
|
|
2122
2229
|
const side = (pick) => {
|
|
2123
|
-
const
|
|
2230
|
+
const sides = members.flatMap((unit) => unit[pick] ? [unit[pick]] : []);
|
|
2124
2231
|
return {
|
|
2125
|
-
passed:
|
|
2126
|
-
total:
|
|
2232
|
+
passed: sides.reduce((sum, entry) => sum + entry.passed, 0),
|
|
2233
|
+
total: sides.reduce((sum, entry) => sum + entry.total, 0)
|
|
2127
2234
|
};
|
|
2128
2235
|
};
|
|
2129
2236
|
return {
|
|
@@ -2232,8 +2339,8 @@ function headRuns(runs, head, options) {
|
|
|
2232
2339
|
return runs.filter((run) => run.source !== void 0 && run.source.commit === head.commit && repoKey(run.source) === repoKey(head) && (!run.source.dirty || options.allowDirty) && !run.archived && run.status !== "running");
|
|
2233
2340
|
}
|
|
2234
2341
|
/** Base runs from a results directory, e.g. an artifact from the base pipeline. Dirty runs are never a base. */
|
|
2235
|
-
async function baseRunsFromDirectory(directory
|
|
2236
|
-
return (await createDashboardReportReader(directory
|
|
2342
|
+
async function baseRunsFromDirectory(directory) {
|
|
2343
|
+
return (await createDashboardReportReader(directory).listRuns()).filter((run) => !run.source?.dirty && !run.archived);
|
|
2237
2344
|
}
|
|
2238
2345
|
/** A full SHA, a unique prefix of at least 7 characters, or a ref's latest shared commit. */
|
|
2239
2346
|
function resolveSharedRef(commits, ref) {
|
|
@@ -2260,7 +2367,7 @@ async function getJson(url, serverUrl, headers) {
|
|
|
2260
2367
|
/** Base runs shared for `ref` of this repository; `undefined` when the base was never shared. */
|
|
2261
2368
|
async function baseRunsFromServer(options) {
|
|
2262
2369
|
const repo = repoKey(options.head);
|
|
2263
|
-
const api = (path) =>
|
|
2370
|
+
const api = (path) => shareApiUrl(options.serverUrl, `api/v1/repos/${repo}/-/${path}`);
|
|
2264
2371
|
const listed = await getJson(api("commits"), options.serverUrl, options.headers);
|
|
2265
2372
|
const commits = Array.isArray(listed?.commits) ? listed.commits : [];
|
|
2266
2373
|
const commit = resolveSharedRef(commits, options.ref);
|
|
@@ -5257,11 +5364,12 @@ function createProgram(actions) {
|
|
|
5257
5364
|
showGlobalOptions: false
|
|
5258
5365
|
}).addHelpText("after", `\nRun from your eval project's directory. Docs: https://evalkit.leostera.dev/docs/\n`);
|
|
5259
5366
|
program.command("new").description("create an eval project (never overwrites files)").argument("<directory>", "where to create it; use . for the current directory").addHelpText("after", examples(["bunx @leostera/evalkit new ./agent-evals", "evalkit new ."])).action((directory) => actions.create(directory));
|
|
5260
|
-
program.command("run").description("run scenarios and save the results in _evalkit-results/").argument("[eval-ids...]", "scenarios to run (default: all)").addOption(configOption()).option("-e, --eval <id>", "run this scenario; repeatable, or comma-separated", collect).option("-s, --suite <id>", "run
|
|
5367
|
+
program.command("run").description("run scenarios and save the results in _evalkit-results/").argument("[eval-ids...]", "scenarios to run (default: all)").addOption(configOption()).option("-e, --eval <id>", "run this scenario; repeatable, or comma-separated", collect).option("-s, --suite <id>", "run the scenarios in this suite; with -m, only those in both").option("-m, --matrix <id>", "run this matrix").option("--model <value>", "--select model=<value> for each comma-separated value", collect).option("--mode <value>", "--select mode=<value> for each comma-separated value", collect).option("-a, --select <axis=value>", "keep only these values of a matrix axis (a for axis); repeatable, comma-separated", collect).option("--param <name=json>", "set a parameter for every scenario; repeatable", collect).option("-n, --repeat <n>", "runs of each scenario, as one batch; overrides config and scenario").option("-j, --concurrency <n>", "matrix cells to run at once (default: 4)").option("--max-tokens <n>", "pass maxTokens to the agent in context.parameters").option("--turn-budget <n>", "pass turnBudget to the agent in context.parameters").option("--chat-timeout-ms <n>", "pass chatTimeoutMs to the agent in context.parameters").option("--local", "require the agent's local runtime").option("--all", "allow more matrix cells than execution.maxCells").option("--dry-run", "print the plan as JSON; run nothing").option("--json", "print one JSON result per matrix cell").option("--junit <file>", "write a JUnit XML report").option("--skip-broken", "run healthy scenarios when some files fail to load, then exit 1").option("-v, --verbose", "print every scenario, not only failures").addOption(logLevelOption()).addHelpText("after", examples([
|
|
5261
5368
|
"evalkit run",
|
|
5262
5369
|
"evalkit run greeting -n 5",
|
|
5263
5370
|
"evalkit run -s billing --junit _evalkit-results/junit.xml",
|
|
5264
|
-
"evalkit run -m models --model model-a,model-b --dry-run"
|
|
5371
|
+
"evalkit run -m models --model model-a,model-b --dry-run",
|
|
5372
|
+
"evalkit run -m models -s testing -a model=luna -a toolchain=riot"
|
|
5265
5373
|
])).action((evalIds, options) => {
|
|
5266
5374
|
setRunnerLogLevel(options.logLevel);
|
|
5267
5375
|
return actions.run(runInput(evalIds, options));
|
|
@@ -5275,11 +5383,12 @@ function createProgram(actions) {
|
|
|
5275
5383
|
return actions.build(options);
|
|
5276
5384
|
});
|
|
5277
5385
|
program.command("tidy").description("archive runs of scenarios no longer in the project, and remove sandboxes keepSandboxes would not keep").addOption(configOption()).action((options) => actions.tidy(options));
|
|
5386
|
+
program.command("upgrade").description("convert results saved by an older EvalKit to the current format, in place").addOption(configOption()).option("--dry-run", "show what would change without changing anything").action((options) => actions.upgrade(options));
|
|
5278
5387
|
program.command("share").description("upload this commit's results to a share server").addOption(configOption()).option("--server <url>", "share server to use instead of share.url in the config").option("--all", "also upload runs from other commits").option("--allow-dirty", "include runs made with uncommitted changes").option("--no-workspace", "leave out candidate file contents").addHelpText("after", examples(["evalkit share", "evalkit share --server https://evals.example.com"])).action((options) => actions.share({
|
|
5279
5388
|
...options,
|
|
5280
5389
|
all: Boolean(options.all),
|
|
5281
5390
|
allowDirty: Boolean(options.allowDirty)
|
|
5282
|
-
})).command("serve").description("run a share server locally, with no authentication").option("-d, --dir <dir>", "where to store uploads", "_evalkit-share").addOption(portOption(4318)).action((options) => actions.shareServe(options));
|
|
5391
|
+
})).command("serve").description("run a share server locally, with no authentication").option("-d, --dir <dir>", "where to store uploads", "_evalkit-share").addOption(portOption(4318)).option("--base-path <path>", "serve everything below this path, e.g. /evals").option("--repo <key>", "show and accept only this repository, e.g. github.com/acme/agents").action((options) => actions.shareServe(options));
|
|
5283
5392
|
program.command("compare").description("compare this commit's results with a base commit's").addOption(configOption()).option("-b, --base <ref>", "base branch, tag or commit, from the share server").option("--base-dir <dir>", "base results from a directory instead").option("--server <url>", "share server to use instead of share.url in the config").option("--fail-on <condition>", "exit 1 on regression, or score-drop=<amount>; repeatable", collect).option("--markdown <file>", "write the comparison as Markdown").option("--json", "print the comparison as JSON").option("--require-base", "exit 1 when the base has no results").option("--allow-dirty", "include runs made with uncommitted changes").addHelpText("after", examples(["evalkit compare -b main", "evalkit compare -b main --fail-on regression --markdown compare.md"])).action((options) => actions.compare({
|
|
5284
5393
|
...options,
|
|
5285
5394
|
failOn: options.failOn ?? [],
|
|
@@ -5303,7 +5412,7 @@ function runInput(evalIds, flags) {
|
|
|
5303
5412
|
"chat-timeout-ms": flags.chatTimeoutMs,
|
|
5304
5413
|
"turn-budget": flags.turnBudget,
|
|
5305
5414
|
concurrency: flags.concurrency,
|
|
5306
|
-
|
|
5415
|
+
repeat: flags.repeat,
|
|
5307
5416
|
json: flags.json,
|
|
5308
5417
|
"dry-run": flags.dryRun,
|
|
5309
5418
|
all: flags.all,
|
|
@@ -5317,7 +5426,7 @@ function runInput(evalIds, flags) {
|
|
|
5317
5426
|
//#region ../cli/src/index.ts
|
|
5318
5427
|
let projectRoot = process.cwd();
|
|
5319
5428
|
let reportRoot = resolve(projectRoot, "_evalkit-results");
|
|
5320
|
-
let sandboxRoot =
|
|
5429
|
+
let sandboxRoot = defaultSandboxDir(projectRoot);
|
|
5321
5430
|
let project;
|
|
5322
5431
|
async function loadRegistry() {
|
|
5323
5432
|
project ??= await loadProject(projectRoot);
|
|
@@ -5343,6 +5452,8 @@ async function runEvals(options) {
|
|
|
5343
5452
|
workspaceRoot: sandboxRoot,
|
|
5344
5453
|
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5345
5454
|
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5455
|
+
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5456
|
+
judge: project?.config?.judge,
|
|
5346
5457
|
suiteId: options.suiteId,
|
|
5347
5458
|
runtime: runtimeFor(evaluation),
|
|
5348
5459
|
concurrency: options.concurrency,
|
|
@@ -5362,14 +5473,14 @@ async function runProvenance(registry) {
|
|
|
5362
5473
|
catalog: runCatalog(registry, projectRoot)
|
|
5363
5474
|
};
|
|
5364
5475
|
}
|
|
5365
|
-
function registerMatrixRoutes(app, registry, dashboardMatrix,
|
|
5476
|
+
function registerMatrixRoutes(app, registry, dashboardMatrix, repeat) {
|
|
5366
5477
|
let matrixRunning = false;
|
|
5367
5478
|
app.get("/v1/matrix", (context) => context.json({ matrix: dashboardMatrix ? {
|
|
5368
5479
|
id: dashboardMatrix.id,
|
|
5369
5480
|
parameters: dashboardMatrix.parameters,
|
|
5370
5481
|
...dashboardMatrix.cases?.length || dashboardMatrix.exclude?.length ? { dimensions: [...Object.keys(dashboardMatrix.cases?.[0] ?? {}), ...Object.keys(dashboardMatrix.parameters)] } : {},
|
|
5371
5482
|
...dashboardMatrix.cases?.length || dashboardMatrix.exclude?.length ? { constrained: true } : {},
|
|
5372
|
-
...
|
|
5483
|
+
...repeat ? { repeat } : {}
|
|
5373
5484
|
} : null }));
|
|
5374
5485
|
app.get("/v1/matrix/cells", (context) => {
|
|
5375
5486
|
if (!dashboardMatrix) return context.text("Project has no matrix", 404);
|
|
@@ -5409,12 +5520,14 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
|
|
|
5409
5520
|
runProvenance(registry).then((provenance) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
|
|
5410
5521
|
selection,
|
|
5411
5522
|
concurrency: 1,
|
|
5412
|
-
|
|
5523
|
+
...repeat ? { repeat } : {},
|
|
5413
5524
|
...suiteId ? { suiteId } : {},
|
|
5414
5525
|
report: localReportStore(reportRoot),
|
|
5415
5526
|
workspaceRoot: sandboxRoot,
|
|
5416
5527
|
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5417
5528
|
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5529
|
+
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5530
|
+
judge: project?.config?.judge,
|
|
5418
5531
|
...provenance,
|
|
5419
5532
|
onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
|
|
5420
5533
|
})))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
@@ -5465,7 +5578,7 @@ function registerReportRoutes(app, registry, reports) {
|
|
|
5465
5578
|
if (!dataset) return context.text("Unknown dataset", 404);
|
|
5466
5579
|
const file = context.req.param("path");
|
|
5467
5580
|
try {
|
|
5468
|
-
return new Response(await staticDatasetFile(dataset, projectRoot, file), { headers:
|
|
5581
|
+
return new Response(await staticDatasetFile(dataset, projectRoot, file), { headers: UNTRUSTED_FILE });
|
|
5469
5582
|
} catch (error) {
|
|
5470
5583
|
return context.text(error instanceof Error ? error.message : "Unable to read dataset file", 404);
|
|
5471
5584
|
}
|
|
@@ -5477,44 +5590,44 @@ function registerReportRoutes(app, registry, reports) {
|
|
|
5477
5590
|
app.get("/v1/evals", (context) => context.json({ evals: registry.metadata() }));
|
|
5478
5591
|
app.get("/v1/suites", (context) => context.json({ suites: registry.suiteMetadata() }));
|
|
5479
5592
|
app.get("/v1/runs", async (context) => context.json({ runs: await reports.listRuns() }));
|
|
5480
|
-
app.get("/v1/runs/:runId
|
|
5593
|
+
app.get("/v1/runs/:runId", async (context) => {
|
|
5481
5594
|
try {
|
|
5482
|
-
return context.json(
|
|
5595
|
+
return context.json(await reports.runDetail(context.req.param("runId")));
|
|
5483
5596
|
} catch (error) {
|
|
5484
|
-
return context.text(error instanceof Error ? error.message : "
|
|
5597
|
+
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5485
5598
|
}
|
|
5486
5599
|
});
|
|
5487
|
-
app.get("/v1/runs/:runId/
|
|
5600
|
+
app.get("/v1/runs/:runId/workspace", async (context) => {
|
|
5488
5601
|
try {
|
|
5489
|
-
return context.json(await reports.
|
|
5602
|
+
return context.json(await reports.listCandidateWorkspace(context.req.param("runId")));
|
|
5490
5603
|
} catch (error) {
|
|
5491
|
-
return context.text(error instanceof Error ? error.message : "
|
|
5604
|
+
return context.text(error instanceof Error ? error.message : "Unable to list workspace", 400);
|
|
5492
5605
|
}
|
|
5493
5606
|
});
|
|
5494
|
-
app.get("/v1/runs/:runId/
|
|
5607
|
+
app.get("/v1/runs/:runId/workspace/:path{.+}", async (context) => {
|
|
5495
5608
|
try {
|
|
5496
|
-
return
|
|
5609
|
+
return await reports.candidateFile(context.req.param("runId"), context.req.param("path"));
|
|
5497
5610
|
} catch (error) {
|
|
5498
|
-
return context.text(error instanceof Error ? error.message : "Unable to
|
|
5611
|
+
return context.text(error instanceof Error ? error.message : "Unable to read workspace file", 400);
|
|
5499
5612
|
}
|
|
5500
5613
|
});
|
|
5501
|
-
app.get("/v1/runs/:runId/
|
|
5614
|
+
app.get("/v1/runs/:runId/attachments/:name{.+}", async (context) => {
|
|
5502
5615
|
try {
|
|
5503
|
-
return await reports.
|
|
5616
|
+
return await reports.attachmentFile(context.req.param("runId"), context.req.param("name"));
|
|
5504
5617
|
} catch (error) {
|
|
5505
|
-
return context.text(error instanceof Error ? error.message : "Unable to read
|
|
5618
|
+
return context.text(error instanceof Error ? error.message : "Unable to read attachment", 400);
|
|
5506
5619
|
}
|
|
5507
5620
|
});
|
|
5508
|
-
app.get("/v1/runs/:runId/
|
|
5621
|
+
app.get("/v1/runs/:runId/artifacts", async (context) => {
|
|
5509
5622
|
try {
|
|
5510
|
-
return context.json({ artifacts: await reports.listArtifacts(context.req.param("runId")
|
|
5623
|
+
return context.json({ artifacts: await reports.listArtifacts(context.req.param("runId")) });
|
|
5511
5624
|
} catch (error) {
|
|
5512
5625
|
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5513
5626
|
}
|
|
5514
5627
|
});
|
|
5515
|
-
app.get("/v1/runs/:runId/
|
|
5628
|
+
app.get("/v1/runs/:runId/events", async (context) => {
|
|
5516
5629
|
try {
|
|
5517
|
-
return context.json({ events: await reports.trajectory(context.req.param("runId")
|
|
5630
|
+
return context.json({ events: await reports.trajectory(context.req.param("runId")) });
|
|
5518
5631
|
} catch (error) {
|
|
5519
5632
|
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5520
5633
|
}
|
|
@@ -5547,7 +5660,7 @@ async function useProject(configPath) {
|
|
|
5547
5660
|
project = await loadProject(projectRoot, configPath);
|
|
5548
5661
|
projectRoot = project.root;
|
|
5549
5662
|
reportRoot = resolve(projectRoot, project.config.reportDir ?? "_evalkit-results");
|
|
5550
|
-
sandboxRoot =
|
|
5663
|
+
sandboxRoot = sandboxLocation(project).path;
|
|
5551
5664
|
}
|
|
5552
5665
|
/**
|
|
5553
5666
|
* `--port` is honoured exactly; without it the server takes the first free
|
|
@@ -5568,11 +5681,11 @@ async function serveDashboard({ port, open }) {
|
|
|
5568
5681
|
const dashboardRoot = dashboardAssets();
|
|
5569
5682
|
const app = new Hono();
|
|
5570
5683
|
const dashboardMatrix = project?.config.matrix ? registry.matrices.at(-1) : void 0;
|
|
5571
|
-
registerMatrixRoutes(app, registry, dashboardMatrix, project?.config.execution?.
|
|
5572
|
-
registerReportRoutes(app, registry, createDashboardReportReader(reportRoot
|
|
5684
|
+
registerMatrixRoutes(app, registry, dashboardMatrix, project?.config.execution?.repeat);
|
|
5685
|
+
registerReportRoutes(app, registry, createDashboardReportReader(reportRoot));
|
|
5573
5686
|
registerDashboardAssets(app, dashboardRoot);
|
|
5574
5687
|
const url = `http://localhost:${(await listen(app.fetch, port, 4317)).port}`;
|
|
5575
|
-
const reader = createDashboardReportReader(reportRoot
|
|
5688
|
+
const reader = createDashboardReportReader(reportRoot);
|
|
5576
5689
|
const runs = (await reader.listRuns()).length;
|
|
5577
5690
|
const legacy = reader.legacyRunIds().length;
|
|
5578
5691
|
const matrixCells = dashboardMatrix?.count({ parameters: {} });
|
|
@@ -5581,7 +5694,7 @@ async function serveDashboard({ port, open }) {
|
|
|
5581
5694
|
printLoadErrors(project?.loadErrors ?? []);
|
|
5582
5695
|
row("Local:", cyan(url));
|
|
5583
5696
|
row("Project:", `${plural(registry.evals.length, "eval")} · ${plural(registry.suites.length, "suite")}` + (dashboardMatrix ? ` · matrix ${authoringId(dashboardMatrix)} (${plural(matrixCells, "cell")})` : ""));
|
|
5584
|
-
row("Reports:", `${relative(process.cwd(), reportRoot) || "."} ${dim(`(${plural(runs, "run")}${legacy ? `, ${legacy} from an older EvalKit hidden
|
|
5697
|
+
row("Reports:", `${relative(process.cwd(), reportRoot) || "."} ${dim(`(${plural(runs, "run")}${legacy ? `, ${legacy} from an older EvalKit hidden; run \`evalkit upgrade\`` : ""})`)}`);
|
|
5585
5698
|
console.log(`\n ${dim("Runs started from the dashboard are logged below. Press Ctrl+C to stop.")}\n`);
|
|
5586
5699
|
if (open) openInBrowser(url);
|
|
5587
5700
|
}
|
|
@@ -5639,7 +5752,7 @@ async function loadProjectStep(configPath) {
|
|
|
5639
5752
|
*/
|
|
5640
5753
|
async function shouldRunEvals(run) {
|
|
5641
5754
|
if (run !== void 0) return run;
|
|
5642
|
-
const runs = (await createDashboardReportReader(reportRoot
|
|
5755
|
+
const runs = (await createDashboardReportReader(reportRoot).listRuns()).filter((entry) => !entry.archived).length;
|
|
5643
5756
|
if (!runs) {
|
|
5644
5757
|
console.log(` ${cyan("◇")} No results yet, so running the evals to have data to build.`);
|
|
5645
5758
|
return true;
|
|
@@ -5672,7 +5785,7 @@ async function tidyAction(options) {
|
|
|
5672
5785
|
}), (value) => {
|
|
5673
5786
|
const keep = project.config.keepSandboxes ?? "failed";
|
|
5674
5787
|
if (keep === "all") return "all kept (keepSandboxes: 'all')";
|
|
5675
|
-
return `${plural(value.removed, "workspace")} of ${keep === "none" ? "finished" : "passed"}
|
|
5788
|
+
return `${plural(value.removed, "workspace")} of ${keep === "none" ? "finished" : "passed"} runs removed`;
|
|
5676
5789
|
});
|
|
5677
5790
|
const byEval = (entries) => [...entries.reduce((groups, entry) => groups.set(entry.evalId, (groups.get(entry.evalId) ?? 0) + 1), /* @__PURE__ */ new Map()).entries()].map(([id, total]) => ` ${bold(id)} ${dim(`(${plural(total, "run")})`)}`).join("\n");
|
|
5678
5791
|
console.log("");
|
|
@@ -5683,16 +5796,34 @@ async function tidyAction(options) {
|
|
|
5683
5796
|
if (totalArchived) console.log(`\n ${dim(`Archived runs (${totalArchived}) are hidden in \`evalkit dev\` (filter "Show archived")`)}\n ${dim("and left out of `evalkit build`. No reports were deleted.")}`);
|
|
5684
5797
|
console.log("");
|
|
5685
5798
|
}
|
|
5799
|
+
async function upgradeAction(options) {
|
|
5800
|
+
banner("upgrade", VERSION);
|
|
5801
|
+
const { root, config } = await loadConfig(projectRoot, options.config);
|
|
5802
|
+
reportRoot = resolve(root, config.reportDir ?? "_evalkit-results");
|
|
5803
|
+
const report = await step(options.dryRun ? "Checking results" : "Upgrading results", () => upgradeReports(reportRoot, { dryRun: options.dryRun }), (value) => `${plural(value.upgraded, "run")} from an older EvalKit ${options.dryRun ? "found" : "upgraded"}`);
|
|
5804
|
+
console.log("");
|
|
5805
|
+
if (!report.upgraded) console.log(` ${cyan("✓")} Every run is already in the current format.`);
|
|
5806
|
+
else {
|
|
5807
|
+
const into = `${plural(report.upgraded, "run")} from older EvalKit versions into ${plural(report.runs, "run")}`;
|
|
5808
|
+
console.log(options.dryRun ? ` ${cyan("◆")} Would upgrade ${into}. Nothing was changed.` : ` ${cyan("✓")} Upgraded ${into}.`);
|
|
5809
|
+
if (report.batches) console.log(` ${dim(`${plural(report.batches, "run")} had more than one trial; each trial is now a run in a batch.`)}`);
|
|
5810
|
+
}
|
|
5811
|
+
if (report.running.length) console.log(` ${yellow("◆")} Left ${plural(report.running.length, "run")} still running alone; upgrade again when they finish.`);
|
|
5812
|
+
console.log("");
|
|
5813
|
+
}
|
|
5686
5814
|
async function shareServeAction(options) {
|
|
5687
5815
|
const root = resolve(process.cwd(), options.dir);
|
|
5688
|
-
const
|
|
5816
|
+
const url = `http://localhost:${(await listen((await createDirectoryShareApp({
|
|
5689
5817
|
dir: root,
|
|
5690
|
-
dashboardRoot: dashboardAssets()
|
|
5691
|
-
|
|
5818
|
+
dashboardRoot: dashboardAssets(),
|
|
5819
|
+
...options.basePath ? { basePath: options.basePath } : {},
|
|
5820
|
+
...options.repo ? { repo: options.repo } : {}
|
|
5821
|
+
})).fetch, options.port, 4318)).port}${normalizeBasePath(options.basePath)}`;
|
|
5692
5822
|
banner("share serve", VERSION);
|
|
5693
|
-
console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(
|
|
5823
|
+
console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(url)}`);
|
|
5824
|
+
if (options.repo) console.log(` ${cyan("➜")} ${bold("Repo:".padEnd(9))}${options.repo}`);
|
|
5694
5825
|
console.log(` ${cyan("➜")} ${bold("Storage:".padEnd(9))}${relative(process.cwd(), root) || "."}`);
|
|
5695
|
-
console.log(`\n ${dim(`Upload with share.url = "
|
|
5826
|
+
console.log(`\n ${dim(`Upload with share.url = "${url}". No authentication: keep it private.`)}\n`);
|
|
5696
5827
|
}
|
|
5697
5828
|
async function shareAction(options) {
|
|
5698
5829
|
const { all, allowDirty } = options;
|
|
@@ -5708,7 +5839,7 @@ async function shareAction(options) {
|
|
|
5708
5839
|
] }), (source) => source ? `${repoKey(source)} @ ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : "not a git repository");
|
|
5709
5840
|
if (!head) throw new Error("evalkit share needs to know the commit: run it inside a git repository or set EVALKIT_SOURCE_REPO and EVALKIT_SOURCE_COMMIT. `evalkit build` works without one.");
|
|
5710
5841
|
const registry = project.registry;
|
|
5711
|
-
const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot
|
|
5842
|
+
const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot).listRuns(), head, {
|
|
5712
5843
|
all,
|
|
5713
5844
|
allowDirty
|
|
5714
5845
|
}), (value) => selectionSummary(value, { all }));
|
|
@@ -5730,7 +5861,7 @@ async function shareAction(options) {
|
|
|
5730
5861
|
step
|
|
5731
5862
|
});
|
|
5732
5863
|
for (const failure of result.failed) console.log(` ${yellow("✗")} ${failure.runId.slice(0, 8)} ${dim(failure.error)}`);
|
|
5733
|
-
const target = shareTarget(serverUrl,
|
|
5864
|
+
const target = shareTarget(serverUrl, result.url, selection.selected);
|
|
5734
5865
|
console.log(`\n ${bold(result.failed.length ? "Shared with errors" : "Shared")} in ${duration(performance.now() - started)}\n`);
|
|
5735
5866
|
console.log(` ${cyan("➜")} ${cyan(target)}\n`);
|
|
5736
5867
|
if (result.failed.length) process.exitCode = 1;
|
|
@@ -5741,17 +5872,16 @@ async function compareAction(options) {
|
|
|
5741
5872
|
const say = options.json ? () => {} : (line = "") => console.log(line);
|
|
5742
5873
|
if (!options.json) banner("compare", VERSION);
|
|
5743
5874
|
await useProject(options.config);
|
|
5744
|
-
const registry = project.registry;
|
|
5745
5875
|
const head = await detectSource(projectRoot, { outputDirs: [
|
|
5746
5876
|
reportRoot,
|
|
5747
5877
|
sandboxRoot,
|
|
5748
5878
|
resolve(projectRoot, "_evalkit-dashboard")
|
|
5749
5879
|
] });
|
|
5750
5880
|
if (!head) throw new Error("evalkit compare needs to know the commit: run it inside a git repository or set EVALKIT_SOURCE_REPO and EVALKIT_SOURCE_COMMIT.");
|
|
5751
|
-
const headSide = headRuns(await createDashboardReportReader(reportRoot
|
|
5881
|
+
const headSide = headRuns(await createDashboardReportReader(reportRoot).listRuns(), head, { allowDirty: options.allowDirty });
|
|
5752
5882
|
let base;
|
|
5753
5883
|
if (options.baseDir) {
|
|
5754
|
-
const runs = await baseRunsFromDirectory(resolve(process.cwd(), options.baseDir)
|
|
5884
|
+
const runs = await baseRunsFromDirectory(resolve(process.cwd(), options.baseDir));
|
|
5755
5885
|
base = runs.length ? {
|
|
5756
5886
|
label: options.baseDir,
|
|
5757
5887
|
runs
|
|
@@ -5825,6 +5955,7 @@ try {
|
|
|
5825
5955
|
dev: devAction,
|
|
5826
5956
|
build: buildAction,
|
|
5827
5957
|
tidy: tidyAction,
|
|
5958
|
+
upgrade: upgradeAction,
|
|
5828
5959
|
share: shareAction,
|
|
5829
5960
|
shareServe: shareServeAction,
|
|
5830
5961
|
compare: compareAction
|