@leostera/evalkit 0.8.0 → 0.9.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 +46 -2
- package/README.md +2 -2
- package/dist/cli.mjs +386 -365
- package/dist/dashboard/assets/index-By1SJ1on.js +66 -0
- package/dist/dashboard/assets/index-geXMXCx9.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-BbVblaHz.mjs} +333 -51
- package/dist/index.d.mts +220 -178
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +226 -168
- 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 +3 -3
- 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 +5 -3
- package/dist/skills/evalkit-matrices/SKILL.md +2 -2
- package/dist/skills/evalkit-results/SKILL.md +6 -6
- package/dist/{src-CZnDXP1K.mjs → src-Bn6fnZnF.mjs} +8 -3
- package/dist/{src-LeGSyDD2.mjs → src-BrfIXfO2.mjs} +72 -54
- package/dist/{src-mm8L_IxM.mjs → src-CcdLZqFk.mjs} +422 -230
- package/dist/upgrade-60SIRius.mjs +110 -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,11 +1,11 @@
|
|
|
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 canonicalParameters, V as defineEvalMatrix, a as authoringId, c as defineSuite, i as agentRuntimes, m as registerEvals, p as recordError } from "./src-BrfIXfO2.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-CcdLZqFk.mjs";
|
|
5
|
+
import { f as encodeBundle, i as normalizeBasePath, l as dashboardIndex, s as repoKey } from "./index-db-BbVblaHz.mjs";
|
|
6
|
+
import { t as createDirectoryShareApp } from "./src-Bn6fnZnF.mjs";
|
|
7
7
|
import { Effect, LogLevel, Logger } from "effect";
|
|
8
|
-
import { cp, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { cp, lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
9
9
|
import path, { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import fs, { existsSync } from "node:fs";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -166,64 +166,6 @@ function shortSource(source) {
|
|
|
166
166
|
//#endregion
|
|
167
167
|
//#region ../cli/src/dashboard-reports.ts
|
|
168
168
|
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
|
-
function runMetrics(trials) {
|
|
170
|
-
const byName = /* @__PURE__ */ new Map();
|
|
171
|
-
for (const trial of trials) for (const result of trial?.scoring?.metrics ?? []) {
|
|
172
|
-
const entry = byName.get(result.name) ?? {
|
|
173
|
-
name: result.name,
|
|
174
|
-
...result.unit ? { unit: result.unit } : {},
|
|
175
|
-
...result.better ? { better: result.better } : {},
|
|
176
|
-
...result.builtIn ? { builtIn: true } : {},
|
|
177
|
-
count: 0,
|
|
178
|
-
sum: 0
|
|
179
|
-
};
|
|
180
|
-
if (result.value !== void 0) {
|
|
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
|
-
}));
|
|
226
|
-
}
|
|
227
169
|
/** Written into a run directory by `evalkit tidy`; deleting it restores the run. */
|
|
228
170
|
const ARCHIVE_MARKER = "archived.json";
|
|
229
171
|
function isMissing(error) {
|
|
@@ -240,23 +182,19 @@ async function ifPresent(operation) {
|
|
|
240
182
|
function assertUuid(id, label) {
|
|
241
183
|
if (!UUID_PATTERN.test(id)) throw new Error(`Invalid ${label}`);
|
|
242
184
|
}
|
|
243
|
-
function
|
|
185
|
+
function runPath(root, runId) {
|
|
244
186
|
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";
|
|
187
|
+
return resolve(root, runId);
|
|
259
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Headers for files an agent, a dataset or an adapter produced: untrusted
|
|
191
|
+
* content on the dashboard's origin, so never rendered or sniffed as HTML.
|
|
192
|
+
*/
|
|
193
|
+
const UNTRUSTED_FILE = {
|
|
194
|
+
"content-type": "application/octet-stream",
|
|
195
|
+
"x-content-type-options": "nosniff",
|
|
196
|
+
"content-security-policy": "sandbox"
|
|
197
|
+
};
|
|
260
198
|
function fileContentType(file) {
|
|
261
199
|
return {
|
|
262
200
|
".css": "text/css",
|
|
@@ -265,15 +203,17 @@ function fileContentType(file) {
|
|
|
265
203
|
}[extname(file)] ?? "application/octet-stream";
|
|
266
204
|
}
|
|
267
205
|
/**
|
|
268
|
-
* Read saved local reports. A missing summary means "running"; malformed
|
|
269
|
-
* Runs
|
|
206
|
+
* Read saved local reports. A missing summary means "running"; malformed
|
|
207
|
+
* reports are errors. Runs from an older report format (before schema
|
|
208
|
+
* version 6) are skipped and counted, for `evalkit upgrade` to convert.
|
|
270
209
|
*/
|
|
271
|
-
function createDashboardReportReader(root
|
|
210
|
+
function createDashboardReportReader(root) {
|
|
272
211
|
const legacyRuns = /* @__PURE__ */ new Set();
|
|
273
212
|
async function listRuns() {
|
|
274
213
|
const entries = await ifPresent(() => readdir(root, { withFileTypes: true }));
|
|
275
214
|
if (!entries) return [];
|
|
276
215
|
const ids = entries.filter((entry) => entry.isDirectory() && UUID_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
216
|
+
legacyRuns.clear();
|
|
277
217
|
return (await Promise.all(ids.map(readRun))).filter((run) => run !== void 0);
|
|
278
218
|
}
|
|
279
219
|
/** True for a run from a report format this version cannot read; never for corrupt JSON. */
|
|
@@ -286,7 +226,7 @@ function createDashboardReportReader(root, registry) {
|
|
|
286
226
|
} catch {
|
|
287
227
|
return false;
|
|
288
228
|
}
|
|
289
|
-
return typeof version === "number" && version <
|
|
229
|
+
return typeof version === "number" && version < 6;
|
|
290
230
|
}
|
|
291
231
|
async function readRun(id) {
|
|
292
232
|
if (await isLegacyRun(id)) {
|
|
@@ -296,15 +236,9 @@ function createDashboardReportReader(root, registry) {
|
|
|
296
236
|
const manifest = await ifPresent(() => readRunManifest(root, id));
|
|
297
237
|
if (!manifest) return void 0;
|
|
298
238
|
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
239
|
const archived = await ifPresent(() => stat(resolve(root, id, ARCHIVE_MARKER)));
|
|
307
240
|
const interrupted = !summary && ownerIsGone(manifest.process);
|
|
241
|
+
const scoring = summary?.scoring;
|
|
308
242
|
return {
|
|
309
243
|
id,
|
|
310
244
|
...archived ? { archived: true } : {},
|
|
@@ -321,76 +255,76 @@ function createDashboardReportReader(root, registry) {
|
|
|
321
255
|
manifest.aut.id,
|
|
322
256
|
manifest.aut.version
|
|
323
257
|
].filter(Boolean).join(" / ") } : {},
|
|
324
|
-
status: summary ?
|
|
258
|
+
status: summary ? summary.status : interrupted ? "cancelled" : "running",
|
|
325
259
|
...interrupted ? { interrupted: true } : {},
|
|
326
260
|
startedAt: manifest.startedAt,
|
|
327
261
|
...summary?.endedAt ? { completedAt: summary.endedAt } : {},
|
|
328
262
|
...summary?.durationMs !== void 0 ? { durationMs: summary.durationMs } : {},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
...
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const summary = await ifPresent(() => readTrialSummary(root, runId, id));
|
|
346
|
-
return {
|
|
263
|
+
...manifest.batch ? { batch: manifest.batch } : {},
|
|
264
|
+
...scoring?.overall === void 0 ? {} : { score: scoring.overall },
|
|
265
|
+
...scoring?.results.length ? { scorers: scoring.results.map((result) => ({
|
|
266
|
+
name: result.name,
|
|
267
|
+
kind: result.kind,
|
|
268
|
+
...result.skipped ? { skipped: true } : {},
|
|
269
|
+
...result.value === void 0 || result.skipped ? {} : { value: result.value }
|
|
270
|
+
})) } : {},
|
|
271
|
+
...scoring?.metrics?.length ? { metrics: scoring.metrics.map((metric) => ({
|
|
272
|
+
name: metric.name,
|
|
273
|
+
...metric.unit ? { unit: metric.unit } : {},
|
|
274
|
+
...metric.better ? { better: metric.better } : {},
|
|
275
|
+
...metric.builtIn ? { builtIn: true } : {},
|
|
276
|
+
...metric.value === void 0 ? {} : { value: metric.value }
|
|
277
|
+
})) } : {},
|
|
278
|
+
...manifest.datasets?.length ? { datasets: manifest.datasets.map(({ id, digest }) => ({
|
|
347
279
|
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);
|
|
280
|
+
digest
|
|
281
|
+
})) } : {}
|
|
282
|
+
};
|
|
360
283
|
}
|
|
361
|
-
async function
|
|
362
|
-
|
|
284
|
+
async function runDetail(runId) {
|
|
285
|
+
runPath(root, runId);
|
|
286
|
+
const manifest = await readRunManifest(root, runId);
|
|
287
|
+
const summary = await ifPresent(() => readRunSummary(root, runId));
|
|
288
|
+
const scoring = summary?.scoring;
|
|
363
289
|
return {
|
|
364
|
-
|
|
365
|
-
|
|
290
|
+
scores: scoring?.results ?? [],
|
|
291
|
+
...scoring?.metrics ? { metrics: scoring.metrics } : {},
|
|
292
|
+
checkpoints: scoring?.checkpoints ?? [],
|
|
293
|
+
...scoring?.skippedScorers ? { skippedScorers: scoring.skippedScorers } : {},
|
|
294
|
+
...summary?.attachments?.length ? { attachments: summary.attachments } : {},
|
|
295
|
+
manifest,
|
|
296
|
+
summary: summary ?? { status: "running" }
|
|
366
297
|
};
|
|
367
298
|
}
|
|
368
|
-
async function listArtifacts(runId
|
|
369
|
-
return walkArtifacts(resolve(
|
|
299
|
+
async function listArtifacts(runId) {
|
|
300
|
+
return walkArtifacts(resolve(runPath(root, runId), "artifacts"));
|
|
370
301
|
}
|
|
371
|
-
async function listCandidateWorkspace(runId
|
|
372
|
-
return walkArtifacts(resolve(
|
|
302
|
+
async function listCandidateWorkspace(runId) {
|
|
303
|
+
return walkArtifacts(resolve(runPath(root, runId), "artifacts", "candidate"));
|
|
373
304
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
if (!
|
|
379
|
-
|
|
305
|
+
/** A file below one of a run's artifact trees; never outside it. */
|
|
306
|
+
async function artifactFile(runId, tree, relativePath) {
|
|
307
|
+
const treeRoot = resolve(runPath(root, runId), "artifacts", tree);
|
|
308
|
+
const file = resolve(treeRoot, relativePath);
|
|
309
|
+
if (file === treeRoot || !file.startsWith(`${treeRoot}${sep}`)) throw new Error(`Invalid ${tree === "candidate" ? "workspace" : "attachment"} path`);
|
|
310
|
+
if (!(await lstat(file)).isFile()) throw new Error(`${tree === "candidate" ? "Workspace path" : "Attachment"} is not a file`);
|
|
311
|
+
return new Response(await readFile(file), { headers: UNTRUSTED_FILE });
|
|
380
312
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
313
|
+
const candidateFile = (runId, path) => artifactFile(runId, "candidate", path);
|
|
314
|
+
const attachmentFile = (runId, name) => artifactFile(runId, "attachments", name);
|
|
315
|
+
async function trajectory(runId) {
|
|
316
|
+
runPath(root, runId);
|
|
317
|
+
return await ifPresent(() => readRunEvents(root, runId)) ?? [];
|
|
384
318
|
}
|
|
385
319
|
return {
|
|
386
320
|
listRuns,
|
|
387
|
-
/** Run IDs skipped by the last `listRuns()
|
|
321
|
+
/** Run IDs skipped by the last `listRuns()`: an older EvalKit wrote them; `evalkit upgrade` converts them. */
|
|
388
322
|
legacyRunIds: () => [...legacyRuns],
|
|
389
|
-
|
|
390
|
-
trialDetail,
|
|
323
|
+
runDetail,
|
|
391
324
|
listArtifacts,
|
|
392
325
|
listCandidateWorkspace,
|
|
393
326
|
candidateFile,
|
|
327
|
+
attachmentFile,
|
|
394
328
|
trajectory
|
|
395
329
|
};
|
|
396
330
|
}
|
|
@@ -461,40 +395,32 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
461
395
|
}
|
|
462
396
|
return files;
|
|
463
397
|
}, (files) => count(files, "dataset file"));
|
|
464
|
-
const reports = createDashboardReportReader(reportRoot
|
|
398
|
+
const reports = createDashboardReportReader(reportRoot);
|
|
465
399
|
const snapshot = await step("Run reports", async (progress) => {
|
|
466
400
|
const all = await reports.listRuns();
|
|
467
401
|
const runs = all.filter((run) => !run.archived);
|
|
468
402
|
await write("runs.json", { runs });
|
|
469
|
-
let trials = 0;
|
|
470
403
|
let files = 0;
|
|
471
|
-
const runTrials = [];
|
|
472
404
|
for (const [index, run] of runs.entries()) {
|
|
473
405
|
progress(`run ${index + 1}/${runs.length} · ${run.id.slice(0, 8)}`);
|
|
474
406
|
const written = await snapshotRun(reports, run.id, directorySink(resolve(data, "runs", run.id)));
|
|
475
|
-
trials += written.trialIds.length;
|
|
476
407
|
files += written.files;
|
|
477
|
-
runTrials.push({
|
|
478
|
-
id: run.id,
|
|
479
|
-
trials: written.trialIds
|
|
480
|
-
});
|
|
481
408
|
}
|
|
482
409
|
return {
|
|
483
410
|
runs: runs.length,
|
|
484
|
-
trials,
|
|
485
411
|
files,
|
|
486
412
|
skipped: reports.legacyRunIds().length,
|
|
487
413
|
archived: all.length - runs.length,
|
|
488
|
-
|
|
414
|
+
runIds: runs.map((run) => run.id)
|
|
489
415
|
};
|
|
490
|
-
}, ({ runs,
|
|
416
|
+
}, ({ runs, files, skipped, archived }) => `${count(runs, "run")} · ${count(files, "candidate file")}` + (archived ? ` · ${archived} archived excluded` : "") + (skipped ? ` · ${skipped} older-format skipped (run \`evalkit upgrade\`)` : ""));
|
|
491
417
|
await step("Route pages", async () => {
|
|
492
418
|
const catalog = registry.catalog();
|
|
493
419
|
const routes = staticRoutes({
|
|
494
420
|
scenarios: catalog.map((entry) => entry.id),
|
|
495
421
|
agents: catalog.flatMap((entry) => entry.agent.id ? [entry.agent.id] : []),
|
|
496
422
|
datasets: catalog.flatMap((entry) => entry.datasets.map((dataset) => dataset.id)),
|
|
497
|
-
runs: snapshot.
|
|
423
|
+
runs: snapshot.runIds
|
|
498
424
|
});
|
|
499
425
|
for (const route of routes) {
|
|
500
426
|
const file = resolve(outDir, `${route}.html`);
|
|
@@ -504,7 +430,7 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
504
430
|
await writeFile(resolve(outDir, "404.html"), dashboardIndex(template, { base: "/" }));
|
|
505
431
|
return routes.length;
|
|
506
432
|
}, (pages) => `${count(pages, "page")} with clean /path URLs`);
|
|
507
|
-
const {
|
|
433
|
+
const { runIds: _runs, ...summary } = snapshot;
|
|
508
434
|
return {
|
|
509
435
|
outDir,
|
|
510
436
|
...summary
|
|
@@ -522,7 +448,6 @@ function staticRoutes(ids) {
|
|
|
522
448
|
return [
|
|
523
449
|
"scoreboard",
|
|
524
450
|
"runs",
|
|
525
|
-
"trials",
|
|
526
451
|
"workspaces",
|
|
527
452
|
"library/agents",
|
|
528
453
|
"library/judges",
|
|
@@ -531,7 +456,11 @@ function staticRoutes(ids) {
|
|
|
531
456
|
...under("library/scenarios", ids.scenarios),
|
|
532
457
|
...under("library/agents", ids.agents),
|
|
533
458
|
...under("library/datasets", ids.datasets),
|
|
534
|
-
...ids.runs.filter(
|
|
459
|
+
...ids.runs.filter(safe).flatMap((id) => [
|
|
460
|
+
`runs/${id}`,
|
|
461
|
+
`runs/${id}/workspace`,
|
|
462
|
+
`runs/${id}/attachments`
|
|
463
|
+
])
|
|
535
464
|
];
|
|
536
465
|
}
|
|
537
466
|
/** `runs/abc.html` is served at `<mount>/runs/abc`; its base is one level up. */
|
|
@@ -564,33 +493,33 @@ function directorySink(root) {
|
|
|
564
493
|
}
|
|
565
494
|
/**
|
|
566
495
|
* One run's snapshot, shared by `evalkit build` (files on disk) and
|
|
567
|
-
* `evalkit share` (an upload bundle): `
|
|
568
|
-
* `
|
|
569
|
-
*
|
|
496
|
+
* `evalkit share` (an upload bundle): `detail.json`, `events.json`,
|
|
497
|
+
* `artifacts.json` and `workspace.json`, the candidate files under
|
|
498
|
+
* `workspace/`, and attachments under `attachments/` (both left out with
|
|
499
|
+
* `workspaceFiles: false`; they stay listed). Evaluator workspaces are never
|
|
500
|
+
* read.
|
|
570
501
|
*/
|
|
571
502
|
async function snapshotRun(reports, runId, sink, options = {}) {
|
|
572
|
-
const
|
|
573
|
-
await sink.json("
|
|
503
|
+
const detail = await reports.runDetail(runId);
|
|
504
|
+
await sink.json("detail.json", detail);
|
|
505
|
+
await sink.json("events.json", { events: await reports.trajectory(runId) });
|
|
506
|
+
await sink.json("artifacts.json", { artifacts: await reports.listArtifacts(runId) });
|
|
507
|
+
const workspace = await reports.listCandidateWorkspace(runId);
|
|
508
|
+
await sink.json("workspace.json", workspace);
|
|
509
|
+
if (options.workspaceFiles === false) return { files: 0 };
|
|
574
510
|
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
|
-
}
|
|
511
|
+
for (const attachment of detail.attachments ?? []) {
|
|
512
|
+
const response = await reports.attachmentFile(runId, attachment.name);
|
|
513
|
+
await sink.bytes(`attachments/${attachment.name}`, new Uint8Array(await response.arrayBuffer()));
|
|
514
|
+
files += 1;
|
|
589
515
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
516
|
+
for (const entry of workspace) {
|
|
517
|
+
if (entry.kind !== "file") continue;
|
|
518
|
+
const response = await reports.candidateFile(runId, entry.path);
|
|
519
|
+
await sink.bytes(`workspace/${entry.path}`, new Uint8Array(await response.arrayBuffer()));
|
|
520
|
+
files += 1;
|
|
521
|
+
}
|
|
522
|
+
return { files };
|
|
594
523
|
}
|
|
595
524
|
/** Total size and file count of a directory, for the build summary. */
|
|
596
525
|
async function directorySize(root) {
|
|
@@ -639,7 +568,7 @@ function listMatrixCells(matrix, input) {
|
|
|
639
568
|
if (![
|
|
640
569
|
"eval",
|
|
641
570
|
"agent",
|
|
642
|
-
"
|
|
571
|
+
"repeat",
|
|
643
572
|
...[...Object.keys(matrix.cases?.[0] ?? {}), ...Object.keys(matrix.parameters)].map((key) => `axis:${key}`)
|
|
644
573
|
].includes(input.sort)) throw new Error(`Unknown matrix sort: ${input.sort}`);
|
|
645
574
|
const selection = input.evalIds.length ? { evals: input.evalIds } : {};
|
|
@@ -663,7 +592,7 @@ function listMatrixCells(matrix, input) {
|
|
|
663
592
|
}
|
|
664
593
|
if (input.sort === "eval") return cell.eval.name ?? cell.eval.id;
|
|
665
594
|
if (input.sort === "agent") return cell.eval.agent?.identity?.name ?? cell.eval.agent?.identity?.kind ?? "";
|
|
666
|
-
return cell.eval.
|
|
595
|
+
return cell.eval.repeat ?? 1;
|
|
667
596
|
}
|
|
668
597
|
cells.sort((left, right) => {
|
|
669
598
|
const a = value(left);
|
|
@@ -894,7 +823,12 @@ async function discover(root, config) {
|
|
|
894
823
|
async function isDirectory(path) {
|
|
895
824
|
return stat(path).then((info) => info.isDirectory(), () => false);
|
|
896
825
|
}
|
|
897
|
-
|
|
826
|
+
/**
|
|
827
|
+
* The project's config and root, without importing any scenario: enough for
|
|
828
|
+
* commands that only touch results (`evalkit upgrade`), and that must work
|
|
829
|
+
* while scenarios from an older EvalKit still fail to load.
|
|
830
|
+
*/
|
|
831
|
+
async function loadConfig(cwd, configPath) {
|
|
898
832
|
const candidates = configPath ? [resolve(cwd, configPath)] : [
|
|
899
833
|
"evalkit.config.ts",
|
|
900
834
|
"evalkit.config.js",
|
|
@@ -908,7 +842,16 @@ async function loadProject(cwd, configPath) {
|
|
|
908
842
|
const root = file ? dirname(file) : resolve(cwd);
|
|
909
843
|
const config = file ? (await importProjectModule(file)).default : {};
|
|
910
844
|
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Config must default-export defineConfig({...})");
|
|
845
|
+
if (config.execution && "trials" in config.execution) throw new Error("execution.trials was renamed to execution.repeat in EvalKit 0.9");
|
|
911
846
|
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`);
|
|
847
|
+
return {
|
|
848
|
+
root,
|
|
849
|
+
...file ? { file } : {},
|
|
850
|
+
config
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
async function loadProject(cwd, configPath) {
|
|
854
|
+
const { root, file, config } = await loadConfig(cwd, configPath);
|
|
912
855
|
let registry = config.registry;
|
|
913
856
|
if (!file && !configPath && await exists$1(resolve(root, "src/registry.ts"))) registry = (await importProjectModule(resolve(root, "src/registry.ts"))).default;
|
|
914
857
|
let loadErrors = [];
|
|
@@ -996,25 +939,23 @@ function cellLabel(name, parameters) {
|
|
|
996
939
|
const shown = Object.entries(parameters).map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`).join(" ");
|
|
997
940
|
return shown ? `${bold(name)} ${dim(shown)}` : bold(name);
|
|
998
941
|
}
|
|
999
|
-
function passed(result) {
|
|
1000
|
-
return result.status === "completed" && (result.aggregateScoring?.passRate === 1 || Boolean(result.scoring?.passed));
|
|
1001
|
-
}
|
|
1002
942
|
/** Live log for runs launched from the `evalkit dev` dashboard. */
|
|
1003
943
|
const runLog = {
|
|
1004
944
|
started(name, parameters = {}, suiteId) {
|
|
1005
945
|
const suite = suiteId ? dim(` (suite ${suiteId})`) : "";
|
|
1006
946
|
console.log(`${clock()} ${cyan("▶ run ")} ${cellLabel(name, parameters)}${suite}`);
|
|
1007
947
|
},
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
948
|
+
/** One line per run; a batch's runs say which one they are. */
|
|
949
|
+
finished(name, parameters, runs, ms) {
|
|
950
|
+
for (const result of runs) {
|
|
951
|
+
const status = result.status === "passed" ? green("✓ pass") : result.status === "failed" ? red("✗ fail") : red(`✗ ${result.status}`);
|
|
952
|
+
const batch = result.batch ? ` #${result.batch.index + 1}/${result.batch.size}` : "";
|
|
953
|
+
console.log(`${clock()} ${status} ${cellLabel(name, parameters)}${dim(`${batch} · ${duration(result.durationMs ?? ms)}`)}`);
|
|
954
|
+
console.log(` ${dim(`report ${result.reportLocation}`)}`);
|
|
955
|
+
}
|
|
1015
956
|
},
|
|
1016
|
-
cell(cell,
|
|
1017
|
-
runLog.finished(cell.eval.name ?? authoringId(cell.eval), cell.parameters,
|
|
957
|
+
cell(cell, runs, ms) {
|
|
958
|
+
runLog.finished(cell.eval.name ?? authoringId(cell.eval), cell.parameters, runs, ms);
|
|
1018
959
|
},
|
|
1019
960
|
failed(name, error) {
|
|
1020
961
|
console.log(`${clock()} ${red("✗ error")} ${bold(name)} ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1066,9 +1007,9 @@ async function confirm(question) {
|
|
|
1066
1007
|
}
|
|
1067
1008
|
//#endregion
|
|
1068
1009
|
//#region ../cli/src/run-reporter.ts
|
|
1069
|
-
function
|
|
1010
|
+
function runOutcome(result) {
|
|
1070
1011
|
if (result.status === "cancelled") return "cancelled";
|
|
1071
|
-
return result.status === "
|
|
1012
|
+
return result.status === "passed" ? "passed" : "failed";
|
|
1072
1013
|
}
|
|
1073
1014
|
function passAtFor(definition, name) {
|
|
1074
1015
|
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "predicate" && step.name === name);
|
|
@@ -1087,51 +1028,48 @@ function checkpointReason(definition, checkpoint) {
|
|
|
1087
1028
|
const passAt = passAtFor(definition, checkpoint.name);
|
|
1088
1029
|
return `step ${checkpoint.step} ${checkpoint.name} ${checkpoint.value === void 0 ? checkpoint.status : value(checkpoint)}${passAt === void 0 ? "" : ` (passes at ${passAt})`}`;
|
|
1089
1030
|
}
|
|
1090
|
-
/** What went wrong in a
|
|
1091
|
-
function reasonsFor(definition,
|
|
1031
|
+
/** What went wrong in a run. */
|
|
1032
|
+
function reasonsFor(definition, run) {
|
|
1092
1033
|
const reasons = /* @__PURE__ */ new Set();
|
|
1093
1034
|
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
|
-
}
|
|
1035
|
+
if (run.status === "errored" && run.error) reasons.add(`error: ${run.error.message}`);
|
|
1036
|
+
for (const checkpoint of run.scoring?.checkpoints ?? []) if (checkpoint.status === "failed" || checkpoint.status === "error") reasons.add(checkpointReason(definition, checkpoint));
|
|
1037
|
+
for (const score of run.scoring?.results ?? []) if (score.skipped) skipped.add(score.name);
|
|
1038
|
+
else if (!score.passed) reasons.add(scoreReason(definition, score));
|
|
1039
|
+
for (const name of run.scoring?.skippedScorers ?? []) if (run.status !== "cancelled") reasons.add(`${name} not run`);
|
|
1101
1040
|
return {
|
|
1102
1041
|
reasons: [...reasons],
|
|
1103
1042
|
skipped: [...skipped]
|
|
1104
1043
|
};
|
|
1105
1044
|
}
|
|
1106
|
-
function
|
|
1045
|
+
function runReport(cell, result, suiteId) {
|
|
1107
1046
|
const evalId = authoringId(cell.eval);
|
|
1108
|
-
const trials = result.trials ?? [result];
|
|
1109
1047
|
return {
|
|
1110
1048
|
label: suiteId ? `${suiteId}/${evalId}` : evalId,
|
|
1111
1049
|
evalId,
|
|
1112
1050
|
...suiteId ? { suiteId } : {},
|
|
1113
1051
|
parameters: cell.parameters,
|
|
1114
|
-
outcome:
|
|
1052
|
+
outcome: runOutcome(result),
|
|
1115
1053
|
...result.durationMs !== void 0 ? { durationMs: result.durationMs } : {},
|
|
1116
1054
|
result,
|
|
1117
|
-
...reasonsFor(cell.eval,
|
|
1055
|
+
...reasonsFor(cell.eval, result)
|
|
1118
1056
|
};
|
|
1119
1057
|
}
|
|
1120
1058
|
function parameterLabel(parameters) {
|
|
1121
1059
|
return Object.entries(parameters).map(([key, item]) => `${key}=${typeof item === "string" ? item : JSON.stringify(item)}`).join(" ");
|
|
1122
1060
|
}
|
|
1123
|
-
/** One printed line for a finished
|
|
1124
|
-
function
|
|
1061
|
+
/** One printed line for a finished run; failing reasons on the same line. */
|
|
1062
|
+
function runLine(scenario) {
|
|
1125
1063
|
const mark = scenario.outcome === "passed" ? green("✓") : scenario.outcome === "cancelled" ? yellow("◌") : red("✗");
|
|
1126
1064
|
const parameters = parameterLabel(scenario.parameters);
|
|
1127
|
-
const
|
|
1128
|
-
const
|
|
1065
|
+
const batch = scenario.result.batch;
|
|
1066
|
+
const position = batch ? dim(` #${batch.index + 1}/${batch.size}`) : "";
|
|
1129
1067
|
const details = [
|
|
1130
1068
|
...scenario.outcome === "failed" ? scenario.reasons : [],
|
|
1131
1069
|
...scenario.outcome === "cancelled" ? ["cancelled"] : [],
|
|
1132
1070
|
...scenario.skipped.map((name) => `${name} skipped`)
|
|
1133
1071
|
];
|
|
1134
|
-
return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${
|
|
1072
|
+
return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${position}${details.length ? ` ${dim(details.join(" · "))}` : ""}`;
|
|
1135
1073
|
}
|
|
1136
1074
|
/**
|
|
1137
1075
|
* Prints run progress. On a terminal, failures print as they happen and a live
|
|
@@ -1177,29 +1115,29 @@ function createRunReporter(options) {
|
|
|
1177
1115
|
timer.unref?.();
|
|
1178
1116
|
}
|
|
1179
1117
|
},
|
|
1180
|
-
onEvent(
|
|
1181
|
-
if (event.source === "runner" && event.kind === "
|
|
1182
|
-
running.set(
|
|
1183
|
-
label: options.labelFor(
|
|
1118
|
+
onEvent(run, event) {
|
|
1119
|
+
if (event.source === "runner" && event.kind === "run-started") {
|
|
1120
|
+
running.set(run.runId, {
|
|
1121
|
+
label: options.labelFor(run),
|
|
1184
1122
|
startedAt: now()
|
|
1185
1123
|
});
|
|
1186
1124
|
return;
|
|
1187
1125
|
}
|
|
1188
|
-
const activity = running.get(
|
|
1126
|
+
const activity = running.get(run.runId);
|
|
1189
1127
|
if (!activity) return;
|
|
1190
|
-
if (event.source === "runner" && (event.kind === "
|
|
1128
|
+
if (event.source === "runner" && (event.kind === "run-completed" || event.kind === "error")) running.delete(run.runId);
|
|
1191
1129
|
else if (event.source === "aut" && event.kind === "tool-call") activity.doing = `tool ${event.name}`;
|
|
1192
1130
|
else if (event.source === "runner" && event.kind === "scorer-started") activity.doing = `scoring ${event.scorer}`;
|
|
1193
1131
|
else if (event.source === "runner" && event.kind === "checkpoint-started") activity.doing = `check ${event.name}`;
|
|
1194
1132
|
},
|
|
1195
|
-
|
|
1133
|
+
onRun(scenario) {
|
|
1196
1134
|
scenarios.push(scenario);
|
|
1197
|
-
|
|
1198
|
-
if (!options.interactive || options.verbose || scenario.outcome !== "passed") print(
|
|
1135
|
+
running.delete(scenario.result.runId);
|
|
1136
|
+
if (!options.interactive || options.verbose || scenario.outcome !== "passed") print(runLine(scenario));
|
|
1199
1137
|
},
|
|
1200
1138
|
/** Called on the first Ctrl+C. */
|
|
1201
1139
|
cancelling() {
|
|
1202
|
-
print(`\n ${yellow("◌")} Cancelling: finishing running
|
|
1140
|
+
print(`\n ${yellow("◌")} Cancelling: finishing running runs. Press Ctrl+C again to exit now.`);
|
|
1203
1141
|
},
|
|
1204
1142
|
finish(durationMs) {
|
|
1205
1143
|
if (timer) clearInterval(timer);
|
|
@@ -1208,11 +1146,12 @@ function createRunReporter(options) {
|
|
|
1208
1146
|
passed: scenarios.filter((s) => s.outcome === "passed").length,
|
|
1209
1147
|
failed: scenarios.filter((s) => s.outcome === "failed").length,
|
|
1210
1148
|
cancelled: scenarios.filter((s) => s.outcome === "cancelled").length,
|
|
1211
|
-
|
|
1149
|
+
runs: scenarios.length,
|
|
1150
|
+
scenarios: new Set(scenarios.map((s) => `${s.label} ${parameterLabel(s.parameters)}`)).size,
|
|
1212
1151
|
durationMs
|
|
1213
1152
|
};
|
|
1214
1153
|
},
|
|
1215
|
-
|
|
1154
|
+
runs: () => scenarios
|
|
1216
1155
|
};
|
|
1217
1156
|
}
|
|
1218
1157
|
function totalsLine(totals) {
|
|
@@ -1221,7 +1160,8 @@ function totalsLine(totals) {
|
|
|
1221
1160
|
totals.failed ? red(`${totals.failed} failed`) : "",
|
|
1222
1161
|
totals.cancelled ? yellow(`${totals.cancelled} cancelled`) : ""
|
|
1223
1162
|
].filter(Boolean);
|
|
1224
|
-
|
|
1163
|
+
const coverage = totals.runs === totals.scenarios ? plural(totals.runs, "run") : `${plural(totals.runs, "run")} of ${plural(totals.scenarios, "scenario")}`;
|
|
1164
|
+
return ` ${bold(parts.join(" · "))} ${dim(`· ${coverage} in ${duration(totals.durationMs)}`)}`;
|
|
1225
1165
|
}
|
|
1226
1166
|
//#endregion
|
|
1227
1167
|
//#region ../cli/src/junit.ts
|
|
@@ -1231,8 +1171,8 @@ function escape(text) {
|
|
|
1231
1171
|
const seconds = (ms) => ((ms ?? 0) / 1e3).toFixed(3);
|
|
1232
1172
|
/**
|
|
1233
1173
|
* A JUnit XML report: one test suite per EvalKit suite (`default` for
|
|
1234
|
-
* scenarios without one) and one test case per
|
|
1235
|
-
*
|
|
1174
|
+
* scenarios without one) and one test case per run, named after its scenario
|
|
1175
|
+
* and parameters, and its place in a batch (` #2`) when repeated.
|
|
1236
1176
|
*/
|
|
1237
1177
|
function junitReport(scenarios) {
|
|
1238
1178
|
const bySuite = /* @__PURE__ */ new Map();
|
|
@@ -1246,7 +1186,8 @@ function junitReport(scenarios) {
|
|
|
1246
1186
|
const time = cases.reduce((sum, c) => sum + (c.durationMs ?? 0), 0);
|
|
1247
1187
|
const testcases = cases.map((scenario) => {
|
|
1248
1188
|
const parameters = parameterLabel(scenario.parameters);
|
|
1249
|
-
const
|
|
1189
|
+
const batch = scenario.result.batch;
|
|
1190
|
+
const attributes = `name="${escape((parameters ? `${scenario.evalId} [${parameters}]` : scenario.evalId) + (batch ? ` #${batch.index + 1}` : ""))}" classname="${escape(suite)}" time="${seconds(scenario.durationMs)}"`;
|
|
1250
1191
|
if (scenario.outcome === "failed") return ` <testcase ${attributes}>\n <failure message="${escape(scenario.reasons.join("; ") || "failed")}">${escape(scenario.reasons.join("\n"))}</failure>\n </testcase>`;
|
|
1251
1192
|
if (scenario.outcome === "cancelled") return ` <testcase ${attributes}>\n <skipped message="cancelled"/>\n </testcase>`;
|
|
1252
1193
|
if (allSkipped(scenario)) return ` <testcase ${attributes}>\n <skipped message="${escape(`all scorers skipped: ${scenario.skipped.join(", ")}`)}"/>\n </testcase>`;
|
|
@@ -1256,15 +1197,15 @@ function junitReport(scenarios) {
|
|
|
1256
1197
|
});
|
|
1257
1198
|
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
1199
|
}
|
|
1259
|
-
/** True when every score
|
|
1200
|
+
/** True when every score was skip()ped. */
|
|
1260
1201
|
function allSkipped(scenario) {
|
|
1261
|
-
const results =
|
|
1202
|
+
const results = scenario.result.scoring?.results ?? [];
|
|
1262
1203
|
return results.length > 0 && results.every((score) => score.skipped);
|
|
1263
1204
|
}
|
|
1264
1205
|
//#endregion
|
|
1265
1206
|
//#region ../cli/src/version.ts
|
|
1266
1207
|
/** The CLI version; kept in sync with package.json when releasing. */
|
|
1267
|
-
const VERSION = "0.
|
|
1208
|
+
const VERSION = "0.9.0";
|
|
1268
1209
|
//#endregion
|
|
1269
1210
|
//#region ../cli/src/run-command.ts
|
|
1270
1211
|
/** Validates `run` flags into a selection, parameter overrides and limits. */
|
|
@@ -1291,28 +1232,27 @@ function normalizeRunOptions(values, positionals) {
|
|
|
1291
1232
|
const index = value.indexOf("=");
|
|
1292
1233
|
if (index < 1) throw new Error("--select requires axis=value");
|
|
1293
1234
|
const axis = value.slice(0, index);
|
|
1294
|
-
|
|
1235
|
+
const raw = value.slice(index + 1);
|
|
1236
|
+
let choices;
|
|
1295
1237
|
try {
|
|
1296
|
-
|
|
1238
|
+
choices = [JSON.parse(raw)];
|
|
1297
1239
|
} catch {
|
|
1298
|
-
|
|
1240
|
+
choices = raw.split(",").filter(Boolean);
|
|
1299
1241
|
}
|
|
1300
|
-
(selection[axis] ??= []).push(
|
|
1242
|
+
(selection[axis] ??= []).push(...choices);
|
|
1301
1243
|
}
|
|
1302
1244
|
for (const value of values.param ?? []) {
|
|
1303
1245
|
const index = value.indexOf("=");
|
|
1304
1246
|
if (index < 1) throw new Error("--param requires name=JSON");
|
|
1305
1247
|
parameters[value.slice(0, index)] = JSON.parse(value.slice(index + 1));
|
|
1306
1248
|
}
|
|
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
1249
|
return {
|
|
1310
1250
|
values,
|
|
1311
1251
|
positionals,
|
|
1312
1252
|
parameters,
|
|
1313
1253
|
selection,
|
|
1314
1254
|
concurrency: integer("concurrency"),
|
|
1315
|
-
|
|
1255
|
+
repeat: integer("repeat")
|
|
1316
1256
|
};
|
|
1317
1257
|
}
|
|
1318
1258
|
/** Load errors that affect what was asked for, so the run must not pretend it covered them. */
|
|
@@ -1333,35 +1273,32 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1333
1273
|
const project = await loadProject(cwd, values.config);
|
|
1334
1274
|
process.chdir(project.root);
|
|
1335
1275
|
const registry = project.registry;
|
|
1336
|
-
const requestedEvals = values.eval
|
|
1276
|
+
const requestedEvals = [...values.eval ?? [], ...positionals].flatMap((value) => value.split(",").filter(Boolean));
|
|
1337
1277
|
let matrix;
|
|
1338
1278
|
let suiteId;
|
|
1339
1279
|
const quiet = Boolean(values.json || values["dry-run"]);
|
|
1340
1280
|
if (!quiet) banner("run", VERSION);
|
|
1281
|
+
const suite = values.suite ? registry.suites.find((item) => item.id === values.suite) : void 0;
|
|
1341
1282
|
if (values.suite) {
|
|
1342
|
-
const suite = registry.suites.find((item) => item.id === values.suite);
|
|
1343
1283
|
const broken = project.loadErrors.filter((error) => error.suiteId === values.suite);
|
|
1344
1284
|
if (!suite && !broken.length) throw new Error(`Unknown suite: ${values.suite}`);
|
|
1345
1285
|
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
1286
|
}
|
|
1287
|
+
const configured = values.matrix ? registry.matrices.find((item) => item.id === values.matrix) : project.config.matrix ? registry.matrices.at(-1) : void 0;
|
|
1288
|
+
if (values.matrix && !configured) throw new Error(`Unknown matrix: ${values.matrix}`);
|
|
1289
|
+
if (configured && suite) {
|
|
1290
|
+
const inSuite = new Set(suite.evals.map(authoringId));
|
|
1291
|
+
const both = configured.evals.map(authoringId).filter((id) => inSuite.has(id));
|
|
1292
|
+
if (!both.length) throw new Error(`Matrix ${authoringId(configured)} has no scenarios from suite ${suite.id}`);
|
|
1293
|
+
const outside = requestedEvals.filter((id) => !both.includes(id));
|
|
1294
|
+
if (outside.length) throw new Error(`Not in both matrix ${authoringId(configured)} and suite ${suite.id}: ${outside.join(", ")}`);
|
|
1295
|
+
if (!requestedEvals.length) requestedEvals.push(...both);
|
|
1296
|
+
}
|
|
1297
|
+
matrix = configured ?? defineEvalMatrix({
|
|
1298
|
+
id: "default",
|
|
1299
|
+
evals: values.suite ? suite?.evals ?? [] : registry.evals,
|
|
1300
|
+
parameters: {}
|
|
1301
|
+
});
|
|
1365
1302
|
const broken = brokenSelection(project.loadErrors, {
|
|
1366
1303
|
...suiteId ? { suiteId } : {},
|
|
1367
1304
|
evalIds: requestedEvals,
|
|
@@ -1385,14 +1322,17 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1385
1322
|
else throw new Error(`Unknown matrix axis: ${axis}`);
|
|
1386
1323
|
const cells = matrix.count(selection);
|
|
1387
1324
|
if (!cells) throw new Error("Selection contains no eligible cells; check cases and exclusions");
|
|
1388
|
-
const
|
|
1325
|
+
const repeat = parsed.repeat ?? project.config.execution?.repeat;
|
|
1326
|
+
let runs = 0;
|
|
1327
|
+
for (const cell of matrix.cells(selection)) runs += repeat ?? cell.eval.repeat ?? 1;
|
|
1389
1328
|
const plan = {
|
|
1390
1329
|
matrix: authoringId(matrix),
|
|
1391
1330
|
evals: requestedEvals.length ? requestedEvals : matrix.evals.map((definition) => definition.id),
|
|
1392
1331
|
cells,
|
|
1332
|
+
runs,
|
|
1393
1333
|
parameters: selection.parameters,
|
|
1394
1334
|
overrides: selection.overrides,
|
|
1395
|
-
|
|
1335
|
+
repeat: repeat ?? "scenario"
|
|
1396
1336
|
};
|
|
1397
1337
|
if (values["dry-run"]) {
|
|
1398
1338
|
console.log(JSON.stringify(plan, null, 2));
|
|
@@ -1409,14 +1349,15 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1409
1349
|
] });
|
|
1410
1350
|
const suiteOf = new Map(registry.suites.flatMap((suite) => suite.evals.map((evaluation) => [authoringId(evaluation), suite.id])));
|
|
1411
1351
|
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");
|
|
1352
|
+
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
1353
|
const reporter = createRunReporter({
|
|
1414
1354
|
interactive: interactive && !values.json,
|
|
1415
1355
|
verbose: Boolean(values.verbose),
|
|
1416
|
-
total:
|
|
1417
|
-
labelFor: (
|
|
1418
|
-
const suite = suiteFor(
|
|
1419
|
-
|
|
1356
|
+
total: runs,
|
|
1357
|
+
labelFor: (run) => {
|
|
1358
|
+
const suite = suiteFor(run.evalId);
|
|
1359
|
+
const label = suite ? `${suite}/${run.evalId}` : run.evalId;
|
|
1360
|
+
return run.batch ? `${label} #${run.batch.index + 1}/${run.batch.size}` : label;
|
|
1420
1361
|
},
|
|
1421
1362
|
write: values.json ? () => {} : void 0
|
|
1422
1363
|
});
|
|
@@ -1433,7 +1374,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1433
1374
|
summary = await Effect.runPromise(runnerLogs(runMatrix(matrix, {
|
|
1434
1375
|
selection,
|
|
1435
1376
|
concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
|
|
1436
|
-
|
|
1377
|
+
...repeat ? { repeat } : {},
|
|
1437
1378
|
...suiteId ? { suiteId } : {},
|
|
1438
1379
|
suiteFor: (cell) => suiteFor(authoringId(cell.eval)),
|
|
1439
1380
|
...values.local ? { runtime: "local" } : {},
|
|
@@ -1443,20 +1384,23 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1443
1384
|
workspaceRoot: sandboxDir,
|
|
1444
1385
|
keepWorkspaces: project.config.keepSandboxes,
|
|
1445
1386
|
artifactExclude: project.config.artifacts?.exclude,
|
|
1387
|
+
attachmentLimits: project.config.artifacts?.attachments,
|
|
1446
1388
|
signal: controller.signal,
|
|
1447
|
-
onEvent: (
|
|
1448
|
-
onResult: (cell,
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1389
|
+
onEvent: (run, event) => reporter.onEvent(run, event),
|
|
1390
|
+
onResult: (cell, results) => {
|
|
1391
|
+
for (const result of results) {
|
|
1392
|
+
const report = runReport(cell, result, suiteFor(authoringId(cell.eval)));
|
|
1393
|
+
if (values.json) console.log(JSON.stringify({
|
|
1394
|
+
cell: {
|
|
1395
|
+
key: cell.key,
|
|
1396
|
+
eval: authoringId(cell.eval),
|
|
1397
|
+
parameters: cell.parameters
|
|
1398
|
+
},
|
|
1399
|
+
outcome: report.outcome,
|
|
1400
|
+
result
|
|
1401
|
+
}));
|
|
1402
|
+
reporter.onRun(report);
|
|
1403
|
+
}
|
|
1460
1404
|
}
|
|
1461
1405
|
})));
|
|
1462
1406
|
} finally {
|
|
@@ -1466,7 +1410,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1466
1410
|
if (values.junit) {
|
|
1467
1411
|
const file = resolve(cwd, values.junit);
|
|
1468
1412
|
await mkdir(dirname(file), { recursive: true });
|
|
1469
|
-
await writeFile(file, junitReport(reporter.
|
|
1413
|
+
await writeFile(file, junitReport(reporter.runs()));
|
|
1470
1414
|
}
|
|
1471
1415
|
if (values.json) console.log(JSON.stringify({ summary: totals }));
|
|
1472
1416
|
else {
|
|
@@ -1498,7 +1442,7 @@ function projectEvalIds(registry) {
|
|
|
1498
1442
|
*/
|
|
1499
1443
|
async function tidyReports(options) {
|
|
1500
1444
|
const known = projectEvalIds(options.registry);
|
|
1501
|
-
const reports = createDashboardReportReader(options.reportRoot
|
|
1445
|
+
const reports = createDashboardReportReader(options.reportRoot);
|
|
1502
1446
|
const result = {
|
|
1503
1447
|
archived: [],
|
|
1504
1448
|
restored: [],
|
|
@@ -1529,19 +1473,19 @@ async function tidyReports(options) {
|
|
|
1529
1473
|
return result;
|
|
1530
1474
|
}
|
|
1531
1475
|
/**
|
|
1532
|
-
* Remove the
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1476
|
+
* Remove the run workspaces `keep` would not have kept: those of passed runs
|
|
1477
|
+
* by default. Unfinished runs are never touched, so a run in progress keeps
|
|
1478
|
+
* its workspace; workspaces without a report are left alone.
|
|
1535
1479
|
*/
|
|
1536
1480
|
async function tidySandboxes(options) {
|
|
1537
1481
|
const keep = options.keep ?? "failed";
|
|
1538
1482
|
if (keep === "all") return { removed: 0 };
|
|
1539
|
-
const reports = createDashboardReportReader(options.reportRoot
|
|
1483
|
+
const reports = createDashboardReportReader(options.reportRoot);
|
|
1540
1484
|
let removed = 0;
|
|
1541
|
-
for (const run of await reports.listRuns())
|
|
1542
|
-
if (
|
|
1543
|
-
if (keep === "failed" &&
|
|
1544
|
-
const workspace =
|
|
1485
|
+
for (const run of await reports.listRuns()) {
|
|
1486
|
+
if (run.status === "running") continue;
|
|
1487
|
+
if (keep === "failed" && run.status !== "passed") continue;
|
|
1488
|
+
const workspace = runWorkspacePath(options.sandboxRoot, run.id);
|
|
1545
1489
|
if (!await stat(workspace).catch(() => void 0)) continue;
|
|
1546
1490
|
await rm(workspace, {
|
|
1547
1491
|
recursive: true,
|
|
@@ -1601,11 +1545,22 @@ function noRunsMessage(selection, head, options) {
|
|
|
1601
1545
|
if (selection.running) return `No finished runs for ${at}: ${plural(selection.running, "run")} still running. Share again when they finish.`;
|
|
1602
1546
|
return `No runs for ${at}. Run \`evalkit run\` first${options.all ? "" : ", or pass --all to share runs from other commits"}.`;
|
|
1603
1547
|
}
|
|
1604
|
-
/**
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1548
|
+
/**
|
|
1549
|
+
* A share server API URL. The server may live below a path
|
|
1550
|
+
* (`https://example.com/evals`), so paths resolve below `share.url`.
|
|
1551
|
+
*/
|
|
1552
|
+
function shareApiUrl(serverUrl, path) {
|
|
1553
|
+
return new URL(path, serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`);
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Where to look after sharing: the commit, or the repository if several
|
|
1557
|
+
* commits were shared. Both come from a run URL the server returned
|
|
1558
|
+
* (`<repo pages>/-/<sha>/runs/<id>`), since only the server knows its layout.
|
|
1559
|
+
*/
|
|
1560
|
+
function shareTarget(serverUrl, runUrl, runs) {
|
|
1561
|
+
const commitUrl = runUrl?.replace(/\/runs\/[^/]+\/?$/, "");
|
|
1562
|
+
if (!commitUrl || commitUrl === runUrl) return serverUrl;
|
|
1563
|
+
return new Set(runs.map((run) => run.source.commit)).size === 1 ? commitUrl : commitUrl.replace(/\/-\/[0-9a-f]{40}$/, "") || serverUrl;
|
|
1609
1564
|
}
|
|
1610
1565
|
/** Build one run's upload bundle: run.json, catalog.json, and its snapshot. */
|
|
1611
1566
|
async function runBundle(options) {
|
|
@@ -1613,7 +1568,10 @@ async function runBundle(options) {
|
|
|
1613
1568
|
const encode = (value) => new TextEncoder().encode(`${JSON.stringify(value)}\n`);
|
|
1614
1569
|
files.push({
|
|
1615
1570
|
path: "run.json",
|
|
1616
|
-
data: encode(
|
|
1571
|
+
data: encode({
|
|
1572
|
+
snapshotVersion: 2,
|
|
1573
|
+
...options.run
|
|
1574
|
+
})
|
|
1617
1575
|
});
|
|
1618
1576
|
const saved = await readFile(resolve(options.reportRoot, options.run.id, "catalog.json")).catch(() => void 0);
|
|
1619
1577
|
files.push({
|
|
@@ -1658,7 +1616,7 @@ async function resolveShareHeaders(headers) {
|
|
|
1658
1616
|
}
|
|
1659
1617
|
/** Check that the URL is an EvalKit share server and the headers get through. */
|
|
1660
1618
|
async function checkShareServer(serverUrl, headers) {
|
|
1661
|
-
const response = await fetch(
|
|
1619
|
+
const response = await fetch(shareApiUrl(serverUrl, "api/v1/repos"), {
|
|
1662
1620
|
headers,
|
|
1663
1621
|
redirect: "manual"
|
|
1664
1622
|
});
|
|
@@ -1681,7 +1639,7 @@ async function uploadBundle(serverUrl, bundle, options = {}) {
|
|
|
1681
1639
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
|
|
1682
1640
|
}
|
|
1683
1641
|
async function postBundle(serverUrl, bundle, headers) {
|
|
1684
|
-
const response = await fetch(
|
|
1642
|
+
const response = await fetch(shareApiUrl(serverUrl, "api/v1/uploads"), {
|
|
1685
1643
|
method: "POST",
|
|
1686
1644
|
headers: {
|
|
1687
1645
|
...headers,
|
|
@@ -1716,7 +1674,7 @@ async function expectShareServer(response, serverUrl) {
|
|
|
1716
1674
|
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
1675
|
}
|
|
1718
1676
|
async function shareRuns(options) {
|
|
1719
|
-
const reports = createDashboardReportReader(options.reportRoot
|
|
1677
|
+
const reports = createDashboardReportReader(options.reportRoot);
|
|
1720
1678
|
return options.step("Uploading", async (progress) => {
|
|
1721
1679
|
const result = {
|
|
1722
1680
|
created: 0,
|
|
@@ -1737,6 +1695,7 @@ async function shareRuns(options) {
|
|
|
1737
1695
|
const uploaded = await uploadBundle(options.serverUrl, bundle, { headers: options.headers });
|
|
1738
1696
|
result.bytes += bundle.byteLength;
|
|
1739
1697
|
result[uploaded.status] += 1;
|
|
1698
|
+
result.url ??= uploaded.url;
|
|
1740
1699
|
} catch (error) {
|
|
1741
1700
|
if (error instanceof ShareServerError) throw error;
|
|
1742
1701
|
result.failed.push({
|
|
@@ -1789,7 +1748,7 @@ export default defineConfig({
|
|
|
1789
1748
|
"datasets/greeting/greeting.txt": "Hello\n",
|
|
1790
1749
|
"datasets/greeting.dataset.ts": `import { defineDataset } from '@leostera/evalkit';
|
|
1791
1750
|
|
|
1792
|
-
// Files the agent can read: copied into each
|
|
1751
|
+
// Files the agent can read: copied into each run's workspace under greeting/.
|
|
1793
1752
|
// Paths are relative to the project root.
|
|
1794
1753
|
export const greeting = defineDataset({
|
|
1795
1754
|
id: 'greeting',
|
|
@@ -1865,14 +1824,14 @@ bun run evalkit build # write a read-only, shareable s
|
|
|
1865
1824
|
Coding agents can use the EvalKit skills that ship with the package, in
|
|
1866
1825
|
\`node_modules/@leostera/evalkit/dist/skills/\` (start with \`evalkit/SKILL.md\`).
|
|
1867
1826
|
|
|
1868
|
-
EvalKit installs from npm. Reports,
|
|
1827
|
+
EvalKit installs from npm. Reports, run workspaces, and dashboard snapshots stay local
|
|
1869
1828
|
and are ignored by Git. A snapshot in \`_evalkit-dashboard/\` contains report messages,
|
|
1870
1829
|
scores, evidence, and candidate files (never evaluator workspaces): review it before sharing.
|
|
1871
1830
|
`;
|
|
1872
1831
|
}
|
|
1873
1832
|
//#endregion
|
|
1874
1833
|
//#region ../cli/src/new-project.ts
|
|
1875
|
-
const npmDependency = "^0.
|
|
1834
|
+
const npmDependency = "^0.9.0";
|
|
1876
1835
|
const ignored = [
|
|
1877
1836
|
"node_modules/",
|
|
1878
1837
|
"_evalkit-results/",
|
|
@@ -1996,24 +1955,59 @@ const FINISHED = /* @__PURE__ */ new Set([
|
|
|
1996
1955
|
function unitKey(run) {
|
|
1997
1956
|
return `${run.evalId}\0${canonicalParameters(run.parameters ?? {})}`;
|
|
1998
1957
|
}
|
|
1999
|
-
/**
|
|
1958
|
+
/**
|
|
1959
|
+
* What represents each unit: the latest finished run's batch (its finished
|
|
1960
|
+
* runs), or that run alone; else the latest run at all.
|
|
1961
|
+
*/
|
|
2000
1962
|
function latestByUnit(runs) {
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
const ordered = [...runs].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
1963
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1964
|
+
const ordered = [...runs].filter((run) => run.status !== "running").sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2004
1965
|
for (const run of ordered) {
|
|
2005
|
-
if (run.status === "running") continue;
|
|
2006
1966
|
const key = unitKey(run);
|
|
2007
|
-
const current =
|
|
2008
|
-
if (!current)
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
1967
|
+
const current = latest.get(key);
|
|
1968
|
+
if (!current || !FINISHED.has(current.status) && FINISHED.has(run.status)) latest.set(key, run);
|
|
1969
|
+
}
|
|
1970
|
+
const byUnit = /* @__PURE__ */ new Map();
|
|
1971
|
+
let used = 0;
|
|
1972
|
+
for (const [key, run] of latest) {
|
|
1973
|
+
const members = run.batch ? ordered.filter((other) => other.batch?.id === run.batch.id && FINISHED.has(other.status)) : [run];
|
|
1974
|
+
used += members.length;
|
|
1975
|
+
byUnit.set(key, side(members));
|
|
2013
1976
|
}
|
|
2014
1977
|
return {
|
|
2015
1978
|
byUnit,
|
|
2016
|
-
ignored
|
|
1979
|
+
ignored: ordered.length - used
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
function side(runs) {
|
|
1983
|
+
const scored = runs.filter((run) => run.status !== "errored");
|
|
1984
|
+
const passed = scored.filter((run) => run.status === "passed").length;
|
|
1985
|
+
const values = /* @__PURE__ */ new Map();
|
|
1986
|
+
for (const run of runs) for (const scorer of run.scorers ?? []) {
|
|
1987
|
+
const key = `${scorer.kind}\0${scorer.name}`;
|
|
1988
|
+
const entry = values.get(key) ?? {
|
|
1989
|
+
scorer,
|
|
1990
|
+
sum: 0,
|
|
1991
|
+
n: 0
|
|
1992
|
+
};
|
|
1993
|
+
if (scorer.value !== void 0 && run.status !== "errored") {
|
|
1994
|
+
entry.sum += scorer.value;
|
|
1995
|
+
entry.n += 1;
|
|
1996
|
+
}
|
|
1997
|
+
values.set(key, entry);
|
|
1998
|
+
}
|
|
1999
|
+
const first = runs[0];
|
|
2000
|
+
return {
|
|
2001
|
+
...first,
|
|
2002
|
+
status: !scored.length ? first.status : passed === runs.length ? "passed" : "failed",
|
|
2003
|
+
scorers: [...values.values()].map(({ scorer, sum, n }) => ({
|
|
2004
|
+
name: scorer.name,
|
|
2005
|
+
kind: scorer.kind,
|
|
2006
|
+
...n ? { value: sum / n } : {}
|
|
2007
|
+
})),
|
|
2008
|
+
passed,
|
|
2009
|
+
total: runs.length,
|
|
2010
|
+
runs: [...runs]
|
|
2017
2011
|
};
|
|
2018
2012
|
}
|
|
2019
2013
|
function scorerSignature(run) {
|
|
@@ -2030,8 +2024,9 @@ function classify(base, head) {
|
|
|
2030
2024
|
change: "not-comparable",
|
|
2031
2025
|
reason: "different scorers"
|
|
2032
2026
|
};
|
|
2033
|
-
|
|
2034
|
-
if (
|
|
2027
|
+
const rate = (side) => side.passed / side.total;
|
|
2028
|
+
if (rate(head) < rate(base)) return { change: "regressed" };
|
|
2029
|
+
if (rate(head) > rate(base)) return { change: "fixed" };
|
|
2035
2030
|
return { change: "unchanged" };
|
|
2036
2031
|
}
|
|
2037
2032
|
function label(run, suite) {
|
|
@@ -2056,8 +2051,8 @@ function suiteScorers(units) {
|
|
|
2056
2051
|
name: scorer.name,
|
|
2057
2052
|
kind: scorer.kind
|
|
2058
2053
|
});
|
|
2059
|
-
if (scorer.
|
|
2060
|
-
sides[side].set(key, [...sides[side].get(key) ?? [], scorer.
|
|
2054
|
+
if (scorer.value === void 0) continue;
|
|
2055
|
+
sides[side].set(key, [...sides[side].get(key) ?? [], scorer.value]);
|
|
2061
2056
|
}
|
|
2062
2057
|
}
|
|
2063
2058
|
for (const unit of units) {
|
|
@@ -2111,7 +2106,11 @@ function compareRuns(baseRuns, headRuns) {
|
|
|
2111
2106
|
const changedDatasets = /* @__PURE__ */ new Set();
|
|
2112
2107
|
for (const unit of units) {
|
|
2113
2108
|
if (!unit.base || !unit.head) continue;
|
|
2114
|
-
const digests = (
|
|
2109
|
+
const digests = (side) => {
|
|
2110
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2111
|
+
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));
|
|
2112
|
+
return new Map([...byId].map(([id, set]) => [id, [...set].sort().join(",")]));
|
|
2113
|
+
};
|
|
2115
2114
|
const before = digests(unit.base);
|
|
2116
2115
|
const after = digests(unit.head);
|
|
2117
2116
|
for (const id of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) if (before.get(id) !== after.get(id)) changedDatasets.add(id);
|
|
@@ -2120,10 +2119,10 @@ function compareRuns(baseRuns, headRuns) {
|
|
|
2120
2119
|
for (const unit of units) bySuite.set(unit.suite, [...bySuite.get(unit.suite) ?? [], unit]);
|
|
2121
2120
|
const suites = [...bySuite].map(([suite, members]) => {
|
|
2122
2121
|
const side = (pick) => {
|
|
2123
|
-
const
|
|
2122
|
+
const sides = members.flatMap((unit) => unit[pick] ? [unit[pick]] : []);
|
|
2124
2123
|
return {
|
|
2125
|
-
passed:
|
|
2126
|
-
total:
|
|
2124
|
+
passed: sides.reduce((sum, entry) => sum + entry.passed, 0),
|
|
2125
|
+
total: sides.reduce((sum, entry) => sum + entry.total, 0)
|
|
2127
2126
|
};
|
|
2128
2127
|
};
|
|
2129
2128
|
return {
|
|
@@ -2232,8 +2231,8 @@ function headRuns(runs, head, options) {
|
|
|
2232
2231
|
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
2232
|
}
|
|
2234
2233
|
/** 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
|
|
2234
|
+
async function baseRunsFromDirectory(directory) {
|
|
2235
|
+
return (await createDashboardReportReader(directory).listRuns()).filter((run) => !run.source?.dirty && !run.archived);
|
|
2237
2236
|
}
|
|
2238
2237
|
/** A full SHA, a unique prefix of at least 7 characters, or a ref's latest shared commit. */
|
|
2239
2238
|
function resolveSharedRef(commits, ref) {
|
|
@@ -2260,7 +2259,7 @@ async function getJson(url, serverUrl, headers) {
|
|
|
2260
2259
|
/** Base runs shared for `ref` of this repository; `undefined` when the base was never shared. */
|
|
2261
2260
|
async function baseRunsFromServer(options) {
|
|
2262
2261
|
const repo = repoKey(options.head);
|
|
2263
|
-
const api = (path) =>
|
|
2262
|
+
const api = (path) => shareApiUrl(options.serverUrl, `api/v1/repos/${repo}/-/${path}`);
|
|
2264
2263
|
const listed = await getJson(api("commits"), options.serverUrl, options.headers);
|
|
2265
2264
|
const commits = Array.isArray(listed?.commits) ? listed.commits : [];
|
|
2266
2265
|
const commit = resolveSharedRef(commits, options.ref);
|
|
@@ -5257,11 +5256,12 @@ function createProgram(actions) {
|
|
|
5257
5256
|
showGlobalOptions: false
|
|
5258
5257
|
}).addHelpText("after", `\nRun from your eval project's directory. Docs: https://evalkit.leostera.dev/docs/\n`);
|
|
5259
5258
|
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
|
|
5259
|
+
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
5260
|
"evalkit run",
|
|
5262
5261
|
"evalkit run greeting -n 5",
|
|
5263
5262
|
"evalkit run -s billing --junit _evalkit-results/junit.xml",
|
|
5264
|
-
"evalkit run -m models --model model-a,model-b --dry-run"
|
|
5263
|
+
"evalkit run -m models --model model-a,model-b --dry-run",
|
|
5264
|
+
"evalkit run -m models -s testing -a model=luna -a toolchain=riot"
|
|
5265
5265
|
])).action((evalIds, options) => {
|
|
5266
5266
|
setRunnerLogLevel(options.logLevel);
|
|
5267
5267
|
return actions.run(runInput(evalIds, options));
|
|
@@ -5275,11 +5275,12 @@ function createProgram(actions) {
|
|
|
5275
5275
|
return actions.build(options);
|
|
5276
5276
|
});
|
|
5277
5277
|
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));
|
|
5278
|
+
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
5279
|
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
5280
|
...options,
|
|
5280
5281
|
all: Boolean(options.all),
|
|
5281
5282
|
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));
|
|
5283
|
+
})).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
5284
|
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
5285
|
...options,
|
|
5285
5286
|
failOn: options.failOn ?? [],
|
|
@@ -5303,7 +5304,7 @@ function runInput(evalIds, flags) {
|
|
|
5303
5304
|
"chat-timeout-ms": flags.chatTimeoutMs,
|
|
5304
5305
|
"turn-budget": flags.turnBudget,
|
|
5305
5306
|
concurrency: flags.concurrency,
|
|
5306
|
-
|
|
5307
|
+
repeat: flags.repeat,
|
|
5307
5308
|
json: flags.json,
|
|
5308
5309
|
"dry-run": flags.dryRun,
|
|
5309
5310
|
all: flags.all,
|
|
@@ -5343,6 +5344,7 @@ async function runEvals(options) {
|
|
|
5343
5344
|
workspaceRoot: sandboxRoot,
|
|
5344
5345
|
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5345
5346
|
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5347
|
+
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5346
5348
|
suiteId: options.suiteId,
|
|
5347
5349
|
runtime: runtimeFor(evaluation),
|
|
5348
5350
|
concurrency: options.concurrency,
|
|
@@ -5362,14 +5364,14 @@ async function runProvenance(registry) {
|
|
|
5362
5364
|
catalog: runCatalog(registry, projectRoot)
|
|
5363
5365
|
};
|
|
5364
5366
|
}
|
|
5365
|
-
function registerMatrixRoutes(app, registry, dashboardMatrix,
|
|
5367
|
+
function registerMatrixRoutes(app, registry, dashboardMatrix, repeat) {
|
|
5366
5368
|
let matrixRunning = false;
|
|
5367
5369
|
app.get("/v1/matrix", (context) => context.json({ matrix: dashboardMatrix ? {
|
|
5368
5370
|
id: dashboardMatrix.id,
|
|
5369
5371
|
parameters: dashboardMatrix.parameters,
|
|
5370
5372
|
...dashboardMatrix.cases?.length || dashboardMatrix.exclude?.length ? { dimensions: [...Object.keys(dashboardMatrix.cases?.[0] ?? {}), ...Object.keys(dashboardMatrix.parameters)] } : {},
|
|
5371
5373
|
...dashboardMatrix.cases?.length || dashboardMatrix.exclude?.length ? { constrained: true } : {},
|
|
5372
|
-
...
|
|
5374
|
+
...repeat ? { repeat } : {}
|
|
5373
5375
|
} : null }));
|
|
5374
5376
|
app.get("/v1/matrix/cells", (context) => {
|
|
5375
5377
|
if (!dashboardMatrix) return context.text("Project has no matrix", 404);
|
|
@@ -5409,12 +5411,13 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
|
|
|
5409
5411
|
runProvenance(registry).then((provenance) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
|
|
5410
5412
|
selection,
|
|
5411
5413
|
concurrency: 1,
|
|
5412
|
-
|
|
5414
|
+
...repeat ? { repeat } : {},
|
|
5413
5415
|
...suiteId ? { suiteId } : {},
|
|
5414
5416
|
report: localReportStore(reportRoot),
|
|
5415
5417
|
workspaceRoot: sandboxRoot,
|
|
5416
5418
|
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5417
5419
|
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5420
|
+
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5418
5421
|
...provenance,
|
|
5419
5422
|
onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
|
|
5420
5423
|
})))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
@@ -5465,7 +5468,7 @@ function registerReportRoutes(app, registry, reports) {
|
|
|
5465
5468
|
if (!dataset) return context.text("Unknown dataset", 404);
|
|
5466
5469
|
const file = context.req.param("path");
|
|
5467
5470
|
try {
|
|
5468
|
-
return new Response(await staticDatasetFile(dataset, projectRoot, file), { headers:
|
|
5471
|
+
return new Response(await staticDatasetFile(dataset, projectRoot, file), { headers: UNTRUSTED_FILE });
|
|
5469
5472
|
} catch (error) {
|
|
5470
5473
|
return context.text(error instanceof Error ? error.message : "Unable to read dataset file", 404);
|
|
5471
5474
|
}
|
|
@@ -5477,44 +5480,44 @@ function registerReportRoutes(app, registry, reports) {
|
|
|
5477
5480
|
app.get("/v1/evals", (context) => context.json({ evals: registry.metadata() }));
|
|
5478
5481
|
app.get("/v1/suites", (context) => context.json({ suites: registry.suiteMetadata() }));
|
|
5479
5482
|
app.get("/v1/runs", async (context) => context.json({ runs: await reports.listRuns() }));
|
|
5480
|
-
app.get("/v1/runs/:runId
|
|
5483
|
+
app.get("/v1/runs/:runId", async (context) => {
|
|
5481
5484
|
try {
|
|
5482
|
-
return context.json(
|
|
5485
|
+
return context.json(await reports.runDetail(context.req.param("runId")));
|
|
5483
5486
|
} catch (error) {
|
|
5484
|
-
return context.text(error instanceof Error ? error.message : "
|
|
5487
|
+
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5485
5488
|
}
|
|
5486
5489
|
});
|
|
5487
|
-
app.get("/v1/runs/:runId/
|
|
5490
|
+
app.get("/v1/runs/:runId/workspace", async (context) => {
|
|
5488
5491
|
try {
|
|
5489
|
-
return context.json(await reports.
|
|
5492
|
+
return context.json(await reports.listCandidateWorkspace(context.req.param("runId")));
|
|
5490
5493
|
} catch (error) {
|
|
5491
|
-
return context.text(error instanceof Error ? error.message : "
|
|
5494
|
+
return context.text(error instanceof Error ? error.message : "Unable to list workspace", 400);
|
|
5492
5495
|
}
|
|
5493
5496
|
});
|
|
5494
|
-
app.get("/v1/runs/:runId/
|
|
5497
|
+
app.get("/v1/runs/:runId/workspace/:path{.+}", async (context) => {
|
|
5495
5498
|
try {
|
|
5496
|
-
return
|
|
5499
|
+
return await reports.candidateFile(context.req.param("runId"), context.req.param("path"));
|
|
5497
5500
|
} catch (error) {
|
|
5498
|
-
return context.text(error instanceof Error ? error.message : "Unable to
|
|
5501
|
+
return context.text(error instanceof Error ? error.message : "Unable to read workspace file", 400);
|
|
5499
5502
|
}
|
|
5500
5503
|
});
|
|
5501
|
-
app.get("/v1/runs/:runId/
|
|
5504
|
+
app.get("/v1/runs/:runId/attachments/:name{.+}", async (context) => {
|
|
5502
5505
|
try {
|
|
5503
|
-
return await reports.
|
|
5506
|
+
return await reports.attachmentFile(context.req.param("runId"), context.req.param("name"));
|
|
5504
5507
|
} catch (error) {
|
|
5505
|
-
return context.text(error instanceof Error ? error.message : "Unable to read
|
|
5508
|
+
return context.text(error instanceof Error ? error.message : "Unable to read attachment", 400);
|
|
5506
5509
|
}
|
|
5507
5510
|
});
|
|
5508
|
-
app.get("/v1/runs/:runId/
|
|
5511
|
+
app.get("/v1/runs/:runId/artifacts", async (context) => {
|
|
5509
5512
|
try {
|
|
5510
|
-
return context.json({ artifacts: await reports.listArtifacts(context.req.param("runId")
|
|
5513
|
+
return context.json({ artifacts: await reports.listArtifacts(context.req.param("runId")) });
|
|
5511
5514
|
} catch (error) {
|
|
5512
5515
|
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5513
5516
|
}
|
|
5514
5517
|
});
|
|
5515
|
-
app.get("/v1/runs/:runId/
|
|
5518
|
+
app.get("/v1/runs/:runId/events", async (context) => {
|
|
5516
5519
|
try {
|
|
5517
|
-
return context.json({ events: await reports.trajectory(context.req.param("runId")
|
|
5520
|
+
return context.json({ events: await reports.trajectory(context.req.param("runId")) });
|
|
5518
5521
|
} catch (error) {
|
|
5519
5522
|
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5520
5523
|
}
|
|
@@ -5568,11 +5571,11 @@ async function serveDashboard({ port, open }) {
|
|
|
5568
5571
|
const dashboardRoot = dashboardAssets();
|
|
5569
5572
|
const app = new Hono();
|
|
5570
5573
|
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
|
|
5574
|
+
registerMatrixRoutes(app, registry, dashboardMatrix, project?.config.execution?.repeat);
|
|
5575
|
+
registerReportRoutes(app, registry, createDashboardReportReader(reportRoot));
|
|
5573
5576
|
registerDashboardAssets(app, dashboardRoot);
|
|
5574
5577
|
const url = `http://localhost:${(await listen(app.fetch, port, 4317)).port}`;
|
|
5575
|
-
const reader = createDashboardReportReader(reportRoot
|
|
5578
|
+
const reader = createDashboardReportReader(reportRoot);
|
|
5576
5579
|
const runs = (await reader.listRuns()).length;
|
|
5577
5580
|
const legacy = reader.legacyRunIds().length;
|
|
5578
5581
|
const matrixCells = dashboardMatrix?.count({ parameters: {} });
|
|
@@ -5581,7 +5584,7 @@ async function serveDashboard({ port, open }) {
|
|
|
5581
5584
|
printLoadErrors(project?.loadErrors ?? []);
|
|
5582
5585
|
row("Local:", cyan(url));
|
|
5583
5586
|
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
|
|
5587
|
+
row("Reports:", `${relative(process.cwd(), reportRoot) || "."} ${dim(`(${plural(runs, "run")}${legacy ? `, ${legacy} from an older EvalKit hidden; run \`evalkit upgrade\`` : ""})`)}`);
|
|
5585
5588
|
console.log(`\n ${dim("Runs started from the dashboard are logged below. Press Ctrl+C to stop.")}\n`);
|
|
5586
5589
|
if (open) openInBrowser(url);
|
|
5587
5590
|
}
|
|
@@ -5639,7 +5642,7 @@ async function loadProjectStep(configPath) {
|
|
|
5639
5642
|
*/
|
|
5640
5643
|
async function shouldRunEvals(run) {
|
|
5641
5644
|
if (run !== void 0) return run;
|
|
5642
|
-
const runs = (await createDashboardReportReader(reportRoot
|
|
5645
|
+
const runs = (await createDashboardReportReader(reportRoot).listRuns()).filter((entry) => !entry.archived).length;
|
|
5643
5646
|
if (!runs) {
|
|
5644
5647
|
console.log(` ${cyan("◇")} No results yet, so running the evals to have data to build.`);
|
|
5645
5648
|
return true;
|
|
@@ -5672,7 +5675,7 @@ async function tidyAction(options) {
|
|
|
5672
5675
|
}), (value) => {
|
|
5673
5676
|
const keep = project.config.keepSandboxes ?? "failed";
|
|
5674
5677
|
if (keep === "all") return "all kept (keepSandboxes: 'all')";
|
|
5675
|
-
return `${plural(value.removed, "workspace")} of ${keep === "none" ? "finished" : "passed"}
|
|
5678
|
+
return `${plural(value.removed, "workspace")} of ${keep === "none" ? "finished" : "passed"} runs removed`;
|
|
5676
5679
|
});
|
|
5677
5680
|
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
5681
|
console.log("");
|
|
@@ -5683,16 +5686,34 @@ async function tidyAction(options) {
|
|
|
5683
5686
|
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
5687
|
console.log("");
|
|
5685
5688
|
}
|
|
5689
|
+
async function upgradeAction(options) {
|
|
5690
|
+
banner("upgrade", VERSION);
|
|
5691
|
+
const { root, config } = await loadConfig(projectRoot, options.config);
|
|
5692
|
+
reportRoot = resolve(root, config.reportDir ?? "_evalkit-results");
|
|
5693
|
+
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"}`);
|
|
5694
|
+
console.log("");
|
|
5695
|
+
if (!report.upgraded) console.log(` ${cyan("✓")} Every run is already in the current format.`);
|
|
5696
|
+
else {
|
|
5697
|
+
const into = `${plural(report.upgraded, "run")} from older EvalKit versions into ${plural(report.runs, "run")}`;
|
|
5698
|
+
console.log(options.dryRun ? ` ${cyan("◆")} Would upgrade ${into}. Nothing was changed.` : ` ${cyan("✓")} Upgraded ${into}.`);
|
|
5699
|
+
if (report.batches) console.log(` ${dim(`${plural(report.batches, "run")} had more than one trial; each trial is now a run in a batch.`)}`);
|
|
5700
|
+
}
|
|
5701
|
+
if (report.running.length) console.log(` ${yellow("◆")} Left ${plural(report.running.length, "run")} still running alone; upgrade again when they finish.`);
|
|
5702
|
+
console.log("");
|
|
5703
|
+
}
|
|
5686
5704
|
async function shareServeAction(options) {
|
|
5687
5705
|
const root = resolve(process.cwd(), options.dir);
|
|
5688
|
-
const
|
|
5706
|
+
const url = `http://localhost:${(await listen((await createDirectoryShareApp({
|
|
5689
5707
|
dir: root,
|
|
5690
|
-
dashboardRoot: dashboardAssets()
|
|
5691
|
-
|
|
5708
|
+
dashboardRoot: dashboardAssets(),
|
|
5709
|
+
...options.basePath ? { basePath: options.basePath } : {},
|
|
5710
|
+
...options.repo ? { repo: options.repo } : {}
|
|
5711
|
+
})).fetch, options.port, 4318)).port}${normalizeBasePath(options.basePath)}`;
|
|
5692
5712
|
banner("share serve", VERSION);
|
|
5693
|
-
console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(
|
|
5713
|
+
console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(url)}`);
|
|
5714
|
+
if (options.repo) console.log(` ${cyan("➜")} ${bold("Repo:".padEnd(9))}${options.repo}`);
|
|
5694
5715
|
console.log(` ${cyan("➜")} ${bold("Storage:".padEnd(9))}${relative(process.cwd(), root) || "."}`);
|
|
5695
|
-
console.log(`\n ${dim(`Upload with share.url = "
|
|
5716
|
+
console.log(`\n ${dim(`Upload with share.url = "${url}". No authentication: keep it private.`)}\n`);
|
|
5696
5717
|
}
|
|
5697
5718
|
async function shareAction(options) {
|
|
5698
5719
|
const { all, allowDirty } = options;
|
|
@@ -5708,7 +5729,7 @@ async function shareAction(options) {
|
|
|
5708
5729
|
] }), (source) => source ? `${repoKey(source)} @ ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : "not a git repository");
|
|
5709
5730
|
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
5731
|
const registry = project.registry;
|
|
5711
|
-
const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot
|
|
5732
|
+
const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot).listRuns(), head, {
|
|
5712
5733
|
all,
|
|
5713
5734
|
allowDirty
|
|
5714
5735
|
}), (value) => selectionSummary(value, { all }));
|
|
@@ -5730,7 +5751,7 @@ async function shareAction(options) {
|
|
|
5730
5751
|
step
|
|
5731
5752
|
});
|
|
5732
5753
|
for (const failure of result.failed) console.log(` ${yellow("✗")} ${failure.runId.slice(0, 8)} ${dim(failure.error)}`);
|
|
5733
|
-
const target = shareTarget(serverUrl,
|
|
5754
|
+
const target = shareTarget(serverUrl, result.url, selection.selected);
|
|
5734
5755
|
console.log(`\n ${bold(result.failed.length ? "Shared with errors" : "Shared")} in ${duration(performance.now() - started)}\n`);
|
|
5735
5756
|
console.log(` ${cyan("➜")} ${cyan(target)}\n`);
|
|
5736
5757
|
if (result.failed.length) process.exitCode = 1;
|
|
@@ -5741,17 +5762,16 @@ async function compareAction(options) {
|
|
|
5741
5762
|
const say = options.json ? () => {} : (line = "") => console.log(line);
|
|
5742
5763
|
if (!options.json) banner("compare", VERSION);
|
|
5743
5764
|
await useProject(options.config);
|
|
5744
|
-
const registry = project.registry;
|
|
5745
5765
|
const head = await detectSource(projectRoot, { outputDirs: [
|
|
5746
5766
|
reportRoot,
|
|
5747
5767
|
sandboxRoot,
|
|
5748
5768
|
resolve(projectRoot, "_evalkit-dashboard")
|
|
5749
5769
|
] });
|
|
5750
5770
|
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
|
|
5771
|
+
const headSide = headRuns(await createDashboardReportReader(reportRoot).listRuns(), head, { allowDirty: options.allowDirty });
|
|
5752
5772
|
let base;
|
|
5753
5773
|
if (options.baseDir) {
|
|
5754
|
-
const runs = await baseRunsFromDirectory(resolve(process.cwd(), options.baseDir)
|
|
5774
|
+
const runs = await baseRunsFromDirectory(resolve(process.cwd(), options.baseDir));
|
|
5755
5775
|
base = runs.length ? {
|
|
5756
5776
|
label: options.baseDir,
|
|
5757
5777
|
runs
|
|
@@ -5825,6 +5845,7 @@ try {
|
|
|
5825
5845
|
dev: devAction,
|
|
5826
5846
|
build: buildAction,
|
|
5827
5847
|
tidy: tidyAction,
|
|
5848
|
+
upgrade: upgradeAction,
|
|
5828
5849
|
share: shareAction,
|
|
5829
5850
|
shareServe: shareServeAction,
|
|
5830
5851
|
compare: compareAction
|