@leostera/evalkit 0.4.0 → 0.6.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 +67 -1
- package/README.md +10 -8
- package/dist/cli.mjs +1287 -266
- package/dist/dashboard/assets/index-BK9x-zoi.css +2 -0
- package/dist/dashboard/assets/index-DzsDLGHx.js +32 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/files-C-U0-PWW.mjs +0 -2
- package/dist/index-db-vbqEGFaU.mjs +0 -2
- package/dist/index.d.mts +1344 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -4
- package/dist/runner.d.mts +807 -11
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +76 -1
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +0 -2
- package/dist/share.d.mts +149 -6
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +1 -1
- package/dist/skills/evalkit/SKILL.md +32 -0
- package/dist/skills/evalkit-agents/SKILL.md +16 -0
- package/dist/skills/evalkit-datasets/SKILL.md +17 -0
- package/dist/skills/evalkit-evals/SKILL.md +19 -0
- package/dist/skills/evalkit-judges/SKILL.md +17 -0
- package/dist/skills/evalkit-matrices/SKILL.md +14 -0
- package/dist/skills/evalkit-results/SKILL.md +14 -0
- package/dist/{src-DoPs9Yug.mjs → src-CVSCn3bZ.mjs} +445 -115
- package/dist/{src-DzZueFn3.mjs → src-CZnDXP1K.mjs} +46 -10
- package/dist/{src-ButXPY8P.mjs → src-D4tCwxNb.mjs} +418 -94
- package/package.json +3 -2
- package/dist/cli.mjs.map +0 -1
- package/dist/dashboard/assets/index-D9NtA46z.css +0 -2
- package/dist/dashboard/assets/index-DrBwLKJD.js +0 -30
- package/dist/files-C-U0-PWW.mjs.map +0 -1
- package/dist/index-U6TUTBf7.d.mts +0 -1019
- package/dist/index-U6TUTBf7.d.mts.map +0 -1
- package/dist/index-db-HdZXelOS.d.mts +0 -142
- package/dist/index-db-HdZXelOS.d.mts.map +0 -1
- package/dist/index-db-vbqEGFaU.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/share-worker.mjs.map +0 -1
- package/dist/src-ButXPY8P.mjs.map +0 -1
- package/dist/src-DoPs9Yug.mjs.map +0 -1
- package/dist/src-DzZueFn3.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
':' //; exec "${EVALKIT_RUNTIME:-$(command -v bun || command -v node)}" "$0" "$@"
|
|
3
|
+
import { L as defineEvalMatrix, R as canonicalParameters, d as recordError, f as registerEvals, i as authoringId, r as agentRuntimes, s as defineSuite } from "./src-D4tCwxNb.mjs";
|
|
4
|
+
import { a as readRunSummary, d as staticDatasetFile, f as staticDatasetFiles, i as readRunManifest, l as readTrialSummary, n as runMatrix, o as readTrialEvents, r as ownerIsGone, s as readTrialManifest, t as runEval, u as localReportStore } from "./src-CVSCn3bZ.mjs";
|
|
4
5
|
import { c as dashboardIndex, d as encodeBundle, o as repoKey } from "./index-db-vbqEGFaU.mjs";
|
|
5
|
-
import { t as createDirectoryShareApp } from "./src-
|
|
6
|
+
import { t as createDirectoryShareApp } from "./src-CZnDXP1K.mjs";
|
|
6
7
|
import * as Schema from "effect/Schema";
|
|
7
8
|
import { Effect, LogLevel, Logger, Option } from "effect";
|
|
9
|
+
import * as OS from "node:os";
|
|
8
10
|
import { cp, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
9
11
|
import * as Path from "node:path";
|
|
10
12
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
11
|
-
import * as OS from "node:os";
|
|
12
13
|
import * as Crypto from "node:crypto";
|
|
13
14
|
import * as NFS from "node:fs";
|
|
14
15
|
import { existsSync } from "node:fs";
|
|
@@ -67,9 +68,10 @@ import * as FiberSet from "effect/FiberSet";
|
|
|
67
68
|
import "effect/Fiber";
|
|
68
69
|
import "effect/Pool";
|
|
69
70
|
import * as MutableRef from "effect/MutableRef";
|
|
71
|
+
import { createServer } from "node:http";
|
|
70
72
|
import { parseArgs, promisify } from "node:util";
|
|
71
73
|
import * as ChildProcess from "node:child_process";
|
|
72
|
-
import { execFile } from "node:child_process";
|
|
74
|
+
import { execFile, spawn } from "node:child_process";
|
|
73
75
|
import * as RcRef from "effect/RcRef";
|
|
74
76
|
import * as readline from "node:readline";
|
|
75
77
|
import { Hono } from "hono";
|
|
@@ -5792,7 +5794,7 @@ const merge = (map1, map2) => {
|
|
|
5792
5794
|
}
|
|
5793
5795
|
return map1;
|
|
5794
5796
|
};
|
|
5795
|
-
const escape = (string) => escapeSingleQuoted(string.replaceAll("\\", "\\\\")).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll(":", "\\:").replaceAll("$", "\\$").replaceAll("`", "\\`").replaceAll("(", "\\(").replaceAll(")", "\\)");
|
|
5797
|
+
const escape$1 = (string) => escapeSingleQuoted(string.replaceAll("\\", "\\\\")).replaceAll("[", "\\[").replaceAll("]", "\\]").replaceAll(":", "\\:").replaceAll("$", "\\$").replaceAll("`", "\\`").replaceAll("(", "\\(").replaceAll(")", "\\)");
|
|
5796
5798
|
const getShortDescription$1 = (self) => {
|
|
5797
5799
|
switch (self._tag) {
|
|
5798
5800
|
case "Empty":
|
|
@@ -5860,7 +5862,7 @@ const getZshCompletions$1 = (self, state = {
|
|
|
5860
5862
|
const possibleValues = getZshCompletions$3(self.primitiveType);
|
|
5861
5863
|
const multiple = state.multiple ? "*" : "";
|
|
5862
5864
|
const conflicts = Arr.isNonEmptyReadonlyArray(state.conflicts) ? `(${Arr.join(state.conflicts, " ")})` : "";
|
|
5863
|
-
return Arr.map(names, (name) => `${conflicts}${multiple}${name}[${escape(description)}]${possibleValues}`);
|
|
5865
|
+
return Arr.map(names, (name) => `${conflicts}${multiple}${name}[${escape$1(description)}]${possibleValues}`);
|
|
5864
5866
|
}
|
|
5865
5867
|
case "KeyValueMap": return getZshCompletions$1(self.argumentOption, {
|
|
5866
5868
|
...state,
|
|
@@ -8069,8 +8071,193 @@ const layerManager = /* @__PURE__ */ Layer.provide(layerManager$2, layerWorker);
|
|
|
8069
8071
|
*/
|
|
8070
8072
|
const layer = /*#__PURE__*/ pipe(/*#__PURE__*/ Layer.mergeAll(layer$3, layer$5, layer$1, layerManager), /*#__PURE__*/ Layer.provideMerge(layer$7));
|
|
8071
8073
|
//#endregion
|
|
8074
|
+
//#region ../cli/src/dataset-files.ts
|
|
8075
|
+
/** The project's datasets by ID; the first definition wins when IDs repeat. */
|
|
8076
|
+
function projectDatasets(registry) {
|
|
8077
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8078
|
+
for (const evaluation of registry.evals) for (const dataset of evaluation.datasets ?? []) if (!byId.has(dataset.id)) byId.set(dataset.id, dataset);
|
|
8079
|
+
return byId;
|
|
8080
|
+
}
|
|
8081
|
+
async function datasetListing(dataset, root) {
|
|
8082
|
+
const files = await staticDatasetFiles(dataset, root);
|
|
8083
|
+
return files ? {
|
|
8084
|
+
kind: "static",
|
|
8085
|
+
mount: dataset.mount,
|
|
8086
|
+
files
|
|
8087
|
+
} : {
|
|
8088
|
+
kind: "computed",
|
|
8089
|
+
mount: dataset.mount
|
|
8090
|
+
};
|
|
8091
|
+
}
|
|
8092
|
+
//#endregion
|
|
8093
|
+
//#region ../cli/src/source.ts
|
|
8094
|
+
const exec = promisify(execFile);
|
|
8095
|
+
/**
|
|
8096
|
+
* Normalize a git remote to a stable repository key: lowercase host, no
|
|
8097
|
+
* credentials, port, scheme, `.git`, or trailing slash.
|
|
8098
|
+
*
|
|
8099
|
+
* git@GitLab.example.com:group/sub/repo.git → gitlab.example.com/group/sub/repo
|
|
8100
|
+
* https://user:token@github.com/o/r/ → github.com/o/r
|
|
8101
|
+
* /srv/repos/thing.git (no host) → local/thing
|
|
8102
|
+
*/
|
|
8103
|
+
function normalizeRepoUrl(url) {
|
|
8104
|
+
const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
|
|
8105
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/.exec(trimmed);
|
|
8106
|
+
if (scp && !/^[a-z]+:\/\//i.test(trimmed)) return `${scp[1].toLowerCase()}/${scp[2].replace(/^\/+/, "")}`;
|
|
8107
|
+
try {
|
|
8108
|
+
const parsed = new URL(trimmed);
|
|
8109
|
+
if (parsed.protocol !== "file:" && parsed.hostname) return `${parsed.hostname.toLowerCase()}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
8110
|
+
} catch {}
|
|
8111
|
+
return `local/${basename(trimmed)}`;
|
|
8112
|
+
}
|
|
8113
|
+
async function git(cwd, ...args) {
|
|
8114
|
+
try {
|
|
8115
|
+
const { stdout } = await exec("git", [
|
|
8116
|
+
"-C",
|
|
8117
|
+
cwd,
|
|
8118
|
+
...args
|
|
8119
|
+
], { timeout: 1e4 });
|
|
8120
|
+
return stdout.trim() || void 0;
|
|
8121
|
+
} catch {
|
|
8122
|
+
return;
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
const posix = (path) => path.split(sep).join("/");
|
|
8126
|
+
const projectPath = (root, projectRoot) => root ? posix(relative(root, projectRoot)) : "";
|
|
8127
|
+
function ciSource(env, projectRoot) {
|
|
8128
|
+
if (env.EVALKIT_SOURCE_REPO && env.EVALKIT_SOURCE_COMMIT) return {
|
|
8129
|
+
repo: normalizeRepoUrl(env.EVALKIT_SOURCE_REPO),
|
|
8130
|
+
project: env.EVALKIT_SOURCE_PROJECT ?? "",
|
|
8131
|
+
commit: env.EVALKIT_SOURCE_COMMIT,
|
|
8132
|
+
...env.EVALKIT_SOURCE_REF ? { ref: env.EVALKIT_SOURCE_REF } : {},
|
|
8133
|
+
...env.EVALKIT_SOURCE_COMMITTED_AT ? { committedAt: env.EVALKIT_SOURCE_COMMITTED_AT } : {}
|
|
8134
|
+
};
|
|
8135
|
+
if (env.GITLAB_CI === "true" && env.CI_PROJECT_URL && env.CI_COMMIT_SHA) return {
|
|
8136
|
+
repo: normalizeRepoUrl(env.CI_PROJECT_URL),
|
|
8137
|
+
project: projectPath(env.CI_PROJECT_DIR, projectRoot),
|
|
8138
|
+
commit: env.CI_COMMIT_SHA,
|
|
8139
|
+
...env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME ? { ref: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME } : {},
|
|
8140
|
+
...env.CI_COMMIT_TIMESTAMP ? { committedAt: env.CI_COMMIT_TIMESTAMP } : {},
|
|
8141
|
+
ci: {
|
|
8142
|
+
provider: "gitlab",
|
|
8143
|
+
...env.CI_PIPELINE_ID ? { pipeline: env.CI_PIPELINE_ID } : {},
|
|
8144
|
+
...env.CI_JOB_ID ? { job: env.CI_JOB_ID } : {},
|
|
8145
|
+
...env.CI_JOB_URL ? { url: env.CI_JOB_URL } : {}
|
|
8146
|
+
}
|
|
8147
|
+
};
|
|
8148
|
+
if (env.GITHUB_ACTIONS === "true" && env.GITHUB_REPOSITORY && env.GITHUB_SHA) {
|
|
8149
|
+
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
8150
|
+
return {
|
|
8151
|
+
repo: normalizeRepoUrl(`${server}/${env.GITHUB_REPOSITORY}`),
|
|
8152
|
+
project: projectPath(env.GITHUB_WORKSPACE, projectRoot),
|
|
8153
|
+
commit: env.GITHUB_SHA,
|
|
8154
|
+
...env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME ? { ref: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME } : {},
|
|
8155
|
+
ci: {
|
|
8156
|
+
provider: "github",
|
|
8157
|
+
...env.GITHUB_RUN_ID ? { pipeline: env.GITHUB_RUN_ID } : {},
|
|
8158
|
+
...env.GITHUB_JOB ? { job: env.GITHUB_JOB } : {},
|
|
8159
|
+
...env.GITHUB_RUN_ID ? { url: `${server}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` } : {}
|
|
8160
|
+
}
|
|
8161
|
+
};
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
/**
|
|
8165
|
+
* Determine which repository and commit this project's eval code comes from.
|
|
8166
|
+
* Order: EVALKIT_SOURCE_* variables, GitLab/GitHub CI variables, then git.
|
|
8167
|
+
* Returns undefined outside git with no CI variables ("untracked" results).
|
|
8168
|
+
*
|
|
8169
|
+
* `outputDirs` (results, sandbox, snapshot) never make the tree dirty, even
|
|
8170
|
+
* when a project forgot to ignore them.
|
|
8171
|
+
*/
|
|
8172
|
+
async function detectSource(givenRoot, options = {}) {
|
|
8173
|
+
const env = options.env ?? process.env;
|
|
8174
|
+
const projectRoot = await realpath(givenRoot).catch(() => givenRoot);
|
|
8175
|
+
const top = await git(projectRoot, "rev-parse", "--show-toplevel");
|
|
8176
|
+
const excludes = top ? (options.outputDirs ?? []).map((dir) => join(projectRoot, relative(givenRoot, dir))).map((dir) => posix(relative(top, dir))).filter((dir) => dir && !dir.startsWith("..")).map((dir) => `:(exclude)${dir}`) : [];
|
|
8177
|
+
const dirty = top ? Boolean(await git(top, "status", "--porcelain", "--", ".", ...excludes)) : false;
|
|
8178
|
+
const fromCi = ciSource(env, projectRoot);
|
|
8179
|
+
if (fromCi) return {
|
|
8180
|
+
...fromCi,
|
|
8181
|
+
...top && !env.EVALKIT_SOURCE_PROJECT ? { project: projectPath(top, projectRoot) } : {},
|
|
8182
|
+
dirty
|
|
8183
|
+
};
|
|
8184
|
+
if (!top) return void 0;
|
|
8185
|
+
const commit = await git(top, "rev-parse", "HEAD");
|
|
8186
|
+
if (!commit) return void 0;
|
|
8187
|
+
const remote = await git(top, "config", "--get", "remote.origin.url") ?? (await git(top, "remote"))?.split("\n").map((name) => name.trim()).find(Boolean);
|
|
8188
|
+
const remoteUrl = remote && !remote.includes("/") && !remote.includes(":") ? await git(top, "config", "--get", `remote.${remote}.url`) : remote;
|
|
8189
|
+
const ref = await git(top, "symbolic-ref", "--short", "-q", "HEAD");
|
|
8190
|
+
const committedAt = await git(top, "show", "-s", "--format=%cI", "HEAD");
|
|
8191
|
+
return {
|
|
8192
|
+
repo: normalizeRepoUrl(remoteUrl ?? top),
|
|
8193
|
+
project: projectPath(top, projectRoot),
|
|
8194
|
+
commit,
|
|
8195
|
+
...ref ? { ref } : {},
|
|
8196
|
+
...committedAt ? { committedAt } : {},
|
|
8197
|
+
dirty
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
8200
|
+
/** What the dashboard's header shows: repository, commit, branch, dirty. */
|
|
8201
|
+
function projectInfo(source) {
|
|
8202
|
+
return {
|
|
8203
|
+
repo: source.repo,
|
|
8204
|
+
...source.project ? { project: source.project } : {},
|
|
8205
|
+
commit: source.commit,
|
|
8206
|
+
...source.ref ? { ref: source.ref } : {},
|
|
8207
|
+
dirty: source.dirty
|
|
8208
|
+
};
|
|
8209
|
+
}
|
|
8210
|
+
/** The project's evals and suites, saved with each run (`catalog.json`). */
|
|
8211
|
+
function runCatalog(registry, root) {
|
|
8212
|
+
return {
|
|
8213
|
+
catalog: registry.catalog({ root }),
|
|
8214
|
+
evals: registry.metadata(),
|
|
8215
|
+
suites: registry.suiteMetadata()
|
|
8216
|
+
};
|
|
8217
|
+
}
|
|
8218
|
+
/** `abc1234` or `abc1234+dirty`. */
|
|
8219
|
+
function shortSource(source) {
|
|
8220
|
+
return `${source.commit.slice(0, 7)}${source.dirty ? "+dirty" : ""}`;
|
|
8221
|
+
}
|
|
8222
|
+
//#endregion
|
|
8072
8223
|
//#region ../cli/src/dashboard-reports.ts
|
|
8073
8224
|
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;
|
|
8225
|
+
function runScorers(trials) {
|
|
8226
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
8227
|
+
for (const trial of trials) for (const score of trial?.scoring?.results ?? []) {
|
|
8228
|
+
const kind = score.kind;
|
|
8229
|
+
const key = `${kind}\0${score.name}`;
|
|
8230
|
+
const entry = byKey.get(key) ?? {
|
|
8231
|
+
name: score.name,
|
|
8232
|
+
kind,
|
|
8233
|
+
count: 0,
|
|
8234
|
+
skipped: 0,
|
|
8235
|
+
sum: 0
|
|
8236
|
+
};
|
|
8237
|
+
if (score.skipped) entry.skipped += 1;
|
|
8238
|
+
else if (score.value !== void 0) {
|
|
8239
|
+
entry.count += 1;
|
|
8240
|
+
entry.sum += score.value;
|
|
8241
|
+
}
|
|
8242
|
+
byKey.set(key, entry);
|
|
8243
|
+
}
|
|
8244
|
+
return [...byKey.values()].map(({ sum, ...scorer }) => ({
|
|
8245
|
+
...scorer,
|
|
8246
|
+
...scorer.count ? { mean: sum / scorer.count } : {}
|
|
8247
|
+
}));
|
|
8248
|
+
}
|
|
8249
|
+
function runDatasets(manifests) {
|
|
8250
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8251
|
+
for (const manifest of manifests) for (const dataset of manifest?.datasets ?? []) {
|
|
8252
|
+
const digests = byId.get(dataset.id) ?? /* @__PURE__ */ new Set();
|
|
8253
|
+
digests.add(dataset.digest);
|
|
8254
|
+
byId.set(dataset.id, digests);
|
|
8255
|
+
}
|
|
8256
|
+
return [...byId].map(([id, digests]) => ({
|
|
8257
|
+
id,
|
|
8258
|
+
digests: [...digests].sort()
|
|
8259
|
+
}));
|
|
8260
|
+
}
|
|
8074
8261
|
/** Written into a run directory by `evalkit tidy`; deleting it restores the run. */
|
|
8075
8262
|
const ARCHIVE_MARKER = "archived.json";
|
|
8076
8263
|
function isMissing(error) {
|
|
@@ -8094,11 +8281,13 @@ function trialPath(root, runId, trialId) {
|
|
|
8094
8281
|
}
|
|
8095
8282
|
function runStatus(summary) {
|
|
8096
8283
|
if (summary.status === "running") return "running";
|
|
8284
|
+
if (summary.status === "cancelled") return "cancelled";
|
|
8097
8285
|
if (summary.status !== "completed") return "errored";
|
|
8098
8286
|
return summary.failed === 0 && summary.passed === summary.trialCount ? "passed" : "failed";
|
|
8099
8287
|
}
|
|
8100
8288
|
function trialStatus(summary) {
|
|
8101
8289
|
if (summary.status === "running") return "running";
|
|
8290
|
+
if (summary.status === "cancelled") return "cancelled";
|
|
8102
8291
|
if (summary.status !== "completed") return "errored";
|
|
8103
8292
|
return summary.scoring?.passed === true ? "passed" : "failed";
|
|
8104
8293
|
}
|
|
@@ -8143,10 +8332,15 @@ function createDashboardReportReader(root, registry) {
|
|
|
8143
8332
|
const summary = await ifPresent(() => readRunSummary(root, id));
|
|
8144
8333
|
const trialIds = await ifPresent(() => readdir(resolve(root, id, "trials")));
|
|
8145
8334
|
const trials = await Promise.all((trialIds ?? []).map((trialId) => ifPresent(() => readTrialSummary(root, id, trialId))));
|
|
8335
|
+
const trialManifests = await Promise.all((trialIds ?? []).map((trialId) => ifPresent(() => readTrialManifest(root, id, trialId))));
|
|
8146
8336
|
const scores = trials.flatMap((trial) => trial?.scoring?.overall === void 0 ? [] : [trial.scoring.overall]);
|
|
8337
|
+
const scorers = runScorers(trials);
|
|
8338
|
+
const datasets = runDatasets(trialManifests);
|
|
8339
|
+
const archived = await ifPresent(() => stat(resolve(root, id, ARCHIVE_MARKER)));
|
|
8340
|
+
const interrupted = !summary && ownerIsGone(manifest.process);
|
|
8147
8341
|
return {
|
|
8148
8342
|
id,
|
|
8149
|
-
...
|
|
8343
|
+
...archived ? { archived: true } : {},
|
|
8150
8344
|
...manifest.source ? { source: manifest.source } : {},
|
|
8151
8345
|
evalId: manifest.evalId,
|
|
8152
8346
|
...manifest.suiteId ? { suiteId: manifest.suiteId } : {},
|
|
@@ -8160,26 +8354,31 @@ function createDashboardReportReader(root, registry) {
|
|
|
8160
8354
|
manifest.aut.id,
|
|
8161
8355
|
manifest.aut.version
|
|
8162
8356
|
].filter(Boolean).join(" / ") } : {},
|
|
8163
|
-
status: summary ? runStatus(summary) : "running",
|
|
8357
|
+
status: summary ? runStatus(summary) : interrupted ? "cancelled" : "running",
|
|
8358
|
+
...interrupted ? { interrupted: true } : {},
|
|
8164
8359
|
startedAt: manifest.startedAt,
|
|
8165
8360
|
...summary?.endedAt ? { completedAt: summary.endedAt } : {},
|
|
8166
8361
|
...summary?.durationMs !== void 0 ? { durationMs: summary.durationMs } : {},
|
|
8167
8362
|
completedTrials: summary?.trialCount ?? trials.filter(Boolean).length,
|
|
8168
8363
|
requestedTrials: summary?.trialCount ?? registry.get(manifest.evalId)?.policy?.trials ?? 1,
|
|
8169
|
-
...scores.length ? { score: scores.reduce((sum, score) => sum + score, 0) / scores.length } : {}
|
|
8364
|
+
...scores.length ? { score: scores.reduce((sum, score) => sum + score, 0) / scores.length } : {},
|
|
8365
|
+
...scorers.length ? { scorers } : {},
|
|
8366
|
+
...datasets.length ? { datasets } : {}
|
|
8170
8367
|
};
|
|
8171
8368
|
}
|
|
8172
8369
|
async function listTrials(runId) {
|
|
8173
8370
|
assertUuid(runId, "run ID");
|
|
8174
8371
|
const ids = await ifPresent(() => readdir(resolve(root, runId, "trials")));
|
|
8175
8372
|
if (!ids) return [];
|
|
8373
|
+
const runManifest = await ifPresent(() => readRunManifest(root, runId));
|
|
8374
|
+
const ownerGone = ownerIsGone(runManifest?.process);
|
|
8176
8375
|
return (await Promise.all(ids.map(async (id) => {
|
|
8177
8376
|
const manifest = await readTrialManifest(root, runId, id);
|
|
8178
8377
|
const summary = await ifPresent(() => readTrialSummary(root, runId, id));
|
|
8179
8378
|
return {
|
|
8180
8379
|
id,
|
|
8181
8380
|
index: manifest.trialIndex,
|
|
8182
|
-
status: summary ? trialStatus(summary) : "running",
|
|
8381
|
+
status: summary ? trialStatus(summary) : ownerGone ? "cancelled" : "running",
|
|
8183
8382
|
startedAt: manifest.startedAt,
|
|
8184
8383
|
...summary?.endedAt ? { completedAt: summary.endedAt } : {},
|
|
8185
8384
|
...summary?.durationMs !== void 0 ? { durationMs: summary.durationMs } : {},
|
|
@@ -8208,7 +8407,7 @@ function createDashboardReportReader(root, registry) {
|
|
|
8208
8407
|
const file = resolve(candidateRoot, relativePath);
|
|
8209
8408
|
if (file !== candidateRoot && !file.startsWith(`${candidateRoot}${sep}`)) throw new Error("Invalid workspace path");
|
|
8210
8409
|
if (!(await stat(file)).isFile()) throw new Error("Workspace path is not a file");
|
|
8211
|
-
return new Response(await
|
|
8410
|
+
return new Response(await readFile(file), { headers: { "content-type": fileContentType(file) } });
|
|
8212
8411
|
}
|
|
8213
8412
|
async function trajectory(runId, trialId) {
|
|
8214
8413
|
trialPath(root, runId, trialId);
|
|
@@ -8241,7 +8440,7 @@ async function walkArtifacts(root) {
|
|
|
8241
8440
|
} else result.push({
|
|
8242
8441
|
path,
|
|
8243
8442
|
kind: "file",
|
|
8244
|
-
size:
|
|
8443
|
+
size: (await stat(resolve(directory, entry.name))).size
|
|
8245
8444
|
});
|
|
8246
8445
|
}
|
|
8247
8446
|
}
|
|
@@ -8254,7 +8453,7 @@ async function walkArtifacts(root) {
|
|
|
8254
8453
|
const MARKER = ".evalkit-dashboard";
|
|
8255
8454
|
const quietStep = (_label, work) => work(() => {});
|
|
8256
8455
|
async function buildDashboard(options, step = quietStep) {
|
|
8257
|
-
const { registry, reportRoot, dashboardRoot, outDir } = options;
|
|
8456
|
+
const { registry, root, reportRoot, dashboardRoot, outDir } = options;
|
|
8258
8457
|
const template = await readFile(resolve(dashboardRoot, "index.html"), "utf8");
|
|
8259
8458
|
const data = resolve(outDir, "data");
|
|
8260
8459
|
const write = async (path, value) => {
|
|
@@ -8269,7 +8468,8 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8269
8468
|
await writeFile(resolve(outDir, "index.html"), dashboardIndex(template, { base: "./" }));
|
|
8270
8469
|
}, () => "read-only UI with relative assets");
|
|
8271
8470
|
await step("Eval catalog", async () => {
|
|
8272
|
-
await write("catalog.json", { evals: registry.catalog() });
|
|
8471
|
+
await write("catalog.json", { evals: registry.catalog({ root }) });
|
|
8472
|
+
await write("project.json", { project: options.source ? projectInfo(options.source) : null });
|
|
8273
8473
|
await write("evals.json", { evals: registry.metadata() });
|
|
8274
8474
|
await write("suites.json", { suites: registry.suiteMetadata() });
|
|
8275
8475
|
return {
|
|
@@ -8277,6 +8477,21 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8277
8477
|
suites: registry.suites.length
|
|
8278
8478
|
};
|
|
8279
8479
|
}, ({ evals, suites }) => `${count(evals, "eval")} · ${count(suites, "suite")}`);
|
|
8480
|
+
await step("Datasets", async () => {
|
|
8481
|
+
let files = 0;
|
|
8482
|
+
for (const [id, dataset] of projectDatasets(registry)) {
|
|
8483
|
+
const listing = await datasetListing(dataset, root ?? process.cwd());
|
|
8484
|
+
await write(`datasets/${id}/files.json`, listing);
|
|
8485
|
+
if (listing.kind !== "static" || dataset.visibility === "evaluator") continue;
|
|
8486
|
+
for (const entry of listing.files) {
|
|
8487
|
+
const file = resolve(data, "datasets", id, "files", entry.path);
|
|
8488
|
+
await mkdir(dirname(file), { recursive: true });
|
|
8489
|
+
await writeFile(file, await staticDatasetFile(dataset, root ?? process.cwd(), entry.path));
|
|
8490
|
+
files++;
|
|
8491
|
+
}
|
|
8492
|
+
}
|
|
8493
|
+
return files;
|
|
8494
|
+
}, (files) => count(files, "dataset file"));
|
|
8280
8495
|
const reports = createDashboardReportReader(reportRoot, registry);
|
|
8281
8496
|
const snapshot = await step("Run reports", async (progress) => {
|
|
8282
8497
|
const all = await reports.listRuns();
|
|
@@ -8284,13 +8499,16 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8284
8499
|
await write("runs.json", { runs });
|
|
8285
8500
|
let trials = 0;
|
|
8286
8501
|
let files = 0;
|
|
8287
|
-
const
|
|
8502
|
+
const runTrials = [];
|
|
8288
8503
|
for (const [index, run] of runs.entries()) {
|
|
8289
8504
|
progress(`run ${index + 1}/${runs.length} · ${run.id.slice(0, 8)}`);
|
|
8290
8505
|
const written = await snapshotRun(reports, run.id, directorySink(resolve(data, "runs", run.id)));
|
|
8291
8506
|
trials += written.trialIds.length;
|
|
8292
8507
|
files += written.files;
|
|
8293
|
-
|
|
8508
|
+
runTrials.push({
|
|
8509
|
+
id: run.id,
|
|
8510
|
+
trials: written.trialIds
|
|
8511
|
+
});
|
|
8294
8512
|
}
|
|
8295
8513
|
return {
|
|
8296
8514
|
runs: runs.length,
|
|
@@ -8298,16 +8516,16 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8298
8516
|
files,
|
|
8299
8517
|
skipped: reports.legacyRunIds().length,
|
|
8300
8518
|
archived: all.length - runs.length,
|
|
8301
|
-
|
|
8302
|
-
trialIds
|
|
8519
|
+
runTrials
|
|
8303
8520
|
};
|
|
8304
8521
|
}, ({ runs, trials, files, skipped, archived }) => `${count(runs, "run")} · ${count(trials, "trial")} · ${count(files, "candidate file")}` + (archived ? ` · ${archived} archived excluded` : "") + (skipped ? ` · ${skipped} older-format skipped` : ""));
|
|
8305
8522
|
await step("Route pages", async () => {
|
|
8523
|
+
const catalog = registry.catalog();
|
|
8306
8524
|
const routes = staticRoutes({
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8525
|
+
scenarios: catalog.map((entry) => entry.id),
|
|
8526
|
+
agents: catalog.flatMap((entry) => entry.agent.id ? [entry.agent.id] : []),
|
|
8527
|
+
datasets: catalog.flatMap((entry) => entry.datasets.map((dataset) => dataset.id)),
|
|
8528
|
+
runs: snapshot.runTrials
|
|
8311
8529
|
});
|
|
8312
8530
|
for (const route of routes) {
|
|
8313
8531
|
const file = resolve(outDir, `${route}.html`);
|
|
@@ -8317,7 +8535,7 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8317
8535
|
await writeFile(resolve(outDir, "404.html"), dashboardIndex(template, { base: "/" }));
|
|
8318
8536
|
return routes.length;
|
|
8319
8537
|
}, (pages) => `${count(pages, "page")} with clean /path URLs`);
|
|
8320
|
-
const {
|
|
8538
|
+
const { runTrials: _runs, ...summary } = snapshot;
|
|
8321
8539
|
return {
|
|
8322
8540
|
outDir,
|
|
8323
8541
|
...summary
|
|
@@ -8330,18 +8548,21 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8330
8548
|
* skipped; those pages still open by navigating inside the app.
|
|
8331
8549
|
*/
|
|
8332
8550
|
function staticRoutes(ids) {
|
|
8333
|
-
const safe = (id) => id.length > 0 && !/[/\\\0]/.test(id) && id !== "." && id !== "..";
|
|
8334
|
-
const under = (prefix, values) => values.filter(safe).map((id) => `${prefix}/${id}`);
|
|
8551
|
+
const safe = (id) => id.length > 0 && !/[/\\\0#?%]/.test(id) && id !== "." && id !== "..";
|
|
8552
|
+
const under = (prefix, values = []) => [...new Set(values)].filter(safe).map((id) => `${prefix}/${id}`);
|
|
8335
8553
|
return [
|
|
8336
|
-
"
|
|
8337
|
-
"agents",
|
|
8338
|
-
"fixtures",
|
|
8554
|
+
"scoreboard",
|
|
8339
8555
|
"runs",
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8556
|
+
"trials",
|
|
8557
|
+
"workspaces",
|
|
8558
|
+
"library/agents",
|
|
8559
|
+
"library/judges",
|
|
8560
|
+
"library/scenarios",
|
|
8561
|
+
"library/datasets",
|
|
8562
|
+
...under("library/scenarios", ids.scenarios),
|
|
8563
|
+
...under("library/agents", ids.agents),
|
|
8564
|
+
...under("library/datasets", ids.datasets),
|
|
8565
|
+
...ids.runs.filter((run) => safe(run.id)).flatMap((run) => [`runs/${run.id}`, ...run.trials.filter(safe).flatMap((trial) => [`runs/${run.id}/trials/${trial}`, `runs/${run.id}/trials/${trial}/workspace`])])
|
|
8345
8566
|
];
|
|
8346
8567
|
}
|
|
8347
8568
|
/** `runs/abc.html` is served at `<mount>/runs/abc`; its base is one level up. */
|
|
@@ -8516,6 +8737,124 @@ function selectDashboardCell(matrix, evalId, parameters) {
|
|
|
8516
8737
|
return selection;
|
|
8517
8738
|
}
|
|
8518
8739
|
//#endregion
|
|
8740
|
+
//#region ../cli/src/runtime.ts
|
|
8741
|
+
/**
|
|
8742
|
+
* The few platform services the CLI needs, on Bun or Node. Everything else
|
|
8743
|
+
* uses `node:` APIs, which both runtimes provide.
|
|
8744
|
+
*/
|
|
8745
|
+
const onBun = typeof globalThis.Bun !== "undefined";
|
|
8746
|
+
/** Node 22.18 is the first 22.x that runs the CLI's own code unflagged. */
|
|
8747
|
+
function assertSupportedRuntime(version = process.versions.node) {
|
|
8748
|
+
if (onBun) return;
|
|
8749
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
8750
|
+
if (major < 22 || major === 22 && minor < 18) throw new Error(`EvalKit needs Node 22.18 or later, or Bun; this is Node ${version}.`);
|
|
8751
|
+
}
|
|
8752
|
+
/** Serves a fetch handler on `port` (0 picks a free one). */
|
|
8753
|
+
async function serve(fetch, port) {
|
|
8754
|
+
if (onBun) {
|
|
8755
|
+
const server = globalThis.Bun.serve({
|
|
8756
|
+
port,
|
|
8757
|
+
fetch
|
|
8758
|
+
});
|
|
8759
|
+
return {
|
|
8760
|
+
port: server.port,
|
|
8761
|
+
stop: async () => server.stop(true)
|
|
8762
|
+
};
|
|
8763
|
+
}
|
|
8764
|
+
const server = createServer((incoming, outgoing) => {
|
|
8765
|
+
(async () => {
|
|
8766
|
+
try {
|
|
8767
|
+
const response = await fetch(toRequest(incoming, port));
|
|
8768
|
+
outgoing.writeHead(response.status, [...response.headers]);
|
|
8769
|
+
if (response.body) for await (const chunk of response.body) outgoing.write(chunk);
|
|
8770
|
+
outgoing.end();
|
|
8771
|
+
} catch (error) {
|
|
8772
|
+
outgoing.statusCode = 500;
|
|
8773
|
+
outgoing.end(error instanceof Error ? error.message : "Internal error");
|
|
8774
|
+
}
|
|
8775
|
+
})();
|
|
8776
|
+
});
|
|
8777
|
+
await new Promise((resolvePromise, reject) => {
|
|
8778
|
+
server.once("error", reject);
|
|
8779
|
+
server.listen(port, () => resolvePromise());
|
|
8780
|
+
});
|
|
8781
|
+
const address = server.address();
|
|
8782
|
+
return {
|
|
8783
|
+
port: typeof address === "object" && address ? address.port : port,
|
|
8784
|
+
stop: () => new Promise((resolvePromise) => server.close(() => resolvePromise()))
|
|
8785
|
+
};
|
|
8786
|
+
}
|
|
8787
|
+
function toRequest(incoming, port) {
|
|
8788
|
+
const host = incoming.headers.host ?? `localhost:${port}`;
|
|
8789
|
+
const url = new URL(incoming.url ?? "/", `http://${host}`);
|
|
8790
|
+
const headers = new Headers();
|
|
8791
|
+
for (const [name, value] of Object.entries(incoming.headers)) if (value !== void 0) for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
|
|
8792
|
+
const method = incoming.method ?? "GET";
|
|
8793
|
+
return new Request(url, {
|
|
8794
|
+
method,
|
|
8795
|
+
headers,
|
|
8796
|
+
...method === "GET" || method === "HEAD" ? {} : {
|
|
8797
|
+
body: incoming,
|
|
8798
|
+
duplex: "half"
|
|
8799
|
+
}
|
|
8800
|
+
});
|
|
8801
|
+
}
|
|
8802
|
+
const sleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
8803
|
+
/** Best effort; the printed URL stays authoritative. */
|
|
8804
|
+
function openInBrowser(url) {
|
|
8805
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
8806
|
+
try {
|
|
8807
|
+
const child = spawn(opener, [url], {
|
|
8808
|
+
stdio: "ignore",
|
|
8809
|
+
detached: true
|
|
8810
|
+
});
|
|
8811
|
+
child.on("error", () => {});
|
|
8812
|
+
child.unref();
|
|
8813
|
+
} catch {}
|
|
8814
|
+
}
|
|
8815
|
+
let jiti;
|
|
8816
|
+
/**
|
|
8817
|
+
* Imports a project's TypeScript or JavaScript module. Bun loads TypeScript
|
|
8818
|
+
* natively; on Node, jiti handles TypeScript, including `.js` specifiers that
|
|
8819
|
+
* point at `.ts` files.
|
|
8820
|
+
*/
|
|
8821
|
+
async function importProjectModule(file) {
|
|
8822
|
+
if (onBun || /\.(m?js|cjs)$/.test(file)) return import(pathToFileURL(file).href);
|
|
8823
|
+
if (!jiti) {
|
|
8824
|
+
const { createJiti } = await import("jiti");
|
|
8825
|
+
jiti = createJiti(import.meta.url, { moduleCache: false });
|
|
8826
|
+
}
|
|
8827
|
+
return jiti.import(file);
|
|
8828
|
+
}
|
|
8829
|
+
/**
|
|
8830
|
+
* A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
|
|
8831
|
+
* spans segments, and `{a,b}` is an alternation.
|
|
8832
|
+
*/
|
|
8833
|
+
function globMatcher(pattern) {
|
|
8834
|
+
let source = "";
|
|
8835
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
8836
|
+
const char = pattern[index];
|
|
8837
|
+
if (char === "*" && pattern[index + 1] === "*") {
|
|
8838
|
+
index++;
|
|
8839
|
+
if (pattern[index + 1] === "/") {
|
|
8840
|
+
index++;
|
|
8841
|
+
source += "(?:.*/)?";
|
|
8842
|
+
} else source += ".*";
|
|
8843
|
+
} else if (char === "*") source += "[^/]*";
|
|
8844
|
+
else if (char === "?") source += "[^/]";
|
|
8845
|
+
else if (char === "{") {
|
|
8846
|
+
const end = pattern.indexOf("}", index);
|
|
8847
|
+
if (end === -1) source += "\\{";
|
|
8848
|
+
else {
|
|
8849
|
+
source += `(?:${pattern.slice(index + 1, end).split(",").map((part) => part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join("|")})`;
|
|
8850
|
+
index = end;
|
|
8851
|
+
}
|
|
8852
|
+
} else source += char.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
8853
|
+
}
|
|
8854
|
+
const expression = new RegExp(`^${source}$`);
|
|
8855
|
+
return (path) => expression.test(path);
|
|
8856
|
+
}
|
|
8857
|
+
//#endregion
|
|
8519
8858
|
//#region ../cli/src/project.ts
|
|
8520
8859
|
const defaultInclude = ["**/*.eval.ts", "**/*.eval.js"];
|
|
8521
8860
|
const defaultExclude = [
|
|
@@ -8538,10 +8877,16 @@ function evaluation(value, source) {
|
|
|
8538
8877
|
if (typeof candidate.agent?.start !== "function" || !Array.isArray(candidate.transcript) || !Array.isArray(candidate.scoring)) throw new Error(`Invalid eval exported by ${source}`);
|
|
8539
8878
|
return candidate;
|
|
8540
8879
|
}
|
|
8880
|
+
/** The suite a discovered file belongs to: the first directory below the discovery root. */
|
|
8881
|
+
function directorySuite(relativePath) {
|
|
8882
|
+
const parts = relativePath.split("/");
|
|
8883
|
+
return parts.length > 1 ? parts[0] : void 0;
|
|
8884
|
+
}
|
|
8541
8885
|
async function discover(root, config) {
|
|
8542
|
-
const
|
|
8543
|
-
const
|
|
8544
|
-
const
|
|
8886
|
+
const testDir = config.testDir ?? "scenarios";
|
|
8887
|
+
const directory = resolve(root, testDir);
|
|
8888
|
+
const includes = (config.include ?? defaultInclude).map((pattern) => ({ match: globMatcher(pattern) }));
|
|
8889
|
+
const excludes = [...defaultExclude, ...config.exclude ?? []].map((pattern) => ({ match: globMatcher(pattern) }));
|
|
8545
8890
|
const paths = [];
|
|
8546
8891
|
async function walk(dir) {
|
|
8547
8892
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -8557,16 +8902,42 @@ async function discover(root, config) {
|
|
|
8557
8902
|
try {
|
|
8558
8903
|
await walk(directory);
|
|
8559
8904
|
} catch (error) {
|
|
8560
|
-
if (error.code
|
|
8561
|
-
|
|
8905
|
+
if (error.code !== "ENOENT") throw error;
|
|
8906
|
+
const hint = config.testDir === void 0 && await isDirectory(resolve(root, "evals")) ? ` Scenarios now live in scenarios/: move evals/ there, or set testDir: 'evals' in evalkit.config.ts.` : "";
|
|
8907
|
+
throw new Error(`Eval directory not found: ${directory}.${hint}`);
|
|
8562
8908
|
}
|
|
8563
|
-
const
|
|
8909
|
+
const directorySuites = config.discovery?.suites ?? true;
|
|
8910
|
+
const standalone = [];
|
|
8911
|
+
const bySuite = /* @__PURE__ */ new Map();
|
|
8912
|
+
const loadErrors = [];
|
|
8564
8913
|
for (const file of paths.sort()) {
|
|
8565
|
-
const
|
|
8566
|
-
|
|
8914
|
+
const fromRoot = relative(root, file).split(sep).join("/");
|
|
8915
|
+
const suiteId = directorySuites ? directorySuite(relative(directory, file).split(sep).join("/")) : void 0;
|
|
8916
|
+
try {
|
|
8917
|
+
if (suiteId !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(suiteId)) throw new Error(`Directory ${testDir}/${suiteId} names a suite, so it must be lowercase kebab-case (or set discovery.suites: false)`);
|
|
8918
|
+
const exported = await (await importProjectModule(file)).default;
|
|
8919
|
+
const evals = (Array.isArray(exported) ? exported : [exported]).map((value) => evaluation(value, fromRoot));
|
|
8920
|
+
if (suiteId === void 0) standalone.push(...evals);
|
|
8921
|
+
else bySuite.set(suiteId, [...bySuite.get(suiteId) ?? [], ...evals]);
|
|
8922
|
+
} catch (error) {
|
|
8923
|
+
loadErrors.push({
|
|
8924
|
+
file: fromRoot,
|
|
8925
|
+
...suiteId === void 0 ? {} : { suiteId },
|
|
8926
|
+
error: recordError(error)
|
|
8927
|
+
});
|
|
8928
|
+
}
|
|
8567
8929
|
}
|
|
8568
|
-
if (!
|
|
8569
|
-
return
|
|
8930
|
+
if (!paths.length) throw new Error(`No eval files found in ${directory}`);
|
|
8931
|
+
return {
|
|
8932
|
+
registrations: [...standalone, ...[...bySuite].map(([id, evals]) => defineSuite({
|
|
8933
|
+
id,
|
|
8934
|
+
evals
|
|
8935
|
+
}))],
|
|
8936
|
+
loadErrors
|
|
8937
|
+
};
|
|
8938
|
+
}
|
|
8939
|
+
async function isDirectory(path) {
|
|
8940
|
+
return stat(path).then((info) => info.isDirectory(), () => false);
|
|
8570
8941
|
}
|
|
8571
8942
|
async function loadProject(cwd, configPath) {
|
|
8572
8943
|
const candidates = configPath ? [resolve(cwd, configPath)] : [
|
|
@@ -8580,12 +8951,18 @@ async function loadProject(cwd, configPath) {
|
|
|
8580
8951
|
if (present.length > 1) throw new Error("Multiple EvalKit configs found; select one with --config");
|
|
8581
8952
|
const file = present[0];
|
|
8582
8953
|
const root = file ? dirname(file) : resolve(cwd);
|
|
8583
|
-
const config = file ? (await
|
|
8954
|
+
const config = file ? (await importProjectModule(file)).default : {};
|
|
8584
8955
|
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Config must default-export defineConfig({...})");
|
|
8585
8956
|
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`);
|
|
8586
8957
|
let registry = config.registry;
|
|
8587
|
-
if (!file && !configPath && await exists$1(resolve(root, "src/registry.ts"))) registry = (await
|
|
8588
|
-
|
|
8958
|
+
if (!file && !configPath && await exists$1(resolve(root, "src/registry.ts"))) registry = (await importProjectModule(resolve(root, "src/registry.ts"))).default;
|
|
8959
|
+
let loadErrors = [];
|
|
8960
|
+
if (!registry && config.evals) registry = registerEvals(config.evals);
|
|
8961
|
+
if (!registry) {
|
|
8962
|
+
const discovered = await discover(root, config);
|
|
8963
|
+
loadErrors = discovered.loadErrors;
|
|
8964
|
+
registry = registerEvals(discovered.registrations);
|
|
8965
|
+
}
|
|
8589
8966
|
if (!registry?.evals || !registry.get) throw new Error("Invalid explicit eval registry");
|
|
8590
8967
|
const ids = /* @__PURE__ */ new Set();
|
|
8591
8968
|
for (const e of registry.evals) {
|
|
@@ -8608,7 +8985,8 @@ async function loadProject(cwd, configPath) {
|
|
|
8608
8985
|
return {
|
|
8609
8986
|
root,
|
|
8610
8987
|
config,
|
|
8611
|
-
registry
|
|
8988
|
+
registry,
|
|
8989
|
+
loadErrors
|
|
8612
8990
|
};
|
|
8613
8991
|
}
|
|
8614
8992
|
//#endregion
|
|
@@ -8717,125 +9095,206 @@ async function confirm(question) {
|
|
|
8717
9095
|
}
|
|
8718
9096
|
}
|
|
8719
9097
|
//#endregion
|
|
8720
|
-
//#region ../cli/src/
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
if (
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
return `
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
9098
|
+
//#region ../cli/src/run-reporter.ts
|
|
9099
|
+
function scenarioOutcome(result) {
|
|
9100
|
+
if (result.status === "cancelled") return "cancelled";
|
|
9101
|
+
return result.status === "completed" && (result.trials ? result.trials.length > 0 && result.trials.every((trial) => trial.status === "completed" && trial.scoring?.passed) : Boolean(result.scoring?.passed)) ? "passed" : "failed";
|
|
9102
|
+
}
|
|
9103
|
+
function passAtFor(definition, name) {
|
|
9104
|
+
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "predicate" && step.name === name);
|
|
9105
|
+
return rule?.kind === "predicate" ? rule.passAt : void 0;
|
|
9106
|
+
}
|
|
9107
|
+
function value(score) {
|
|
9108
|
+
return score.value === void 0 ? "" : score.value.toFixed(1);
|
|
9109
|
+
}
|
|
9110
|
+
function scoreReason(definition, score) {
|
|
9111
|
+
if (score.error) return `${score.name} error: ${score.error.message}`;
|
|
9112
|
+
const passAt = passAtFor(definition, score.name);
|
|
9113
|
+
return `${score.name} ${value(score)}${passAt === void 0 ? "" : ` (passes at ${passAt})`}`;
|
|
9114
|
+
}
|
|
9115
|
+
function checkpointReason(definition, checkpoint) {
|
|
9116
|
+
if (checkpoint.status === "error") return `step ${checkpoint.step} ${checkpoint.name} error: ${checkpoint.error?.message ?? ""}`;
|
|
9117
|
+
const passAt = passAtFor(definition, checkpoint.name);
|
|
9118
|
+
return `step ${checkpoint.step} ${checkpoint.name} ${checkpoint.value === void 0 ? checkpoint.status : value(checkpoint)}${passAt === void 0 ? "" : ` (passes at ${passAt})`}`;
|
|
9119
|
+
}
|
|
9120
|
+
/** What went wrong in a scenario, deduplicated across its trials. */
|
|
9121
|
+
function reasonsFor(definition, trials) {
|
|
9122
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
9123
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
9124
|
+
for (const trial of trials) {
|
|
9125
|
+
if (trial.status === "failed" && trial.error) reasons.add(`error: ${trial.error.message}`);
|
|
9126
|
+
for (const checkpoint of trial.scoring?.checkpoints ?? []) if (checkpoint.status === "failed" || checkpoint.status === "error") reasons.add(checkpointReason(definition, checkpoint));
|
|
9127
|
+
for (const score of trial.scoring?.results ?? []) if (score.skipped) skipped.add(score.name);
|
|
9128
|
+
else if (!score.passed) reasons.add(scoreReason(definition, score));
|
|
9129
|
+
for (const name of trial.scoring?.skippedScorers ?? []) if (trial.status !== "cancelled") reasons.add(`${name} not run`);
|
|
8750
9130
|
}
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
function ciSource(env, projectRoot) {
|
|
8755
|
-
if (env.EVALKIT_SOURCE_REPO && env.EVALKIT_SOURCE_COMMIT) return {
|
|
8756
|
-
repo: normalizeRepoUrl(env.EVALKIT_SOURCE_REPO),
|
|
8757
|
-
project: env.EVALKIT_SOURCE_PROJECT ?? "",
|
|
8758
|
-
commit: env.EVALKIT_SOURCE_COMMIT,
|
|
8759
|
-
...env.EVALKIT_SOURCE_REF ? { ref: env.EVALKIT_SOURCE_REF } : {},
|
|
8760
|
-
...env.EVALKIT_SOURCE_COMMITTED_AT ? { committedAt: env.EVALKIT_SOURCE_COMMITTED_AT } : {}
|
|
8761
|
-
};
|
|
8762
|
-
if (env.GITLAB_CI === "true" && env.CI_PROJECT_URL && env.CI_COMMIT_SHA) return {
|
|
8763
|
-
repo: normalizeRepoUrl(env.CI_PROJECT_URL),
|
|
8764
|
-
project: projectPath(env.CI_PROJECT_DIR, projectRoot),
|
|
8765
|
-
commit: env.CI_COMMIT_SHA,
|
|
8766
|
-
...env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME ? { ref: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME } : {},
|
|
8767
|
-
...env.CI_COMMIT_TIMESTAMP ? { committedAt: env.CI_COMMIT_TIMESTAMP } : {},
|
|
8768
|
-
ci: {
|
|
8769
|
-
provider: "gitlab",
|
|
8770
|
-
...env.CI_PIPELINE_ID ? { pipeline: env.CI_PIPELINE_ID } : {},
|
|
8771
|
-
...env.CI_JOB_ID ? { job: env.CI_JOB_ID } : {},
|
|
8772
|
-
...env.CI_JOB_URL ? { url: env.CI_JOB_URL } : {}
|
|
8773
|
-
}
|
|
9131
|
+
return {
|
|
9132
|
+
reasons: [...reasons],
|
|
9133
|
+
skipped: [...skipped]
|
|
8774
9134
|
};
|
|
8775
|
-
if (env.GITHUB_ACTIONS === "true" && env.GITHUB_REPOSITORY && env.GITHUB_SHA) {
|
|
8776
|
-
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
8777
|
-
return {
|
|
8778
|
-
repo: normalizeRepoUrl(`${server}/${env.GITHUB_REPOSITORY}`),
|
|
8779
|
-
project: projectPath(env.GITHUB_WORKSPACE, projectRoot),
|
|
8780
|
-
commit: env.GITHUB_SHA,
|
|
8781
|
-
...env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME ? { ref: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME } : {},
|
|
8782
|
-
ci: {
|
|
8783
|
-
provider: "github",
|
|
8784
|
-
...env.GITHUB_RUN_ID ? { pipeline: env.GITHUB_RUN_ID } : {},
|
|
8785
|
-
...env.GITHUB_JOB ? { job: env.GITHUB_JOB } : {},
|
|
8786
|
-
...env.GITHUB_RUN_ID ? { url: `${server}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` } : {}
|
|
8787
|
-
}
|
|
8788
|
-
};
|
|
8789
|
-
}
|
|
8790
9135
|
}
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
* Returns undefined outside git with no CI variables ("untracked" results).
|
|
8795
|
-
*
|
|
8796
|
-
* `outputDirs` (results, sandbox, snapshot) never make the tree dirty, even
|
|
8797
|
-
* when a project forgot to ignore them.
|
|
8798
|
-
*/
|
|
8799
|
-
async function detectSource(givenRoot, options = {}) {
|
|
8800
|
-
const env = options.env ?? process.env;
|
|
8801
|
-
const projectRoot = await realpath(givenRoot).catch(() => givenRoot);
|
|
8802
|
-
const top = await git(projectRoot, "rev-parse", "--show-toplevel");
|
|
8803
|
-
const excludes = top ? (options.outputDirs ?? []).map((dir) => join(projectRoot, relative(givenRoot, dir))).map((dir) => posix(relative(top, dir))).filter((dir) => dir && !dir.startsWith("..")).map((dir) => `:(exclude)${dir}`) : [];
|
|
8804
|
-
const dirty = top ? Boolean(await git(top, "status", "--porcelain", "--", ".", ...excludes)) : false;
|
|
8805
|
-
const fromCi = ciSource(env, projectRoot);
|
|
8806
|
-
if (fromCi) return {
|
|
8807
|
-
...fromCi,
|
|
8808
|
-
...top && !env.EVALKIT_SOURCE_PROJECT ? { project: projectPath(top, projectRoot) } : {},
|
|
8809
|
-
dirty
|
|
8810
|
-
};
|
|
8811
|
-
if (!top) return void 0;
|
|
8812
|
-
const commit = await git(top, "rev-parse", "HEAD");
|
|
8813
|
-
if (!commit) return void 0;
|
|
8814
|
-
const remote = await git(top, "config", "--get", "remote.origin.url") ?? (await git(top, "remote"))?.split("\n").map((name) => name.trim()).find(Boolean);
|
|
8815
|
-
const remoteUrl = remote && !remote.includes("/") && !remote.includes(":") ? await git(top, "config", "--get", `remote.${remote}.url`) : remote;
|
|
8816
|
-
const ref = await git(top, "symbolic-ref", "--short", "-q", "HEAD");
|
|
8817
|
-
const committedAt = await git(top, "show", "-s", "--format=%cI", "HEAD");
|
|
9136
|
+
function scenarioResult(cell, result, suiteId) {
|
|
9137
|
+
const evalId = authoringId(cell.eval);
|
|
9138
|
+
const trials = result.trials ?? [result];
|
|
8818
9139
|
return {
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
9140
|
+
label: suiteId ? `${suiteId}/${evalId}` : evalId,
|
|
9141
|
+
evalId,
|
|
9142
|
+
...suiteId ? { suiteId } : {},
|
|
9143
|
+
parameters: cell.parameters,
|
|
9144
|
+
outcome: scenarioOutcome(result),
|
|
9145
|
+
...result.durationMs !== void 0 ? { durationMs: result.durationMs } : {},
|
|
9146
|
+
result,
|
|
9147
|
+
...reasonsFor(cell.eval, trials)
|
|
8825
9148
|
};
|
|
8826
9149
|
}
|
|
8827
|
-
|
|
8828
|
-
|
|
9150
|
+
function parameterLabel(parameters) {
|
|
9151
|
+
return Object.entries(parameters).map(([key, item]) => `${key}=${typeof item === "string" ? item : JSON.stringify(item)}`).join(" ");
|
|
9152
|
+
}
|
|
9153
|
+
/** One printed line for a finished scenario; failing reasons on the same line. */
|
|
9154
|
+
function scenarioLine(scenario) {
|
|
9155
|
+
const mark = scenario.outcome === "passed" ? green("✓") : scenario.outcome === "cancelled" ? yellow("◌") : red("✗");
|
|
9156
|
+
const parameters = parameterLabel(scenario.parameters);
|
|
9157
|
+
const trials = scenario.result.trials;
|
|
9158
|
+
const trialCount = trials && trials.length > 1 ? dim(` ${trials.filter((trial) => trial.status === "completed" && trial.scoring?.passed).length}/${trials.length} trials`) : "";
|
|
9159
|
+
const details = [
|
|
9160
|
+
...scenario.outcome === "failed" ? scenario.reasons : [],
|
|
9161
|
+
...scenario.outcome === "cancelled" ? ["cancelled"] : [],
|
|
9162
|
+
...scenario.skipped.map((name) => `${name} skipped`)
|
|
9163
|
+
];
|
|
9164
|
+
return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${trialCount}${details.length ? ` ${dim(details.join(" · "))}` : ""}`;
|
|
9165
|
+
}
|
|
9166
|
+
/**
|
|
9167
|
+
* Prints run progress. On a terminal, failures print as they happen and a live
|
|
9168
|
+
* line shows counts and what is running. Elsewhere (CI), every finished
|
|
9169
|
+
* scenario prints one plain line.
|
|
9170
|
+
*/
|
|
9171
|
+
function createRunReporter(options) {
|
|
9172
|
+
const write = options.write ?? ((text) => process.stdout.write(text));
|
|
9173
|
+
const now = options.now ?? (() => performance.now());
|
|
9174
|
+
const running = /* @__PURE__ */ new Map();
|
|
9175
|
+
const scenarios = [];
|
|
9176
|
+
let liveShown = false;
|
|
9177
|
+
let timer;
|
|
9178
|
+
function clearLive() {
|
|
9179
|
+
if (liveShown) write("\r\x1B[2K");
|
|
9180
|
+
liveShown = false;
|
|
9181
|
+
}
|
|
9182
|
+
function liveLine() {
|
|
9183
|
+
const failed = scenarios.filter((s) => s.outcome === "failed").length;
|
|
9184
|
+
const active = [...running.values()].slice(0, 2).map((activity) => {
|
|
9185
|
+
const seconds = Math.round((now() - activity.startedAt) / 1e3);
|
|
9186
|
+
return `${activity.label} (${activity.doing ? `${activity.doing}, ` : ""}${seconds}s)`;
|
|
9187
|
+
});
|
|
9188
|
+
const more = running.size > 2 ? ", …" : "";
|
|
9189
|
+
return ` ${cyan("◇")} ${scenarios.length}/${options.total}${failed ? ` · ${red(`${failed} failed`)}` : ""}${active.length ? dim(` · running: ${active.join(", ")}${more}`) : ""}`;
|
|
9190
|
+
}
|
|
9191
|
+
function drawLive() {
|
|
9192
|
+
if (!options.interactive) return;
|
|
9193
|
+
const line = liveLine();
|
|
9194
|
+
const width = process.stdout.columns ?? 120;
|
|
9195
|
+
write(`\r\x1b[2K${line.length > width + 40 ? line.slice(0, width + 40) : line}`);
|
|
9196
|
+
liveShown = true;
|
|
9197
|
+
}
|
|
9198
|
+
function print(line) {
|
|
9199
|
+
clearLive();
|
|
9200
|
+
write(`${line}\n`);
|
|
9201
|
+
drawLive();
|
|
9202
|
+
}
|
|
8829
9203
|
return {
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
9204
|
+
start() {
|
|
9205
|
+
if (options.interactive) {
|
|
9206
|
+
timer = setInterval(drawLive, 100);
|
|
9207
|
+
timer.unref?.();
|
|
9208
|
+
}
|
|
9209
|
+
},
|
|
9210
|
+
onEvent(trial, event) {
|
|
9211
|
+
if (event.source === "runner" && event.kind === "trial-started") {
|
|
9212
|
+
running.set(trial.trialId, {
|
|
9213
|
+
label: options.labelFor(trial),
|
|
9214
|
+
startedAt: now()
|
|
9215
|
+
});
|
|
9216
|
+
return;
|
|
9217
|
+
}
|
|
9218
|
+
const activity = running.get(trial.trialId);
|
|
9219
|
+
if (!activity) return;
|
|
9220
|
+
if (event.source === "runner" && (event.kind === "trial-completed" || event.kind === "error")) running.delete(trial.trialId);
|
|
9221
|
+
else if (event.source === "aut" && event.kind === "tool-call") activity.doing = `tool ${event.name}`;
|
|
9222
|
+
else if (event.source === "runner" && event.kind === "scorer-started") activity.doing = `scoring ${event.scorer}`;
|
|
9223
|
+
else if (event.source === "runner" && event.kind === "checkpoint-started") activity.doing = `check ${event.name}`;
|
|
9224
|
+
},
|
|
9225
|
+
onScenario(scenario) {
|
|
9226
|
+
scenarios.push(scenario);
|
|
9227
|
+
for (const trial of scenario.result.trials ?? [scenario.result]) running.delete(trial.trialId);
|
|
9228
|
+
if (!options.interactive || options.verbose || scenario.outcome !== "passed") print(scenarioLine(scenario));
|
|
9229
|
+
},
|
|
9230
|
+
/** Called on the first Ctrl+C. */
|
|
9231
|
+
cancelling() {
|
|
9232
|
+
print(`\n ${yellow("◌")} Cancelling: finishing running trials. Press Ctrl+C again to exit now.`);
|
|
9233
|
+
},
|
|
9234
|
+
finish(durationMs) {
|
|
9235
|
+
if (timer) clearInterval(timer);
|
|
9236
|
+
clearLive();
|
|
9237
|
+
return {
|
|
9238
|
+
passed: scenarios.filter((s) => s.outcome === "passed").length,
|
|
9239
|
+
failed: scenarios.filter((s) => s.outcome === "failed").length,
|
|
9240
|
+
cancelled: scenarios.filter((s) => s.outcome === "cancelled").length,
|
|
9241
|
+
scenarios: scenarios.length,
|
|
9242
|
+
durationMs
|
|
9243
|
+
};
|
|
9244
|
+
},
|
|
9245
|
+
scenarios: () => scenarios
|
|
8833
9246
|
};
|
|
8834
9247
|
}
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
9248
|
+
function totalsLine(totals) {
|
|
9249
|
+
const parts = [
|
|
9250
|
+
green(`${totals.passed} passed`),
|
|
9251
|
+
totals.failed ? red(`${totals.failed} failed`) : "",
|
|
9252
|
+
totals.cancelled ? yellow(`${totals.cancelled} cancelled`) : ""
|
|
9253
|
+
].filter(Boolean);
|
|
9254
|
+
return ` ${bold(parts.join(" · "))} ${dim(`· ${plural(totals.scenarios, "scenario")} in ${duration(totals.durationMs)}`)}`;
|
|
9255
|
+
}
|
|
9256
|
+
//#endregion
|
|
9257
|
+
//#region ../cli/src/junit.ts
|
|
9258
|
+
function escape(text) {
|
|
9259
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
9260
|
+
}
|
|
9261
|
+
const seconds = (ms) => ((ms ?? 0) / 1e3).toFixed(3);
|
|
9262
|
+
/**
|
|
9263
|
+
* A JUnit XML report: one test suite per EvalKit suite (`default` for
|
|
9264
|
+
* scenarios without one) and one test case per scenario, per matrix cell
|
|
9265
|
+
* when it has parameters.
|
|
9266
|
+
*/
|
|
9267
|
+
function junitReport(scenarios) {
|
|
9268
|
+
const bySuite = /* @__PURE__ */ new Map();
|
|
9269
|
+
for (const scenario of scenarios) {
|
|
9270
|
+
const suite = scenario.suiteId ?? "default";
|
|
9271
|
+
bySuite.set(suite, [...bySuite.get(suite) ?? [], scenario]);
|
|
9272
|
+
}
|
|
9273
|
+
const suites = [...bySuite].map(([suite, cases]) => {
|
|
9274
|
+
const failures = cases.filter((c) => c.outcome === "failed").length;
|
|
9275
|
+
const skipped = cases.filter((c) => c.outcome === "cancelled" || c.outcome === "passed" && c.skipped.length > 0 && allSkipped(c)).length;
|
|
9276
|
+
const time = cases.reduce((sum, c) => sum + (c.durationMs ?? 0), 0);
|
|
9277
|
+
const testcases = cases.map((scenario) => {
|
|
9278
|
+
const parameters = parameterLabel(scenario.parameters);
|
|
9279
|
+
const attributes = `name="${escape(parameters ? `${scenario.evalId} [${parameters}]` : scenario.evalId)}" classname="${escape(suite)}" time="${seconds(scenario.durationMs)}"`;
|
|
9280
|
+
if (scenario.outcome === "failed") return ` <testcase ${attributes}>\n <failure message="${escape(scenario.reasons.join("; ") || "failed")}">${escape(scenario.reasons.join("\n"))}</failure>\n </testcase>`;
|
|
9281
|
+
if (scenario.outcome === "cancelled") return ` <testcase ${attributes}>\n <skipped message="cancelled"/>\n </testcase>`;
|
|
9282
|
+
if (allSkipped(scenario)) return ` <testcase ${attributes}>\n <skipped message="${escape(`all scorers skipped: ${scenario.skipped.join(", ")}`)}"/>\n </testcase>`;
|
|
9283
|
+
return ` <testcase ${attributes}/>`;
|
|
9284
|
+
});
|
|
9285
|
+
return ` <testsuite name="${escape(suite)}" tests="${cases.length}" failures="${failures}" errors="0" skipped="${skipped}" time="${seconds(time)}">\n${testcases.join("\n")}\n </testsuite>`;
|
|
9286
|
+
});
|
|
9287
|
+
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`;
|
|
8838
9288
|
}
|
|
9289
|
+
/** True when every score of every trial was skip()ped. */
|
|
9290
|
+
function allSkipped(scenario) {
|
|
9291
|
+
const results = (scenario.result.trials ?? [scenario.result]).flatMap((trial) => trial.scoring?.results ?? []);
|
|
9292
|
+
return results.length > 0 && results.every((score) => score.skipped);
|
|
9293
|
+
}
|
|
9294
|
+
//#endregion
|
|
9295
|
+
//#region ../cli/src/version.ts
|
|
9296
|
+
/** The CLI version; kept in sync with package.json when releasing. */
|
|
9297
|
+
const VERSION = "0.6.0";
|
|
8839
9298
|
//#endregion
|
|
8840
9299
|
//#region ../cli/src/run-command.ts
|
|
8841
9300
|
function parseRunArgs(args) {
|
|
@@ -8875,7 +9334,10 @@ function parseRunArgs(args) {
|
|
|
8875
9334
|
json: { type: "boolean" },
|
|
8876
9335
|
"dry-run": { type: "boolean" },
|
|
8877
9336
|
all: { type: "boolean" },
|
|
8878
|
-
local: { type: "boolean" }
|
|
9337
|
+
local: { type: "boolean" },
|
|
9338
|
+
junit: { type: "string" },
|
|
9339
|
+
verbose: { type: "boolean" },
|
|
9340
|
+
"skip-broken": { type: "boolean" }
|
|
8879
9341
|
}
|
|
8880
9342
|
});
|
|
8881
9343
|
return normalizeRunOptions(values, positionals);
|
|
@@ -8928,21 +9390,37 @@ function normalizeRunOptions(values, positionals) {
|
|
|
8928
9390
|
trials: integer("trials")
|
|
8929
9391
|
};
|
|
8930
9392
|
}
|
|
9393
|
+
/** Load errors that affect what was asked for, so the run must not pretend it covered them. */
|
|
9394
|
+
function brokenSelection(loadErrors, request) {
|
|
9395
|
+
if (!loadErrors.length) return [];
|
|
9396
|
+
if (request.suiteId) return loadErrors.filter((error) => error.suiteId === request.suiteId);
|
|
9397
|
+
if (request.evalIds.length) return request.evalIds.some((id) => !request.registry.get(id)) ? loadErrors : [];
|
|
9398
|
+
return loadErrors;
|
|
9399
|
+
}
|
|
9400
|
+
/** Load errors are warnings; `print` is stderr when stdout must stay machine-readable. */
|
|
9401
|
+
function printLoadErrors(loadErrors, print = console.log) {
|
|
9402
|
+
for (const { file, error } of loadErrors) print(` ${yellow("✗")} ${bold(file)} failed to load:\n ${dim(error.message)}`);
|
|
9403
|
+
if (loadErrors.length) print("");
|
|
9404
|
+
}
|
|
8931
9405
|
async function runProjectCommand(args, cwd = process.cwd(), parsed = parseRunArgs(args)) {
|
|
8932
9406
|
const { values, positionals, parameters } = parsed;
|
|
9407
|
+
const started = performance.now();
|
|
8933
9408
|
const project = await loadProject(cwd, values.config);
|
|
8934
9409
|
process.chdir(project.root);
|
|
8935
9410
|
const registry = project.registry;
|
|
8936
9411
|
const requestedEvals = values.eval?.flatMap((value) => value.split(",")) ?? [];
|
|
8937
9412
|
let matrix;
|
|
8938
9413
|
let suiteId;
|
|
9414
|
+
const quiet = Boolean(values.json || values["dry-run"]);
|
|
9415
|
+
if (!quiet) banner("run", VERSION);
|
|
8939
9416
|
if (values.suite) {
|
|
8940
9417
|
const suite = registry.suites.find((item) => item.id === values.suite);
|
|
8941
|
-
|
|
8942
|
-
|
|
9418
|
+
const broken = project.loadErrors.filter((error) => error.suiteId === values.suite);
|
|
9419
|
+
if (!suite && !broken.length) throw new Error(`Unknown suite: ${values.suite}`);
|
|
9420
|
+
suiteId = values.suite;
|
|
8943
9421
|
matrix = defineEvalMatrix({
|
|
8944
9422
|
id: "default",
|
|
8945
|
-
evals: suite
|
|
9423
|
+
evals: suite?.evals ?? [],
|
|
8946
9424
|
parameters: project.config.matrix?.parameters ?? {},
|
|
8947
9425
|
defaults: project.config.matrix?.defaults,
|
|
8948
9426
|
cases: project.config.matrix?.cases,
|
|
@@ -8959,6 +9437,19 @@ async function runProjectCommand(args, cwd = process.cwd(), parsed = parseRunArg
|
|
|
8959
9437
|
parameters: {}
|
|
8960
9438
|
});
|
|
8961
9439
|
}
|
|
9440
|
+
const broken = brokenSelection(project.loadErrors, {
|
|
9441
|
+
...suiteId ? { suiteId } : {},
|
|
9442
|
+
evalIds: requestedEvals,
|
|
9443
|
+
registry
|
|
9444
|
+
});
|
|
9445
|
+
printLoadErrors(project.loadErrors, quiet ? console.error : console.log);
|
|
9446
|
+
if (broken.length && !values["skip-broken"] && !values["dry-run"]) throw new Error(`${plural(broken.length, "eval file")} in this selection failed to load: ${broken.map((error) => error.file).join(", ")}. Fix ${broken.length === 1 ? "it" : "them"}, or pass --skip-broken to run the rest.`);
|
|
9447
|
+
const knownEvals = requestedEvals.filter((id) => registry.get(id));
|
|
9448
|
+
if (values["skip-broken"] && knownEvals.length < requestedEvals.length) {
|
|
9449
|
+
requestedEvals.length = 0;
|
|
9450
|
+
requestedEvals.push(...knownEvals);
|
|
9451
|
+
if (!requestedEvals.length) throw new Error("Every selected eval is in a file that failed to load.");
|
|
9452
|
+
}
|
|
8962
9453
|
const selection = {
|
|
8963
9454
|
parameters: {},
|
|
8964
9455
|
overrides: { ...parameters },
|
|
@@ -8982,7 +9473,8 @@ async function runProjectCommand(args, cwd = process.cwd(), parsed = parseRunArg
|
|
|
8982
9473
|
console.log(JSON.stringify(plan, null, 2));
|
|
8983
9474
|
return;
|
|
8984
9475
|
}
|
|
8985
|
-
|
|
9476
|
+
const sweeps = Object.keys(matrix.parameters).length > 0 || Boolean(matrix.cases?.length);
|
|
9477
|
+
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.`);
|
|
8986
9478
|
const reportDir = resolve(project.root, project.config.reportDir ?? "_evalkit-results");
|
|
8987
9479
|
const sandboxDir = resolve(project.root, project.config.sandboxDir ?? "_evalkit-sandbox");
|
|
8988
9480
|
const source = await detectSource(project.root, { outputDirs: [
|
|
@@ -8990,36 +9482,75 @@ async function runProjectCommand(args, cwd = process.cwd(), parsed = parseRunArg
|
|
|
8990
9482
|
sandboxDir,
|
|
8991
9483
|
resolve(project.root, "_evalkit-dashboard")
|
|
8992
9484
|
] });
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9485
|
+
const suiteOf = new Map(registry.suites.flatMap((suite) => suite.evals.map((evaluation) => [authoringId(evaluation), suite.id])));
|
|
9486
|
+
const suiteFor = (evalId) => suiteId ?? suiteOf.get(evalId);
|
|
9487
|
+
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");
|
|
9488
|
+
const reporter = createRunReporter({
|
|
9489
|
+
interactive: interactive && !values.json,
|
|
9490
|
+
verbose: Boolean(values.verbose),
|
|
9491
|
+
total: cells,
|
|
9492
|
+
labelFor: (trial) => {
|
|
9493
|
+
const suite = suiteFor(trial.evalId);
|
|
9494
|
+
return suite ? `${suite}/${trial.evalId}` : trial.evalId;
|
|
9495
|
+
},
|
|
9496
|
+
write: values.json ? () => {} : void 0
|
|
9497
|
+
});
|
|
9498
|
+
const controller = new AbortController();
|
|
9499
|
+
const onInterrupt = () => {
|
|
9500
|
+
if (controller.signal.aborted) process.exit(130);
|
|
9501
|
+
controller.abort();
|
|
9502
|
+
if (!values.json) reporter.cancelling();
|
|
9503
|
+
};
|
|
9504
|
+
process.on("SIGINT", onInterrupt);
|
|
9505
|
+
reporter.start();
|
|
9506
|
+
let summary;
|
|
9507
|
+
try {
|
|
9508
|
+
summary = await Effect.runPromise(runnerLogs(runMatrix(matrix, {
|
|
9509
|
+
selection,
|
|
9510
|
+
concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
|
|
9511
|
+
trials,
|
|
9512
|
+
...suiteId ? { suiteId } : {},
|
|
9513
|
+
suiteFor: (cell) => suiteFor(authoringId(cell.eval)),
|
|
9514
|
+
...values.local ? { runtime: "local" } : {},
|
|
9515
|
+
...source ? { source } : {},
|
|
9516
|
+
catalog: runCatalog(registry, project.root),
|
|
9517
|
+
report: localReportStore(reportDir),
|
|
9518
|
+
workspaceRoot: sandboxDir,
|
|
9519
|
+
signal: controller.signal,
|
|
9520
|
+
onEvent: (trial, event) => reporter.onEvent(trial, event),
|
|
9521
|
+
onResult: (cell, result) => {
|
|
9522
|
+
const scenario = scenarioResult(cell, result, suiteFor(authoringId(cell.eval)));
|
|
9523
|
+
if (values.json) console.log(JSON.stringify({
|
|
9524
|
+
cell: {
|
|
9525
|
+
key: cell.key,
|
|
9526
|
+
eval: authoringId(cell.eval),
|
|
9527
|
+
parameters: cell.parameters
|
|
9528
|
+
},
|
|
9529
|
+
outcome: scenario.outcome,
|
|
9530
|
+
result
|
|
9531
|
+
}));
|
|
9532
|
+
reporter.onScenario(scenario);
|
|
9020
9533
|
}
|
|
9021
|
-
}
|
|
9022
|
-
}
|
|
9534
|
+
})));
|
|
9535
|
+
} finally {
|
|
9536
|
+
process.off("SIGINT", onInterrupt);
|
|
9537
|
+
}
|
|
9538
|
+
const totals = reporter.finish(performance.now() - started);
|
|
9539
|
+
if (values.junit) {
|
|
9540
|
+
const file = resolve(cwd, values.junit);
|
|
9541
|
+
await mkdir(dirname(file), { recursive: true });
|
|
9542
|
+
await writeFile(file, junitReport(reporter.scenarios()));
|
|
9543
|
+
}
|
|
9544
|
+
if (values.json) console.log(JSON.stringify({ summary: totals }));
|
|
9545
|
+
else {
|
|
9546
|
+
console.log(`\n${totalsLine(totals)}`);
|
|
9547
|
+
if (values.junit) console.log(` ${dim(`JUnit report: ${values.junit}`)}`);
|
|
9548
|
+
console.log(` ${cyan("➜")} bun run evalkit dev ${dim("to inspect")}`);
|
|
9549
|
+
if (project.config.share) console.log(` ${cyan("➜")} bun run evalkit share ${dim("to publish")}`);
|
|
9550
|
+
console.log("");
|
|
9551
|
+
}
|
|
9552
|
+
if (controller.signal.aborted) process.exitCode = 130;
|
|
9553
|
+
else if (summary.failed || broken.length) process.exitCode = 1;
|
|
9023
9554
|
}
|
|
9024
9555
|
//#endregion
|
|
9025
9556
|
//#region ../cli/src/tidy.ts
|
|
@@ -9083,7 +9614,8 @@ function selectRuns(runs, head, options) {
|
|
|
9083
9614
|
otherCommits: 0,
|
|
9084
9615
|
dirty: 0,
|
|
9085
9616
|
archived: 0,
|
|
9086
|
-
running: 0
|
|
9617
|
+
running: 0,
|
|
9618
|
+
cancelled: 0
|
|
9087
9619
|
};
|
|
9088
9620
|
for (const run of runs) {
|
|
9089
9621
|
const { source } = run;
|
|
@@ -9092,6 +9624,7 @@ function selectRuns(runs, head, options) {
|
|
|
9092
9624
|
else if (source.dirty && !options.allowDirty) result.dirty += 1;
|
|
9093
9625
|
else if (run.archived) result.archived += 1;
|
|
9094
9626
|
else if (run.status === "running") result.running += 1;
|
|
9627
|
+
else if (run.status === "cancelled") result.cancelled += 1;
|
|
9095
9628
|
else result.selected.push({
|
|
9096
9629
|
...run,
|
|
9097
9630
|
source
|
|
@@ -9105,6 +9638,7 @@ function selectionSummary(selection, options) {
|
|
|
9105
9638
|
`${plural(selection.selected.length, "run")} to share`,
|
|
9106
9639
|
selection.otherCommits && !options.all ? `${selection.otherCommits} from other commits` : "",
|
|
9107
9640
|
selection.running ? `${selection.running} still running skipped` : "",
|
|
9641
|
+
selection.cancelled ? `${selection.cancelled} cancelled skipped` : "",
|
|
9108
9642
|
selection.dirty ? `${selection.dirty} dirty skipped` : "",
|
|
9109
9643
|
selection.untracked ? `${selection.untracked} untracked skipped` : "",
|
|
9110
9644
|
selection.archived ? `${selection.archived} archived skipped` : ""
|
|
@@ -9152,29 +9686,61 @@ async function runBundle(options) {
|
|
|
9152
9686
|
}, { workspaceFiles: options.workspaceFiles });
|
|
9153
9687
|
return encodeBundle(files);
|
|
9154
9688
|
}
|
|
9155
|
-
/** The server refused
|
|
9689
|
+
/** The server refused this bundle (4xx): retrying it cannot help. */
|
|
9156
9690
|
var UploadRejected = class extends Error {};
|
|
9691
|
+
/**
|
|
9692
|
+
* The URL does not answer as an EvalKit share server (a redirect, an HTML page,
|
|
9693
|
+
* an auth proxy). Every upload would fail the same way, so sharing stops.
|
|
9694
|
+
*/
|
|
9695
|
+
var ShareServerError = class extends Error {};
|
|
9696
|
+
/** `share.headers` from the config: called once, checked, never printed. */
|
|
9697
|
+
async function resolveShareHeaders(headers) {
|
|
9698
|
+
let value;
|
|
9699
|
+
try {
|
|
9700
|
+
value = typeof headers === "function" ? await headers() : headers ?? {};
|
|
9701
|
+
} catch (error) {
|
|
9702
|
+
throw new Error(`share.headers failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9703
|
+
}
|
|
9704
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("share.headers must be an object of header names to values");
|
|
9705
|
+
const entries = Object.entries(value);
|
|
9706
|
+
for (const [name, item] of entries) if (typeof item !== "string" || !item) throw new Error(`share.headers "${name}" must be a non-empty string`);
|
|
9707
|
+
return Object.fromEntries(entries);
|
|
9708
|
+
}
|
|
9709
|
+
/** Check that the URL is an EvalKit share server and the headers get through. */
|
|
9710
|
+
async function checkShareServer(serverUrl, headers) {
|
|
9711
|
+
const response = await fetch(new URL("/api/v1/repos", serverUrl), {
|
|
9712
|
+
headers,
|
|
9713
|
+
redirect: "manual"
|
|
9714
|
+
});
|
|
9715
|
+
await expectShareServer(response, serverUrl);
|
|
9716
|
+
await response.body?.cancel();
|
|
9717
|
+
}
|
|
9157
9718
|
/** POST one bundle, retrying network errors and 5xx responses. */
|
|
9158
9719
|
async function uploadBundle(serverUrl, bundle, options = {}) {
|
|
9159
9720
|
const attempts = options.attempts ?? 3;
|
|
9160
9721
|
let lastError;
|
|
9161
9722
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
9162
9723
|
try {
|
|
9163
|
-
return await postBundle(serverUrl, bundle);
|
|
9724
|
+
return await postBundle(serverUrl, bundle, options.headers ?? {});
|
|
9164
9725
|
} catch (error) {
|
|
9165
|
-
if (error instanceof UploadRejected) throw error;
|
|
9726
|
+
if (error instanceof UploadRejected || error instanceof ShareServerError) throw error;
|
|
9166
9727
|
lastError = error;
|
|
9167
9728
|
}
|
|
9168
|
-
if (attempt < attempts) await
|
|
9729
|
+
if (attempt < attempts) await sleep((options.delayMs ?? 500) * 2 ** (attempt - 1));
|
|
9169
9730
|
}
|
|
9170
9731
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
|
|
9171
9732
|
}
|
|
9172
|
-
async function postBundle(serverUrl, bundle) {
|
|
9733
|
+
async function postBundle(serverUrl, bundle, headers) {
|
|
9173
9734
|
const response = await fetch(new URL("/api/v1/uploads", serverUrl), {
|
|
9174
9735
|
method: "POST",
|
|
9175
|
-
headers: {
|
|
9176
|
-
|
|
9736
|
+
headers: {
|
|
9737
|
+
...headers,
|
|
9738
|
+
"content-type": "application/octet-stream"
|
|
9739
|
+
},
|
|
9740
|
+
body: bundle,
|
|
9741
|
+
redirect: "manual"
|
|
9177
9742
|
});
|
|
9743
|
+
await expectShareServer(response, serverUrl);
|
|
9178
9744
|
if (response.ok) return parseUploadResult(await response.json());
|
|
9179
9745
|
const reason = await response.text();
|
|
9180
9746
|
if (response.status < 500) throw new UploadRejected(`Share server rejected the upload: ${reason}`);
|
|
@@ -9186,7 +9752,18 @@ function parseUploadResult(value) {
|
|
|
9186
9752
|
status: result.status,
|
|
9187
9753
|
url: result.url
|
|
9188
9754
|
};
|
|
9189
|
-
throw new
|
|
9755
|
+
throw new ShareServerError(`Unexpected share server response: ${JSON.stringify(value)}`);
|
|
9756
|
+
}
|
|
9757
|
+
/** Redirects and non-JSON answers come from something in front of the server. */
|
|
9758
|
+
async function expectShareServer(response, serverUrl) {
|
|
9759
|
+
if (response.status >= 300 && response.status < 400) {
|
|
9760
|
+
const location = response.headers.get("location");
|
|
9761
|
+
const target = location ? new URL(location, serverUrl).host : "elsewhere";
|
|
9762
|
+
throw new ShareServerError(`${new URL(serverUrl).host} redirected to ${target} instead of answering. If an auth proxy is in front of the share server, set share.headers in evalkit.config.ts.`);
|
|
9763
|
+
}
|
|
9764
|
+
const type = response.headers.get("content-type") ?? "";
|
|
9765
|
+
const denied = response.status === 401 || response.status === 403;
|
|
9766
|
+
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."));
|
|
9190
9767
|
}
|
|
9191
9768
|
async function shareRuns(options) {
|
|
9192
9769
|
const reports = createDashboardReportReader(options.reportRoot, options.registry);
|
|
@@ -9207,10 +9784,11 @@ async function shareRuns(options) {
|
|
|
9207
9784
|
run,
|
|
9208
9785
|
workspaceFiles: options.workspaceFiles
|
|
9209
9786
|
});
|
|
9210
|
-
const uploaded = await uploadBundle(options.serverUrl, bundle);
|
|
9787
|
+
const uploaded = await uploadBundle(options.serverUrl, bundle, { headers: options.headers });
|
|
9211
9788
|
result.bytes += bundle.byteLength;
|
|
9212
9789
|
result[uploaded.status] += 1;
|
|
9213
9790
|
} catch (error) {
|
|
9791
|
+
if (error instanceof ShareServerError) throw error;
|
|
9214
9792
|
result.failed.push({
|
|
9215
9793
|
runId: run.id,
|
|
9216
9794
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -9227,7 +9805,7 @@ function formatBytes(size) {
|
|
|
9227
9805
|
}
|
|
9228
9806
|
//#endregion
|
|
9229
9807
|
//#region ../cli/src/new-project-template.ts
|
|
9230
|
-
/** Provider-free example: one
|
|
9808
|
+
/** Provider-free example: one dataset, agent, scorer, and a two-cell matrix. */
|
|
9231
9809
|
const starterFiles = {
|
|
9232
9810
|
"tsconfig.json": `${JSON.stringify({
|
|
9233
9811
|
compilerOptions: {
|
|
@@ -9240,15 +9818,17 @@ const starterFiles = {
|
|
|
9240
9818
|
},
|
|
9241
9819
|
include: [
|
|
9242
9820
|
"agents/**/*.ts",
|
|
9243
|
-
"
|
|
9821
|
+
"datasets/**/*.ts",
|
|
9244
9822
|
"judges/**/*.ts",
|
|
9823
|
+
"scenarios/**/*.ts",
|
|
9245
9824
|
"evalkit.config.ts"
|
|
9246
9825
|
]
|
|
9247
9826
|
}, null, 2)}\n`,
|
|
9248
9827
|
"evalkit.config.ts": `import { defineConfig } from '@leostera/evalkit';
|
|
9249
9828
|
|
|
9250
9829
|
export default defineConfig({
|
|
9251
|
-
// Default-exported evals in
|
|
9830
|
+
// Default-exported evals in scenarios/**/*.eval.ts are discovered automatically;
|
|
9831
|
+
// each directory below scenarios/ is a suite.
|
|
9252
9832
|
matrix: {
|
|
9253
9833
|
id: 'styles',
|
|
9254
9834
|
parameters: { style: ['plain', 'shout'] },
|
|
@@ -9256,7 +9836,16 @@ export default defineConfig({
|
|
|
9256
9836
|
execution: { concurrency: 2, maxCells: 20 },
|
|
9257
9837
|
});
|
|
9258
9838
|
`,
|
|
9259
|
-
"
|
|
9839
|
+
"datasets/greeting/greeting.txt": "Hello\n",
|
|
9840
|
+
"datasets/greeting.dataset.ts": `import { defineDataset } from '@leostera/evalkit';
|
|
9841
|
+
|
|
9842
|
+
// Files the agent can read: copied into each trial's workspace under greeting/.
|
|
9843
|
+
// Paths are relative to the project root.
|
|
9844
|
+
export const greeting = defineDataset({
|
|
9845
|
+
id: 'greeting',
|
|
9846
|
+
files: 'datasets/greeting',
|
|
9847
|
+
});
|
|
9848
|
+
`,
|
|
9260
9849
|
"agents/greeting-agent.ts": `import { readFile } from 'node:fs/promises';
|
|
9261
9850
|
import { join } from 'node:path';
|
|
9262
9851
|
import { defineAgent } from '@leostera/evalkit';
|
|
@@ -9264,23 +9853,18 @@ import { defineAgent } from '@leostera/evalkit';
|
|
|
9264
9853
|
// Replace this in-process example with an adapter for your own agent.
|
|
9265
9854
|
export const greetingAgent = defineAgent({
|
|
9266
9855
|
identity: { id: 'greeting-agent', kind: 'in-process' },
|
|
9267
|
-
|
|
9268
|
-
async start({ context, onEvent }) {
|
|
9856
|
+
async start({ context }) {
|
|
9269
9857
|
return {
|
|
9858
|
+
// Return the reply; EvalKit records it. Call onEvent instead to stream events.
|
|
9270
9859
|
async send(message: string) {
|
|
9271
|
-
|
|
9272
|
-
const prefix = (await readFile(
|
|
9860
|
+
const file = join(context.workspace.root, 'greeting', 'greeting.txt');
|
|
9861
|
+
const prefix = (await readFile(file, 'utf8')).trim();
|
|
9273
9862
|
const style = context.parameters?.style;
|
|
9274
9863
|
if (style !== 'plain' && style !== 'shout')
|
|
9275
9864
|
throw new Error(\`Unknown style: \${String(style)}\`);
|
|
9276
9865
|
const greeting = \`\${prefix}, \${message}!\`;
|
|
9277
|
-
|
|
9278
|
-
kind: 'message', role: 'assistant',
|
|
9279
|
-
content: style === 'shout' ? greeting.toUpperCase() : greeting,
|
|
9280
|
-
timestamp: new Date().toISOString(),
|
|
9281
|
-
});
|
|
9866
|
+
return style === 'shout' ? greeting.toUpperCase() : greeting;
|
|
9282
9867
|
},
|
|
9283
|
-
async close() {},
|
|
9284
9868
|
};
|
|
9285
9869
|
},
|
|
9286
9870
|
});
|
|
@@ -9288,25 +9872,20 @@ export const greetingAgent = defineAgent({
|
|
|
9288
9872
|
"judges/matches-greeting.ts": `import { predicate } from '@leostera/evalkit';
|
|
9289
9873
|
|
|
9290
9874
|
// Deterministic scorers need no judge model; add a separate judge agent for judge(...) rules.
|
|
9291
|
-
export const matchesGreeting = predicate('matches greeting and style', ({ context,
|
|
9292
|
-
const reply = trajectory.events.filter(
|
|
9293
|
-
(event) => event.source === 'aut' && event.kind === 'message' && event.role === 'assistant',
|
|
9294
|
-
).at(-1);
|
|
9875
|
+
export const matchesGreeting = predicate('matches greeting and style', ({ context, output }) => {
|
|
9295
9876
|
const expected = context.parameters?.style === 'shout' ? 'HELLO, ADA!' : 'Hello, Ada!';
|
|
9296
|
-
return {
|
|
9297
|
-
value: reply?.kind === 'message' && reply.content === expected ? 1 : 0,
|
|
9298
|
-
explanation: \`Expected \${expected}\`,
|
|
9299
|
-
};
|
|
9877
|
+
return { value: output === expected ? 1 : 0, explanation: \`Expected \${expected}\` };
|
|
9300
9878
|
});
|
|
9301
9879
|
`,
|
|
9302
|
-
"
|
|
9880
|
+
"scenarios/greeting.eval.ts": `import { defineEval, user } from '@leostera/evalkit';
|
|
9303
9881
|
import { greetingAgent } from '../agents/greeting-agent.js';
|
|
9882
|
+
import { greeting } from '../datasets/greeting.dataset.js';
|
|
9304
9883
|
import { matchesGreeting } from '../judges/matches-greeting.js';
|
|
9305
9884
|
|
|
9306
9885
|
export default defineEval({
|
|
9307
9886
|
id: 'greeting',
|
|
9308
9887
|
agent: greetingAgent,
|
|
9309
|
-
|
|
9888
|
+
datasets: [greeting],
|
|
9310
9889
|
transcript: [user('Ada')],
|
|
9311
9890
|
scoring: [matchesGreeting],
|
|
9312
9891
|
});
|
|
@@ -9315,10 +9894,11 @@ export default defineEval({
|
|
|
9315
9894
|
function starterReadme(name) {
|
|
9316
9895
|
return `# ${name}
|
|
9317
9896
|
|
|
9318
|
-
A local EvalKit project. Each default-exported eval in \`
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9897
|
+
A local EvalKit project. Each default-exported eval in \`scenarios/\` is discovered
|
|
9898
|
+
automatically, and each directory below \`scenarios/\` is a suite. Start with
|
|
9899
|
+
\`scenarios/greeting.eval.ts\`: it imports an in-process agent from \`agents/\`, a
|
|
9900
|
+
dataset of files from \`datasets/\`, and a deterministic scorer from \`judges/\`.
|
|
9901
|
+
Keep each scenario small and concrete; share agents, datasets and judges between them.
|
|
9322
9902
|
|
|
9323
9903
|
\`evalkit.config.ts\` defines a provider-free \`styles\` matrix; the example agent reads
|
|
9324
9904
|
\`context.parameters.style\`. Run:
|
|
@@ -9332,6 +9912,9 @@ bun run evalkit dev # inspect local reports and laun
|
|
|
9332
9912
|
bun run evalkit build # write a read-only, shareable snapshot
|
|
9333
9913
|
\`\`\`
|
|
9334
9914
|
|
|
9915
|
+
Coding agents can use the EvalKit skills that ship with the package, in
|
|
9916
|
+
\`node_modules/@leostera/evalkit/dist/skills/\` (start with \`evalkit/SKILL.md\`).
|
|
9917
|
+
|
|
9335
9918
|
EvalKit installs from npm. Reports, trial workspaces, and dashboard snapshots stay local
|
|
9336
9919
|
and are ignored by Git. A snapshot in \`_evalkit-dashboard/\` contains report messages,
|
|
9337
9920
|
scores, evidence, and candidate files (never evaluator workspaces): review it before sharing.
|
|
@@ -9339,7 +9922,7 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
|
|
|
9339
9922
|
}
|
|
9340
9923
|
//#endregion
|
|
9341
9924
|
//#region ../cli/src/new-project.ts
|
|
9342
|
-
const npmDependency = "^0.
|
|
9925
|
+
const npmDependency = "^0.6.0";
|
|
9343
9926
|
const ignored = [
|
|
9344
9927
|
"node_modules/",
|
|
9345
9928
|
"_evalkit-results/",
|
|
@@ -9403,9 +9986,9 @@ async function checkExistingScaffold(root) {
|
|
|
9403
9986
|
for (const extension of ["js", "mjs"]) if (await exists(resolve(root, `evalkit.config.${extension}`))) throw new Error(`EvalKit config already exists: evalkit.config.${extension}`);
|
|
9404
9987
|
for (const directory of [
|
|
9405
9988
|
"agents",
|
|
9406
|
-
"
|
|
9407
|
-
"
|
|
9408
|
-
"
|
|
9989
|
+
"datasets",
|
|
9990
|
+
"judges",
|
|
9991
|
+
"scenarios"
|
|
9409
9992
|
]) {
|
|
9410
9993
|
const path = resolve(root, directory);
|
|
9411
9994
|
if (await exists(path) && !(await stat(path)).isDirectory()) throw new Error(`Scaffold directory is not a directory: ${path}`);
|
|
@@ -9451,8 +10034,300 @@ async function writeStarterReadme(root, name, inPlace) {
|
|
|
9451
10034
|
if (!inPlace && !await exists(readmePath)) await writeFile(readmePath, starterReadme(name));
|
|
9452
10035
|
}
|
|
9453
10036
|
//#endregion
|
|
10037
|
+
//#region ../cli/src/compare.ts
|
|
10038
|
+
const FINISHED = /* @__PURE__ */ new Set([
|
|
10039
|
+
"passed",
|
|
10040
|
+
"failed",
|
|
10041
|
+
"errored"
|
|
10042
|
+
]);
|
|
10043
|
+
function unitKey(run) {
|
|
10044
|
+
return `${run.evalId}\0${canonicalParameters(run.parameters ?? {})}`;
|
|
10045
|
+
}
|
|
10046
|
+
/** The run that represents each unit: the latest finished one, else the latest at all. */
|
|
10047
|
+
function latestByUnit(runs) {
|
|
10048
|
+
const byUnit = /* @__PURE__ */ new Map();
|
|
10049
|
+
let ignored = 0;
|
|
10050
|
+
const ordered = [...runs].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
10051
|
+
for (const run of ordered) {
|
|
10052
|
+
if (run.status === "running") continue;
|
|
10053
|
+
const key = unitKey(run);
|
|
10054
|
+
const current = byUnit.get(key);
|
|
10055
|
+
if (!current) byUnit.set(key, run);
|
|
10056
|
+
else {
|
|
10057
|
+
ignored += 1;
|
|
10058
|
+
if (!FINISHED.has(current.status) && FINISHED.has(run.status)) byUnit.set(key, run);
|
|
10059
|
+
}
|
|
10060
|
+
}
|
|
10061
|
+
return {
|
|
10062
|
+
byUnit,
|
|
10063
|
+
ignored
|
|
10064
|
+
};
|
|
10065
|
+
}
|
|
10066
|
+
function scorerSignature(run) {
|
|
10067
|
+
return (run.scorers ?? []).map((scorer) => `${scorer.kind}:${scorer.name}`).sort().join("\n");
|
|
10068
|
+
}
|
|
10069
|
+
function classify(base, head) {
|
|
10070
|
+
if (!base) return { change: "new" };
|
|
10071
|
+
if (!head) return { change: "removed" };
|
|
10072
|
+
for (const [side, run] of [["base", base], ["head", head]]) if (!FINISHED.has(run.status) || run.status === "errored") return {
|
|
10073
|
+
change: "not-comparable",
|
|
10074
|
+
reason: `${run.status} on ${side}`
|
|
10075
|
+
};
|
|
10076
|
+
if (scorerSignature(base) !== scorerSignature(head)) return {
|
|
10077
|
+
change: "not-comparable",
|
|
10078
|
+
reason: "different scorers"
|
|
10079
|
+
};
|
|
10080
|
+
if (base.status === "passed" && head.status === "failed") return { change: "regressed" };
|
|
10081
|
+
if (base.status === "failed" && head.status === "passed") return { change: "fixed" };
|
|
10082
|
+
return { change: "unchanged" };
|
|
10083
|
+
}
|
|
10084
|
+
function label(run, suite) {
|
|
10085
|
+
const parameters = Object.entries(run.parameters ?? {}).map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`).join(" ");
|
|
10086
|
+
const name = suite === "default" ? run.evalId : `${suite}/${run.evalId}`;
|
|
10087
|
+
return parameters ? `${name} [${parameters}]` : name;
|
|
10088
|
+
}
|
|
10089
|
+
function mean(values) {
|
|
10090
|
+
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : void 0;
|
|
10091
|
+
}
|
|
10092
|
+
function suiteScorers(units) {
|
|
10093
|
+
const sides = {
|
|
10094
|
+
base: /* @__PURE__ */ new Map(),
|
|
10095
|
+
head: /* @__PURE__ */ new Map()
|
|
10096
|
+
};
|
|
10097
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
10098
|
+
for (const unit of units) {
|
|
10099
|
+
if (unit.change === "not-comparable" || !unit.base || !unit.head) continue;
|
|
10100
|
+
for (const side of ["base", "head"]) for (const scorer of unit[side].scorers ?? []) {
|
|
10101
|
+
const key = `${scorer.kind}\0${scorer.name}`;
|
|
10102
|
+
kinds.set(key, {
|
|
10103
|
+
name: scorer.name,
|
|
10104
|
+
kind: scorer.kind
|
|
10105
|
+
});
|
|
10106
|
+
if (scorer.mean === void 0) continue;
|
|
10107
|
+
sides[side].set(key, [...sides[side].get(key) ?? [], scorer.mean]);
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
for (const unit of units) {
|
|
10111
|
+
if (!unit.base || !unit.head) continue;
|
|
10112
|
+
const names = (run) => new Set((run.scorers ?? []).map((s) => `${s.kind}\0${s.name}`));
|
|
10113
|
+
const baseNames = names(unit.base);
|
|
10114
|
+
const headNames = names(unit.head);
|
|
10115
|
+
for (const scorer of unit.base.scorers ?? []) if (!headNames.has(`${scorer.kind}\0${scorer.name}`)) kinds.set(`${scorer.kind}\0${scorer.name}`, {
|
|
10116
|
+
name: scorer.name,
|
|
10117
|
+
kind: scorer.kind,
|
|
10118
|
+
onlyOn: "base"
|
|
10119
|
+
});
|
|
10120
|
+
for (const scorer of unit.head.scorers ?? []) if (!baseNames.has(`${scorer.kind}\0${scorer.name}`)) kinds.set(`${scorer.kind}\0${scorer.name}`, {
|
|
10121
|
+
name: scorer.name,
|
|
10122
|
+
kind: scorer.kind,
|
|
10123
|
+
onlyOn: "head"
|
|
10124
|
+
});
|
|
10125
|
+
}
|
|
10126
|
+
return [...kinds].map(([key, scorer]) => {
|
|
10127
|
+
const base = mean(sides.base.get(key) ?? []);
|
|
10128
|
+
const head = mean(sides.head.get(key) ?? []);
|
|
10129
|
+
return {
|
|
10130
|
+
...scorer,
|
|
10131
|
+
...base === void 0 || scorer.onlyOn ? {} : { base },
|
|
10132
|
+
...head === void 0 || scorer.onlyOn ? {} : { head }
|
|
10133
|
+
};
|
|
10134
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
10135
|
+
}
|
|
10136
|
+
/**
|
|
10137
|
+
* Matches base and head runs by eval ID and parameters and classifies each
|
|
10138
|
+
* unit. It reads recorded verdicts and means; it never re-scores.
|
|
10139
|
+
*/
|
|
10140
|
+
function compareRuns(baseRuns, headRuns) {
|
|
10141
|
+
const base = latestByUnit(baseRuns);
|
|
10142
|
+
const head = latestByUnit(headRuns);
|
|
10143
|
+
const units = [.../* @__PURE__ */ new Set([...base.byUnit.keys(), ...head.byUnit.keys()])].map((key) => {
|
|
10144
|
+
const baseRun = base.byUnit.get(key);
|
|
10145
|
+
const headRun = head.byUnit.get(key);
|
|
10146
|
+
const run = headRun ?? baseRun;
|
|
10147
|
+
const suite = headRun?.suiteId ?? baseRun?.suiteId ?? "default";
|
|
10148
|
+
return {
|
|
10149
|
+
label: label(run, suite),
|
|
10150
|
+
evalId: run.evalId,
|
|
10151
|
+
suite,
|
|
10152
|
+
parameters: run.parameters ?? {},
|
|
10153
|
+
...classify(baseRun, headRun),
|
|
10154
|
+
...baseRun ? { base: baseRun } : {},
|
|
10155
|
+
...headRun ? { head: headRun } : {}
|
|
10156
|
+
};
|
|
10157
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
10158
|
+
const changedDatasets = /* @__PURE__ */ new Set();
|
|
10159
|
+
for (const unit of units) {
|
|
10160
|
+
if (!unit.base || !unit.head) continue;
|
|
10161
|
+
const digests = (run) => new Map((run.datasets ?? []).map((d) => [d.id, d.digests.join(",")]));
|
|
10162
|
+
const before = digests(unit.base);
|
|
10163
|
+
const after = digests(unit.head);
|
|
10164
|
+
for (const id of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) if (before.get(id) !== after.get(id)) changedDatasets.add(id);
|
|
10165
|
+
}
|
|
10166
|
+
const bySuite = /* @__PURE__ */ new Map();
|
|
10167
|
+
for (const unit of units) bySuite.set(unit.suite, [...bySuite.get(unit.suite) ?? [], unit]);
|
|
10168
|
+
const suites = [...bySuite].map(([suite, members]) => {
|
|
10169
|
+
const side = (pick) => {
|
|
10170
|
+
const runs = members.flatMap((unit) => unit[pick] ? [unit[pick]] : []);
|
|
10171
|
+
return {
|
|
10172
|
+
passed: runs.filter((run) => run.status === "passed").length,
|
|
10173
|
+
total: runs.length
|
|
10174
|
+
};
|
|
10175
|
+
};
|
|
10176
|
+
return {
|
|
10177
|
+
suite,
|
|
10178
|
+
base: side("base"),
|
|
10179
|
+
head: side("head"),
|
|
10180
|
+
scorers: suiteScorers(members)
|
|
10181
|
+
};
|
|
10182
|
+
}).sort((a, b) => a.suite.localeCompare(b.suite));
|
|
10183
|
+
const counts = {
|
|
10184
|
+
regressed: 0,
|
|
10185
|
+
fixed: 0,
|
|
10186
|
+
new: 0,
|
|
10187
|
+
removed: 0,
|
|
10188
|
+
unchanged: 0,
|
|
10189
|
+
"not-comparable": 0
|
|
10190
|
+
};
|
|
10191
|
+
for (const unit of units) counts[unit.change] += 1;
|
|
10192
|
+
return {
|
|
10193
|
+
units,
|
|
10194
|
+
suites,
|
|
10195
|
+
changedDatasets: [...changedDatasets].sort(),
|
|
10196
|
+
ignoredRuns: {
|
|
10197
|
+
base: base.ignored,
|
|
10198
|
+
head: head.ignored
|
|
10199
|
+
},
|
|
10200
|
+
counts
|
|
10201
|
+
};
|
|
10202
|
+
}
|
|
10203
|
+
function parseFailOn(values) {
|
|
10204
|
+
return values.flatMap((value) => value.split(",")).map((value) => {
|
|
10205
|
+
if (value === "regression") return { kind: "regression" };
|
|
10206
|
+
const drop = /^score-drop=(\d*\.?\d+)$/.exec(value);
|
|
10207
|
+
if (drop) {
|
|
10208
|
+
const threshold = Number(drop[1]);
|
|
10209
|
+
if (threshold >= 0 && threshold <= 1) return {
|
|
10210
|
+
kind: "score-drop",
|
|
10211
|
+
threshold
|
|
10212
|
+
};
|
|
10213
|
+
}
|
|
10214
|
+
throw new Error(`Unknown --fail-on ${JSON.stringify(value)}; use regression or score-drop=0.05`);
|
|
10215
|
+
});
|
|
10216
|
+
}
|
|
10217
|
+
/** Human-readable reasons the comparison fails the given conditions; empty when it passes. */
|
|
10218
|
+
function gateFailures(comparison, conditions) {
|
|
10219
|
+
const failures = [];
|
|
10220
|
+
for (const condition of conditions) {
|
|
10221
|
+
if (condition.kind === "regression" && comparison.counts.regressed) failures.push(`${comparison.counts.regressed} regressed: ${comparison.units.filter((unit) => unit.change === "regressed").map((unit) => unit.label).join(", ")}`);
|
|
10222
|
+
if (condition.kind === "score-drop") {
|
|
10223
|
+
for (const suite of comparison.suites) for (const scorer of suite.scorers) if (scorer.base !== void 0 && scorer.head !== void 0 && scorer.base - scorer.head > condition.threshold) failures.push(`${suite.suite} ${scorer.name} dropped ${scorer.base.toFixed(2)} → ${scorer.head.toFixed(2)} (more than ${condition.threshold})`);
|
|
10224
|
+
}
|
|
10225
|
+
}
|
|
10226
|
+
return failures;
|
|
10227
|
+
}
|
|
10228
|
+
const fixed2 = (value) => value === void 0 ? "–" : value.toFixed(2);
|
|
10229
|
+
function scorerText(scorer) {
|
|
10230
|
+
if (scorer.onlyOn) return `${scorer.name} only on ${scorer.onlyOn}`;
|
|
10231
|
+
return `${scorer.name} ${fixed2(scorer.base)} → ${fixed2(scorer.head)}`;
|
|
10232
|
+
}
|
|
10233
|
+
/** A merge request comment. */
|
|
10234
|
+
function comparisonMarkdown(comparison, heading) {
|
|
10235
|
+
const { counts } = comparison;
|
|
10236
|
+
const list = (change) => comparison.units.filter((unit) => unit.change === change).map((unit) => `- \`${unit.label}\`${unit.reason ? ` (${unit.reason})` : ""}`);
|
|
10237
|
+
const lines = [
|
|
10238
|
+
`### EvalKit: ${heading.base} → ${heading.head}`,
|
|
10239
|
+
"",
|
|
10240
|
+
`**${counts.regressed} regressed** · ${counts.fixed} fixed · ${counts.new} new · ${counts.removed} removed · ${counts.unchanged} unchanged${counts["not-comparable"] ? ` · ${counts["not-comparable"]} not comparable` : ""}`,
|
|
10241
|
+
""
|
|
10242
|
+
];
|
|
10243
|
+
for (const [title, change] of [
|
|
10244
|
+
["Regressed", "regressed"],
|
|
10245
|
+
["Fixed", "fixed"],
|
|
10246
|
+
["New", "new"],
|
|
10247
|
+
["Removed", "removed"],
|
|
10248
|
+
["Not comparable", "not-comparable"]
|
|
10249
|
+
]) {
|
|
10250
|
+
const items = list(change);
|
|
10251
|
+
if (items.length) lines.push(`**${title}**`, "", ...items, "");
|
|
10252
|
+
}
|
|
10253
|
+
lines.push("| Suite | Passed | Scores |", "| --- | --- | --- |");
|
|
10254
|
+
for (const suite of comparison.suites) lines.push(`| ${suite.suite} | ${suite.base.passed}/${suite.base.total} → ${suite.head.passed}/${suite.head.total} | ${suite.scorers.map(scorerText).join("<br>") || "–"} |`);
|
|
10255
|
+
if (comparison.changedDatasets.length) lines.push("", `Datasets changed: ${comparison.changedDatasets.map((id) => `\`${id}\``).join(", ")}`);
|
|
10256
|
+
return `${lines.join("\n")}\n`;
|
|
10257
|
+
}
|
|
10258
|
+
function comparisonLines(comparison) {
|
|
10259
|
+
const { counts } = comparison;
|
|
10260
|
+
const labels = (change) => comparison.units.filter((unit) => unit.change === change).map((unit) => unit.reason ? `${unit.label} (${unit.reason})` : unit.label);
|
|
10261
|
+
return {
|
|
10262
|
+
units: [
|
|
10263
|
+
"regressed",
|
|
10264
|
+
"fixed",
|
|
10265
|
+
"not-comparable"
|
|
10266
|
+
].map((change) => ({
|
|
10267
|
+
change,
|
|
10268
|
+
labels: labels(change)
|
|
10269
|
+
})).filter((entry) => entry.labels.length),
|
|
10270
|
+
counts: `+ ${counts.new} new − ${counts.removed} removed ${counts.unchanged} unchanged`,
|
|
10271
|
+
suites: comparison.suites.map((suite) => `${suite.suite.padEnd(12)} ${suite.base.passed}/${suite.base.total} → ${suite.head.passed}/${suite.head.total} ${suite.scorers.map(scorerText).join(" · ")}`),
|
|
10272
|
+
...comparison.changedDatasets.length ? { datasets: `datasets ${comparison.changedDatasets.join(", ")} changed` } : {}
|
|
10273
|
+
};
|
|
10274
|
+
}
|
|
10275
|
+
//#endregion
|
|
10276
|
+
//#region ../cli/src/compare-command.ts
|
|
10277
|
+
/** The runs at HEAD that `compare` uses: not running, not archived, clean unless allowed. */
|
|
10278
|
+
function headRuns(runs, head, options) {
|
|
10279
|
+
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");
|
|
10280
|
+
}
|
|
10281
|
+
/** Base runs from a results directory, e.g. an artifact from the base pipeline. Dirty runs are never a base. */
|
|
10282
|
+
async function baseRunsFromDirectory(directory, registry) {
|
|
10283
|
+
return (await createDashboardReportReader(directory, registry).listRuns()).filter((run) => !run.source?.dirty && !run.archived);
|
|
10284
|
+
}
|
|
10285
|
+
/** A full SHA, a unique prefix of at least 7 characters, or a ref's latest shared commit. */
|
|
10286
|
+
function resolveSharedRef(commits, ref) {
|
|
10287
|
+
if (/^[0-9a-f]{40}$/.test(ref)) return commits.some((row) => row.commit === ref) ? ref : void 0;
|
|
10288
|
+
if (/^[0-9a-f]{7,39}$/.test(ref)) {
|
|
10289
|
+
const matches = commits.filter((row) => row.commit.startsWith(ref));
|
|
10290
|
+
if (matches.length === 1) return matches[0].commit;
|
|
10291
|
+
}
|
|
10292
|
+
return commits.find((row) => row.ref === ref)?.commit;
|
|
10293
|
+
}
|
|
10294
|
+
async function getJson(url, serverUrl, headers) {
|
|
10295
|
+
const response = await fetch(url, {
|
|
10296
|
+
headers,
|
|
10297
|
+
redirect: "manual"
|
|
10298
|
+
});
|
|
10299
|
+
await expectShareServer(response, serverUrl);
|
|
10300
|
+
if (response.status === 404) {
|
|
10301
|
+
await response.body?.cancel();
|
|
10302
|
+
return;
|
|
10303
|
+
}
|
|
10304
|
+
if (!response.ok) throw new Error(`Share server error ${response.status}: ${await response.text()}`);
|
|
10305
|
+
return await response.json();
|
|
10306
|
+
}
|
|
10307
|
+
/** Base runs shared for `ref` of this repository; `undefined` when the base was never shared. */
|
|
10308
|
+
async function baseRunsFromServer(options) {
|
|
10309
|
+
const repo = repoKey(options.head);
|
|
10310
|
+
const api = (path) => new URL(`/api/v1/repos/${repo}/-/${path}`, options.serverUrl);
|
|
10311
|
+
const listed = await getJson(api("commits"), options.serverUrl, options.headers);
|
|
10312
|
+
const commits = Array.isArray(listed?.commits) ? listed.commits : [];
|
|
10313
|
+
const commit = resolveSharedRef(commits, options.ref);
|
|
10314
|
+
if (!commit) return void 0;
|
|
10315
|
+
const data = await getJson(api(`${commit}/data/runs.json`), options.serverUrl, options.headers);
|
|
10316
|
+
const runs = (Array.isArray(data?.runs) ? data.runs : []).filter(isComparedRun);
|
|
10317
|
+
const ref = commits.find((row) => row.commit === commit)?.ref;
|
|
10318
|
+
return {
|
|
10319
|
+
commit,
|
|
10320
|
+
...ref ? { ref } : {},
|
|
10321
|
+
runs: runs.filter((run) => !run.source?.dirty)
|
|
10322
|
+
};
|
|
10323
|
+
}
|
|
10324
|
+
/** Shared runs are uploaded by other EvalKit versions; keep only what compare can read. */
|
|
10325
|
+
function isComparedRun(value) {
|
|
10326
|
+
const run = value;
|
|
10327
|
+
return typeof run === "object" && run !== null && typeof run.id === "string" && typeof run.evalId === "string" && typeof run.status === "string" && typeof run.startedAt === "string";
|
|
10328
|
+
}
|
|
10329
|
+
//#endregion
|
|
9454
10330
|
//#region ../cli/src/index.ts
|
|
9455
|
-
const VERSION = "0.4.0";
|
|
9456
10331
|
let projectRoot = process.cwd();
|
|
9457
10332
|
let reportRoot = resolve(projectRoot, "_evalkit-results");
|
|
9458
10333
|
const sandboxRoot = resolve(projectRoot, "_evalkit-sandbox");
|
|
@@ -9466,7 +10341,7 @@ function runtimeFor(evaluation) {
|
|
|
9466
10341
|
"local",
|
|
9467
10342
|
"remote",
|
|
9468
10343
|
"sandbox"
|
|
9469
|
-
]) if (evaluation.agent
|
|
10344
|
+
]) if (agentRuntimes(evaluation.agent)[runtime]) return runtime;
|
|
9470
10345
|
}
|
|
9471
10346
|
async function runEvals(options) {
|
|
9472
10347
|
const registry = await loadRegistry();
|
|
@@ -9495,7 +10370,7 @@ async function runProvenance(registry) {
|
|
|
9495
10370
|
] });
|
|
9496
10371
|
return {
|
|
9497
10372
|
...source ? { source } : {},
|
|
9498
|
-
catalog: runCatalog(registry)
|
|
10373
|
+
catalog: runCatalog(registry, projectRoot)
|
|
9499
10374
|
};
|
|
9500
10375
|
}
|
|
9501
10376
|
function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
|
|
@@ -9577,7 +10452,37 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
|
|
|
9577
10452
|
});
|
|
9578
10453
|
}
|
|
9579
10454
|
function registerReportRoutes(app, registry, reports) {
|
|
9580
|
-
app.get("/v1/
|
|
10455
|
+
app.get("/v1/project", async (context) => {
|
|
10456
|
+
const source = await detectSource(projectRoot, { outputDirs: [
|
|
10457
|
+
reportRoot,
|
|
10458
|
+
sandboxRoot,
|
|
10459
|
+
resolve(projectRoot, "_evalkit-dashboard")
|
|
10460
|
+
] });
|
|
10461
|
+
return context.json({ project: source ? projectInfo(source) : null });
|
|
10462
|
+
});
|
|
10463
|
+
app.get("/v1/datasets/:id/files", async (context) => {
|
|
10464
|
+
const dataset = projectDatasets(registry).get(context.req.param("id"));
|
|
10465
|
+
if (!dataset) return context.text("Unknown dataset", 404);
|
|
10466
|
+
try {
|
|
10467
|
+
return context.json(await datasetListing(dataset, projectRoot));
|
|
10468
|
+
} catch (error) {
|
|
10469
|
+
return context.text(error instanceof Error ? error.message : "Unable to list dataset", 400);
|
|
10470
|
+
}
|
|
10471
|
+
});
|
|
10472
|
+
app.get("/v1/datasets/:id/files/:path{.+}", async (context) => {
|
|
10473
|
+
const dataset = projectDatasets(registry).get(context.req.param("id"));
|
|
10474
|
+
if (!dataset) return context.text("Unknown dataset", 404);
|
|
10475
|
+
const file = context.req.param("path");
|
|
10476
|
+
try {
|
|
10477
|
+
return new Response(await staticDatasetFile(dataset, projectRoot, file), { headers: { "content-type": fileContentType(file) } });
|
|
10478
|
+
} catch (error) {
|
|
10479
|
+
return context.text(error instanceof Error ? error.message : "Unable to read dataset file", 404);
|
|
10480
|
+
}
|
|
10481
|
+
});
|
|
10482
|
+
app.get("/v1/catalog", (context) => context.json({
|
|
10483
|
+
evals: registry.catalog({ root: projectRoot }),
|
|
10484
|
+
loadErrors: project?.loadErrors ?? []
|
|
10485
|
+
}));
|
|
9581
10486
|
app.get("/v1/evals", (context) => context.json({ evals: registry.metadata() }));
|
|
9582
10487
|
app.get("/v1/suites", (context) => context.json({ suites: registry.suiteMetadata() }));
|
|
9583
10488
|
app.get("/v1/runs", async (context) => context.json({ runs: await reports.listRuns() }));
|
|
@@ -9602,9 +10507,9 @@ function registerReportRoutes(app, registry, reports) {
|
|
|
9602
10507
|
return context.text(error instanceof Error ? error.message : "Unable to list workspace", 400);
|
|
9603
10508
|
}
|
|
9604
10509
|
});
|
|
9605
|
-
app.get("/v1/runs/:runId/trials/:trialId/workspace
|
|
10510
|
+
app.get("/v1/runs/:runId/trials/:trialId/workspace/:path{.+}", async (context) => {
|
|
9606
10511
|
try {
|
|
9607
|
-
return await reports.candidateFile(context.req.param("runId"), context.req.param("trialId"), context.req.param("
|
|
10512
|
+
return await reports.candidateFile(context.req.param("runId"), context.req.param("trialId"), context.req.param("path"));
|
|
9608
10513
|
} catch (error) {
|
|
9609
10514
|
return context.text(error instanceof Error ? error.message : "Unable to read workspace file", 400);
|
|
9610
10515
|
}
|
|
@@ -9630,12 +10535,12 @@ function registerDashboardAssets(app, dashboardRoot) {
|
|
|
9630
10535
|
const pathname = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
9631
10536
|
const file = resolve(dashboardRoot, `.${pathname}`);
|
|
9632
10537
|
if (!file.startsWith(dashboardRoot)) return context.text("Not found", 404);
|
|
9633
|
-
let content = Bun.file(file);
|
|
9634
10538
|
let servedFile = file;
|
|
9635
|
-
|
|
10539
|
+
let content = await readFile(file).catch(() => void 0);
|
|
10540
|
+
if (!content) {
|
|
9636
10541
|
if (extname(pathname)) return context.text("Not found", 404);
|
|
9637
10542
|
servedFile = resolve(dashboardRoot, "index.html");
|
|
9638
|
-
content =
|
|
10543
|
+
content = await readFile(servedFile);
|
|
9639
10544
|
}
|
|
9640
10545
|
return new Response(content, { headers: { "content-type": fileContentType(servedFile) } });
|
|
9641
10546
|
});
|
|
@@ -9663,10 +10568,7 @@ async function serveDashboard() {
|
|
|
9663
10568
|
const port = Number(process.env.PORT ?? 4317);
|
|
9664
10569
|
let server;
|
|
9665
10570
|
try {
|
|
9666
|
-
server =
|
|
9667
|
-
port,
|
|
9668
|
-
fetch: app.fetch
|
|
9669
|
-
});
|
|
10571
|
+
server = await serve(app.fetch, port);
|
|
9670
10572
|
} catch (error) {
|
|
9671
10573
|
if (error.code === "EADDRINUSE") throw new Error(`Port ${port} is already in use. Stop the other server or choose another port: PORT=4318 evalkit dev`);
|
|
9672
10574
|
throw error;
|
|
@@ -9678,19 +10580,12 @@ async function serveDashboard() {
|
|
|
9678
10580
|
const matrixCells = dashboardMatrix?.count({ parameters: {} });
|
|
9679
10581
|
const row = (label, value) => console.log(` ${cyan("➜")} ${bold(label.padEnd(9))}${value}`);
|
|
9680
10582
|
banner("dev", VERSION);
|
|
10583
|
+
printLoadErrors(project?.loadErrors ?? []);
|
|
9681
10584
|
row("Local:", cyan(url));
|
|
9682
10585
|
row("Project:", `${plural(registry.evals.length, "eval")} · ${plural(registry.suites.length, "suite")}` + (dashboardMatrix ? ` · matrix ${authoringId(dashboardMatrix)} (${plural(matrixCells, "cell")})` : ""));
|
|
9683
10586
|
row("Reports:", `${relative(process.cwd(), reportRoot) || "."} ${dim(`(${plural(runs, "run")}${legacy ? `, ${legacy} from an older EvalKit hidden` : ""})`)}`);
|
|
9684
10587
|
console.log(`\n ${dim("Runs started from the dashboard are logged below. Press Ctrl+C to stop.")}\n`);
|
|
9685
|
-
if (process.env.EVALKIT_NO_OPEN !== "1")
|
|
9686
|
-
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
9687
|
-
try {
|
|
9688
|
-
Bun.spawn([opener, url], {
|
|
9689
|
-
stdout: "ignore",
|
|
9690
|
-
stderr: "ignore"
|
|
9691
|
-
});
|
|
9692
|
-
} catch {}
|
|
9693
|
-
}
|
|
10588
|
+
if (process.env.EVALKIT_NO_OPEN !== "1") openInBrowser(url);
|
|
9694
10589
|
}
|
|
9695
10590
|
const config = text("config").pipe(optional);
|
|
9696
10591
|
const runOptions = {
|
|
@@ -9710,7 +10605,10 @@ const runOptions = {
|
|
|
9710
10605
|
json: boolean("json"),
|
|
9711
10606
|
dryRun: boolean("dry-run"),
|
|
9712
10607
|
all: boolean("all"),
|
|
9713
|
-
local: boolean("local")
|
|
10608
|
+
local: boolean("local"),
|
|
10609
|
+
junit: text("junit").pipe(optional),
|
|
10610
|
+
verbose: boolean("verbose"),
|
|
10611
|
+
skipBroken: boolean("skip-broken")
|
|
9714
10612
|
};
|
|
9715
10613
|
function runInput(options, positionals) {
|
|
9716
10614
|
const unexpected = positionals.find((value) => value.startsWith("-"));
|
|
@@ -9732,7 +10630,10 @@ function runInput(options, positionals) {
|
|
|
9732
10630
|
json: options.json,
|
|
9733
10631
|
"dry-run": options.dryRun,
|
|
9734
10632
|
all: options.all,
|
|
9735
|
-
local: options.local
|
|
10633
|
+
local: options.local,
|
|
10634
|
+
junit: Option.getOrUndefined(options.junit),
|
|
10635
|
+
verbose: options.verbose,
|
|
10636
|
+
"skip-broken": options.skipBroken
|
|
9736
10637
|
}, positionals);
|
|
9737
10638
|
}
|
|
9738
10639
|
const execute = (input) => Effect.tryPromise({
|
|
@@ -9775,6 +10676,12 @@ const buildCommand = make$1("build", {
|
|
|
9775
10676
|
})) await runAllEvals(configPath);
|
|
9776
10677
|
const result = await buildDashboard({
|
|
9777
10678
|
registry: project.registry,
|
|
10679
|
+
root: projectRoot,
|
|
10680
|
+
source: await detectSource(projectRoot, { outputDirs: [
|
|
10681
|
+
reportRoot,
|
|
10682
|
+
sandboxRoot,
|
|
10683
|
+
resolve(projectRoot, "_evalkit-dashboard")
|
|
10684
|
+
] }),
|
|
9778
10685
|
reportRoot,
|
|
9779
10686
|
dashboardRoot: dashboardAssets(),
|
|
9780
10687
|
outDir: resolve(projectRoot, Option.getOrElse(out, () => "_evalkit-dashboard"))
|
|
@@ -9791,8 +10698,13 @@ const buildCommand = make$1("build", {
|
|
|
9791
10698
|
async function loadProjectStep(configPath) {
|
|
9792
10699
|
await step("Eval project", () => useProject(configPath), () => {
|
|
9793
10700
|
const where = relative(process.cwd(), projectRoot);
|
|
9794
|
-
|
|
10701
|
+
const broken = project.loadErrors.length;
|
|
10702
|
+
return `${plural(project.registry.evals.length, "eval")} loaded${where ? ` from ${where}` : ""}${broken ? yellow(` · ${plural(broken, "file")} failed to load`) : ""}`;
|
|
9795
10703
|
});
|
|
10704
|
+
if (project.loadErrors.length) {
|
|
10705
|
+
console.log("");
|
|
10706
|
+
printLoadErrors(project.loadErrors);
|
|
10707
|
+
}
|
|
9796
10708
|
}
|
|
9797
10709
|
/**
|
|
9798
10710
|
* A snapshot needs data: with no current results, run the evals. Otherwise ask,
|
|
@@ -9850,10 +10762,7 @@ const shareServeCommand = make$1("serve", {
|
|
|
9850
10762
|
});
|
|
9851
10763
|
let server;
|
|
9852
10764
|
try {
|
|
9853
|
-
server =
|
|
9854
|
-
port,
|
|
9855
|
-
fetch: app.fetch
|
|
9856
|
-
});
|
|
10765
|
+
server = await serve(app.fetch, port);
|
|
9857
10766
|
} catch (error) {
|
|
9858
10767
|
if (error.code === "EADDRINUSE") throw new Error(`Port ${port} is already in use. Choose another: evalkit share serve --port 4319`);
|
|
9859
10768
|
throw error;
|
|
@@ -9891,6 +10800,14 @@ const shareCommand = make$1("share", {
|
|
|
9891
10800
|
if (!selection.selected.length) throw new Error(noRunsMessage(selection, head, { all }));
|
|
9892
10801
|
const result = await shareRuns({
|
|
9893
10802
|
serverUrl,
|
|
10803
|
+
headers: await step("Server", async () => {
|
|
10804
|
+
const headers = await resolveShareHeaders(project.config.share?.headers);
|
|
10805
|
+
await checkShareServer(serverUrl, headers);
|
|
10806
|
+
return headers;
|
|
10807
|
+
}, (headers) => {
|
|
10808
|
+
const names = Object.keys(headers);
|
|
10809
|
+
return `${new URL(serverUrl).host}${names.length ? dim(` with ${names.join(", ")}`) : ""}`;
|
|
10810
|
+
}),
|
|
9894
10811
|
reportRoot,
|
|
9895
10812
|
registry,
|
|
9896
10813
|
runs: selection.selected,
|
|
@@ -9905,13 +10822,118 @@ const shareCommand = make$1("share", {
|
|
|
9905
10822
|
},
|
|
9906
10823
|
catch: (error) => error
|
|
9907
10824
|
})).pipe(withSubcommands([shareServeCommand]));
|
|
10825
|
+
const compareCommand = make$1("compare", {
|
|
10826
|
+
config,
|
|
10827
|
+
base: text("base").pipe(optional),
|
|
10828
|
+
baseDir: text("base-dir").pipe(optional),
|
|
10829
|
+
markdown: text("markdown").pipe(optional),
|
|
10830
|
+
json: boolean("json"),
|
|
10831
|
+
failOn: text("fail-on").pipe(repeated),
|
|
10832
|
+
requireBase: boolean("require-base"),
|
|
10833
|
+
allowDirty: boolean("allow-dirty")
|
|
10834
|
+
}, (options) => Effect.tryPromise({
|
|
10835
|
+
try: () => compareCommandAction({
|
|
10836
|
+
config: Option.getOrUndefined(options.config),
|
|
10837
|
+
base: Option.getOrUndefined(options.base),
|
|
10838
|
+
baseDir: Option.getOrUndefined(options.baseDir),
|
|
10839
|
+
markdown: Option.getOrUndefined(options.markdown),
|
|
10840
|
+
json: options.json,
|
|
10841
|
+
failOn: options.failOn,
|
|
10842
|
+
requireBase: options.requireBase,
|
|
10843
|
+
allowDirty: options.allowDirty
|
|
10844
|
+
}),
|
|
10845
|
+
catch: (error) => error
|
|
10846
|
+
}));
|
|
10847
|
+
async function compareCommandAction(options) {
|
|
10848
|
+
if (Boolean(options.base) === Boolean(options.baseDir)) throw new Error("Give exactly one of --base <ref> or --base-dir <dir>");
|
|
10849
|
+
const conditions = parseFailOn(options.failOn);
|
|
10850
|
+
const say = options.json ? () => {} : (line = "") => console.log(line);
|
|
10851
|
+
if (!options.json) banner("compare", VERSION);
|
|
10852
|
+
await useProject(options.config);
|
|
10853
|
+
const registry = project.registry;
|
|
10854
|
+
const head = await detectSource(projectRoot, { outputDirs: [
|
|
10855
|
+
reportRoot,
|
|
10856
|
+
sandboxRoot,
|
|
10857
|
+
resolve(projectRoot, "_evalkit-dashboard")
|
|
10858
|
+
] });
|
|
10859
|
+
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.");
|
|
10860
|
+
const headSide = headRuns(await createDashboardReportReader(reportRoot, registry).listRuns(), head, { allowDirty: options.allowDirty });
|
|
10861
|
+
let base;
|
|
10862
|
+
if (options.baseDir) {
|
|
10863
|
+
const runs = await baseRunsFromDirectory(resolve(process.cwd(), options.baseDir), registry);
|
|
10864
|
+
base = runs.length ? {
|
|
10865
|
+
label: options.baseDir,
|
|
10866
|
+
runs
|
|
10867
|
+
} : void 0;
|
|
10868
|
+
} else {
|
|
10869
|
+
const serverUrl = process.env.EVALKIT_SHARE_URL ?? project.config.share?.url;
|
|
10870
|
+
if (!serverUrl) throw new Error("No share server configured for --base. Set share: { url } in evalkit.config.ts or EVALKIT_SHARE_URL, or use --base-dir.");
|
|
10871
|
+
const found = await baseRunsFromServer({
|
|
10872
|
+
serverUrl,
|
|
10873
|
+
headers: await resolveShareHeaders(project.config.share?.headers),
|
|
10874
|
+
head,
|
|
10875
|
+
ref: options.base
|
|
10876
|
+
});
|
|
10877
|
+
base = found?.runs.length ? {
|
|
10878
|
+
label: `${found.commit.slice(0, 7)}${found.ref ? ` (${found.ref})` : ""}`,
|
|
10879
|
+
runs: found.runs
|
|
10880
|
+
} : void 0;
|
|
10881
|
+
}
|
|
10882
|
+
const headLabel = `${shortSource(head)}${head.ref ? ` (${head.ref})` : ""}`;
|
|
10883
|
+
if (!base) {
|
|
10884
|
+
const message = `No shared results for base ${options.base ?? options.baseDir}; nothing to compare.`;
|
|
10885
|
+
if (options.json) console.log(JSON.stringify({
|
|
10886
|
+
base: null,
|
|
10887
|
+
head: headLabel,
|
|
10888
|
+
message
|
|
10889
|
+
}));
|
|
10890
|
+
else say(` ${yellow("◇")} ${message}\n`);
|
|
10891
|
+
if (options.requireBase) process.exitCode = 1;
|
|
10892
|
+
return;
|
|
10893
|
+
}
|
|
10894
|
+
if (!headSide.length) throw new Error(`No results at ${headLabel} to compare. Run \`evalkit run\` first${head.dirty ? ", or pass --allow-dirty" : ""}.`);
|
|
10895
|
+
const comparison = compareRuns(base.runs, headSide);
|
|
10896
|
+
const failures = gateFailures(comparison, conditions);
|
|
10897
|
+
if (options.markdown) await writeFile(resolve(process.cwd(), options.markdown), comparisonMarkdown(comparison, {
|
|
10898
|
+
base: base.label,
|
|
10899
|
+
head: headLabel
|
|
10900
|
+
}));
|
|
10901
|
+
if (options.json) console.log(JSON.stringify({
|
|
10902
|
+
base: base.label,
|
|
10903
|
+
head: headLabel,
|
|
10904
|
+
...comparison,
|
|
10905
|
+
failures
|
|
10906
|
+
}));
|
|
10907
|
+
else {
|
|
10908
|
+
const lines = comparisonLines(comparison);
|
|
10909
|
+
say(` base ${bold(base.label)} → head ${bold(headLabel)}\n`);
|
|
10910
|
+
const marks = {
|
|
10911
|
+
regressed: red("✗"),
|
|
10912
|
+
fixed: green("✓"),
|
|
10913
|
+
"not-comparable": yellow("◌")
|
|
10914
|
+
};
|
|
10915
|
+
for (const { change, labels } of lines.units) say(` ${marks[change]} ${`${labels.length} ${change.replace("-", " ")}`.padEnd(18)} ${labels.join(", ")}`);
|
|
10916
|
+
say(` ${dim(lines.counts)}`);
|
|
10917
|
+
const ignored = comparison.ignoredRuns.base + comparison.ignoredRuns.head;
|
|
10918
|
+
if (ignored) say(` ${dim(`${plural(ignored, "older run")} of the same scenarios ignored`)}`);
|
|
10919
|
+
say("");
|
|
10920
|
+
for (const line of lines.suites) say(` ${line}`);
|
|
10921
|
+
if (lines.datasets) say(` ${lines.datasets}`);
|
|
10922
|
+
say("");
|
|
10923
|
+
for (const failure of failures) say(` ${red("✗")} ${failure}`);
|
|
10924
|
+
if (options.markdown) say(` ${dim(`Markdown: ${options.markdown}`)}`);
|
|
10925
|
+
if (failures.length || options.markdown) say("");
|
|
10926
|
+
}
|
|
10927
|
+
if (failures.length) process.exitCode = 1;
|
|
10928
|
+
}
|
|
9908
10929
|
const cli = run(make$1("evalkit").pipe(withSubcommands([
|
|
9909
10930
|
newCommand,
|
|
9910
10931
|
runCommand,
|
|
9911
10932
|
devCommand,
|
|
9912
10933
|
buildCommand,
|
|
9913
10934
|
tidyCommand,
|
|
9914
|
-
shareCommand
|
|
10935
|
+
shareCommand,
|
|
10936
|
+
compareCommand
|
|
9915
10937
|
])), {
|
|
9916
10938
|
name: "EvalKit",
|
|
9917
10939
|
version: VERSION
|
|
@@ -9920,6 +10942,7 @@ const argv = process.argv.slice();
|
|
|
9920
10942
|
if (argv[2] === "help") argv.splice(2, 1, "--help");
|
|
9921
10943
|
if (argv.at(-2) === "--" && argv.at(-1) === "--help") argv.splice(-2, 1);
|
|
9922
10944
|
try {
|
|
10945
|
+
assertSupportedRuntime();
|
|
9923
10946
|
await Effect.runPromise(cli(argv).pipe(Effect.provide(layer)));
|
|
9924
10947
|
} catch (error) {
|
|
9925
10948
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -9927,5 +10950,3 @@ try {
|
|
|
9927
10950
|
}
|
|
9928
10951
|
//#endregion
|
|
9929
10952
|
export {};
|
|
9930
|
-
|
|
9931
|
-
//# sourceMappingURL=cli.mjs.map
|