@leostera/evalkit 0.9.0 → 0.11.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 +114 -1
- package/README.md +3 -2
- package/dist/cli.mjs +311 -92
- package/dist/dashboard/assets/index-CFJzcw3o.css +2 -0
- package/dist/dashboard/assets/{index-By1SJ1on.js → index-DgoX2x_M.js} +34 -31
- package/dist/dashboard/index.html +2 -2
- package/dist/{index-db-BbVblaHz.mjs → index-db-DI7xKMSX.mjs} +237 -47
- package/dist/index.d.mts +1141 -220
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +241 -52
- package/dist/runner.d.mts +782 -225
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +1 -1
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +1 -1
- package/dist/share.d.mts +9 -6
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +2 -2
- package/dist/skills/evalkit/SKILL.md +3 -3
- package/dist/skills/evalkit-agents/SKILL.md +3 -3
- package/dist/skills/evalkit-datasets/SKILL.md +1 -1
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +4 -4
- package/dist/skills/evalkit-results/SKILL.md +2 -2
- package/dist/{src-Bn6fnZnF.mjs → src-CXzbHAKN.mjs} +1 -1
- package/dist/{src-BrfIXfO2.mjs → src-D6Ax2t9b.mjs} +636 -220
- package/dist/src-Dc3GXacB.mjs +3637 -0
- package/dist/upgrade-rMsGzUEJ.mjs +230 -0
- package/dist/upgrade-transcript-C8DUykLd.mjs +402 -0
- package/package.json +2 -1
- package/dist/dashboard/assets/index-geXMXCx9.css +0 -2
- package/dist/src-CcdLZqFk.mjs +0 -1714
- package/dist/upgrade-60SIRius.mjs +0 -110
package/dist/cli.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
':' //; exec "${EVALKIT_RUNTIME:-$(command -v bun || command -v node)}" "$0" "$@"
|
|
3
|
-
import {
|
|
4
|
-
import { a as readRunEvents, c as
|
|
5
|
-
import { f as encodeBundle, i as normalizeBasePath, l as dashboardIndex, s as repoKey } from "./index-db-
|
|
6
|
-
import { t as createDirectoryShareApp } from "./src-
|
|
3
|
+
import { Et as resolveLimits, Q as canonicalParameters, St as formatLimit, Tt as parseLimits, Z as defineEvalMatrix, _ as recordError, i as agentRuntimes, l as defineSuite, lt as passMark, o as authoringId, v as registerEvals, wt as parseLimitFlag } from "./src-D6Ax2t9b.mjs";
|
|
4
|
+
import { _ as removeRunContainer, a as readRunEvents, c as readRunSubagents, d as localReportStore, f as runWorkspacePath, g as fixtureSet, h as globMatcher, i as ownerIsGone, l as readRunSummary, m as staticDatasetFiles, n as upgradeReports, o as readRunManifest, p as staticDatasetFile, r as runMatrix, t as runEval, u as readRunTranscript } from "./src-Dc3GXacB.mjs";
|
|
5
|
+
import { f as encodeBundle, i as normalizeBasePath, l as dashboardIndex, s as repoKey } from "./index-db-DI7xKMSX.mjs";
|
|
6
|
+
import { t as createDirectoryShareApp } from "./src-CXzbHAKN.mjs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { Effect, LogLevel, Logger } from "effect";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
8
10
|
import { cp, lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
-
import path, { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
11
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12
|
+
import childProcess, { execFile, spawn } from "node:child_process";
|
|
10
13
|
import fs, { existsSync } from "node:fs";
|
|
11
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
15
|
import { Hono } from "hono";
|
|
13
|
-
import childProcess, { execFile, spawn } from "node:child_process";
|
|
14
16
|
import { promisify, stripVTControlCharacters } from "node:util";
|
|
15
17
|
import { createServer } from "node:http";
|
|
16
18
|
import { EventEmitter } from "node:events";
|
|
@@ -166,6 +168,19 @@ function shortSource(source) {
|
|
|
166
168
|
//#endregion
|
|
167
169
|
//#region ../cli/src/dashboard-reports.ts
|
|
168
170
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
171
|
+
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
|
|
172
|
+
const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f]/g;
|
|
173
|
+
/**
|
|
174
|
+
* The first non-empty line of a message, without terminal colours, cut to
|
|
175
|
+
* `max` characters: a scorer may explain itself with 300 lines of stderr,
|
|
176
|
+
* and a summary line must stay one line. The run page has the rest.
|
|
177
|
+
*/
|
|
178
|
+
function firstLine(text, max = 100) {
|
|
179
|
+
const lines = (text ?? "").replace(ANSI, "").split(/\r?\n/).map((line) => line.replace(CONTROL, "").trim()).filter(Boolean);
|
|
180
|
+
const line = lines[0] ?? "";
|
|
181
|
+
const more = lines.length > 1 ? " …" : "";
|
|
182
|
+
return line.length > max ? `${line.slice(0, max - 1)}…` : `${line}${more}`;
|
|
183
|
+
}
|
|
169
184
|
/** Written into a run directory by `evalkit tidy`; deleting it restores the run. */
|
|
170
185
|
const ARCHIVE_MARKER = "archived.json";
|
|
171
186
|
function isMissing(error) {
|
|
@@ -226,7 +241,7 @@ function createDashboardReportReader(root) {
|
|
|
226
241
|
} catch {
|
|
227
242
|
return false;
|
|
228
243
|
}
|
|
229
|
-
return typeof version === "number" && version <
|
|
244
|
+
return typeof version === "number" && version < 7;
|
|
230
245
|
}
|
|
231
246
|
async function readRun(id) {
|
|
232
247
|
if (await isLegacyRun(id)) {
|
|
@@ -256,6 +271,9 @@ function createDashboardReportReader(root) {
|
|
|
256
271
|
manifest.aut.version
|
|
257
272
|
].filter(Boolean).join(" / ") } : {},
|
|
258
273
|
status: summary ? summary.status : interrupted ? "cancelled" : "running",
|
|
274
|
+
...manifest.limits ? { limits: manifest.limits } : {},
|
|
275
|
+
...manifest.isolation ? { isolation: manifest.isolation } : {},
|
|
276
|
+
...summary?.limit ? { limit: summary.limit } : {},
|
|
259
277
|
...interrupted ? { interrupted: true } : {},
|
|
260
278
|
startedAt: manifest.startedAt,
|
|
261
279
|
...summary?.endedAt ? { completedAt: summary.endedAt } : {},
|
|
@@ -266,7 +284,12 @@ function createDashboardReportReader(root) {
|
|
|
266
284
|
name: result.name,
|
|
267
285
|
kind: result.kind,
|
|
268
286
|
...result.skipped ? { skipped: true } : {},
|
|
269
|
-
...result.value === void 0 || result.skipped ? {} : { value: result.value }
|
|
287
|
+
...result.value === void 0 || result.skipped ? {} : { value: result.value },
|
|
288
|
+
...result.passed === void 0 || result.skipped ? {} : { passed: result.passed },
|
|
289
|
+
...result.error ? { errored: true } : {},
|
|
290
|
+
...result.choice ? { choice: result.choice } : {},
|
|
291
|
+
...result.explanation ? { explanation: firstLine(result.explanation, 200) } : {},
|
|
292
|
+
...result.judge?.model ? { model: result.judge.model } : {}
|
|
270
293
|
})) } : {},
|
|
271
294
|
...scoring?.metrics?.length ? { metrics: scoring.metrics.map((metric) => ({
|
|
272
295
|
name: metric.name,
|
|
@@ -312,9 +335,19 @@ function createDashboardReportReader(root) {
|
|
|
312
335
|
}
|
|
313
336
|
const candidateFile = (runId, path) => artifactFile(runId, "candidate", path);
|
|
314
337
|
const attachmentFile = (runId, name) => artifactFile(runId, "attachments", name);
|
|
315
|
-
|
|
338
|
+
/** The agent's transcript and EvalKit's events, as far as a running run has got. */
|
|
339
|
+
async function transcript(runId) {
|
|
316
340
|
runPath(root, runId);
|
|
317
|
-
|
|
341
|
+
const [recorded, subagents, runEvents] = await Promise.all([
|
|
342
|
+
ifPresent(() => readRunTranscript(root, runId)),
|
|
343
|
+
readRunSubagents(root, runId),
|
|
344
|
+
ifPresent(() => readRunEvents(root, runId))
|
|
345
|
+
]);
|
|
346
|
+
return {
|
|
347
|
+
transcript: recorded ?? null,
|
|
348
|
+
subagents,
|
|
349
|
+
runEvents: runEvents ?? []
|
|
350
|
+
};
|
|
318
351
|
}
|
|
319
352
|
return {
|
|
320
353
|
listRuns,
|
|
@@ -325,7 +358,7 @@ function createDashboardReportReader(root) {
|
|
|
325
358
|
listCandidateWorkspace,
|
|
326
359
|
candidateFile,
|
|
327
360
|
attachmentFile,
|
|
328
|
-
|
|
361
|
+
transcript
|
|
329
362
|
};
|
|
330
363
|
}
|
|
331
364
|
/** Both artifact endpoints walk a specific root; never broaden the candidate-file read root. */
|
|
@@ -456,13 +489,22 @@ function staticRoutes(ids) {
|
|
|
456
489
|
...under("library/scenarios", ids.scenarios),
|
|
457
490
|
...under("library/agents", ids.agents),
|
|
458
491
|
...under("library/datasets", ids.datasets),
|
|
459
|
-
...ids.runs.filter(safe).flatMap((id) => [
|
|
460
|
-
`runs/${id}`,
|
|
461
|
-
`runs/${id}/workspace`,
|
|
462
|
-
`runs/${id}/attachments`
|
|
463
|
-
])
|
|
492
|
+
...ids.runs.filter(safe).flatMap((id) => [`runs/${id}`, ...RUN_SECTIONS.map((section) => `runs/${id}/${section}`)])
|
|
464
493
|
];
|
|
465
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* A run's sections, each its own page; `attachments` is the logs' URL from
|
|
497
|
+
* before 0.10, kept so old links redirect.
|
|
498
|
+
*/
|
|
499
|
+
const RUN_SECTIONS = [
|
|
500
|
+
"judging",
|
|
501
|
+
"metrics",
|
|
502
|
+
"workspace",
|
|
503
|
+
"logs",
|
|
504
|
+
"datasets",
|
|
505
|
+
"raw",
|
|
506
|
+
"attachments"
|
|
507
|
+
];
|
|
466
508
|
/** `runs/abc.html` is served at `<mount>/runs/abc`; its base is one level up. */
|
|
467
509
|
function baseFor(route) {
|
|
468
510
|
const depth = route.split("/").length - 1;
|
|
@@ -493,7 +535,7 @@ function directorySink(root) {
|
|
|
493
535
|
}
|
|
494
536
|
/**
|
|
495
537
|
* One run's snapshot, shared by `evalkit build` (files on disk) and
|
|
496
|
-
* `evalkit share` (an upload bundle): `detail.json`, `
|
|
538
|
+
* `evalkit share` (an upload bundle): `detail.json`, `transcript.json`,
|
|
497
539
|
* `artifacts.json` and `workspace.json`, the candidate files under
|
|
498
540
|
* `workspace/`, and attachments under `attachments/` (both left out with
|
|
499
541
|
* `workspaceFiles: false`; they stay listed). Evaluator workspaces are never
|
|
@@ -502,7 +544,7 @@ function directorySink(root) {
|
|
|
502
544
|
async function snapshotRun(reports, runId, sink, options = {}) {
|
|
503
545
|
const detail = await reports.runDetail(runId);
|
|
504
546
|
await sink.json("detail.json", detail);
|
|
505
|
-
await sink.json("
|
|
547
|
+
await sink.json("transcript.json", await reports.transcript(runId));
|
|
506
548
|
await sink.json("artifacts.json", { artifacts: await reports.listArtifacts(runId) });
|
|
507
549
|
const workspace = await reports.listCandidateWorkspace(runId);
|
|
508
550
|
await sink.json("workspace.json", workspace);
|
|
@@ -843,6 +885,7 @@ async function loadConfig(cwd, configPath) {
|
|
|
843
885
|
const config = file ? (await importProjectModule(file)).default : {};
|
|
844
886
|
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Config must default-export defineConfig({...})");
|
|
845
887
|
if (config.execution && "trials" in config.execution) throw new Error("execution.trials was renamed to execution.repeat in EvalKit 0.9");
|
|
888
|
+
if (config.judge !== void 0 && typeof config.judge?.start !== "function") throw new Error("judge in the config must be an agent made with defineAgent()");
|
|
846
889
|
for (const [key, value] of Object.entries(config.execution ?? {})) if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new Error(`execution.${key} must be a positive integer`);
|
|
847
890
|
return {
|
|
848
891
|
root,
|
|
@@ -880,6 +923,17 @@ async function loadProject(cwd, configPath) {
|
|
|
880
923
|
matrices: [...registry.matrices, matrix]
|
|
881
924
|
};
|
|
882
925
|
}
|
|
926
|
+
const judge = config.judge;
|
|
927
|
+
if (judge) {
|
|
928
|
+
const catalog = registry.catalog;
|
|
929
|
+
registry = {
|
|
930
|
+
...registry,
|
|
931
|
+
catalog: (options) => catalog({
|
|
932
|
+
judge,
|
|
933
|
+
...options
|
|
934
|
+
})
|
|
935
|
+
};
|
|
936
|
+
}
|
|
883
937
|
return {
|
|
884
938
|
root,
|
|
885
939
|
config,
|
|
@@ -887,6 +941,34 @@ async function loadProject(cwd, configPath) {
|
|
|
887
941
|
loadErrors
|
|
888
942
|
};
|
|
889
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* Where a project's run workspaces live. By default outside the project, in
|
|
946
|
+
* the system temp directory (one directory per project, so `tidy` and `dev`
|
|
947
|
+
* find the ones it kept): an agent that looks around its workspace finds no
|
|
948
|
+
* project source, no `AGENTS.md` or skills above it, and no results.
|
|
949
|
+
* `sandboxDir` overrides it; `inside` says when that puts them back in the
|
|
950
|
+
* project, which `run` warns about.
|
|
951
|
+
*/
|
|
952
|
+
function sandboxLocation(project) {
|
|
953
|
+
if (project.config.sandboxDir === void 0) return {
|
|
954
|
+
path: defaultSandboxDir(project.root),
|
|
955
|
+
inside: false
|
|
956
|
+
};
|
|
957
|
+
const path = resolve(project.root, project.config.sandboxDir);
|
|
958
|
+
const fromRoot = relative(project.root, path);
|
|
959
|
+
return {
|
|
960
|
+
path,
|
|
961
|
+
inside: !fromRoot.startsWith("..") && !isAbsolute(fromRoot)
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/** `<tmp>/evalkit/<project name>-<hash of its path>`: stable per checkout. */
|
|
965
|
+
function defaultSandboxDir(root) {
|
|
966
|
+
const hash = createHash("sha256").update(root).digest("hex").slice(0, 10);
|
|
967
|
+
const name = basename(root).replace(/[^A-Za-z0-9._-]/g, "-") || "project";
|
|
968
|
+
return join(tmpdir(), "evalkit", `${name}-${hash}`);
|
|
969
|
+
}
|
|
970
|
+
/** The warning `run` prints when workspaces sit inside the project. */
|
|
971
|
+
const SANDBOX_INSIDE_WARNING = "sandboxDir is inside the project: agents can read its source, its AGENTS.md and skills, its results and other runs’ workspaces from there. Leave sandboxDir unset to keep workspaces in the system temp directory.";
|
|
890
972
|
//#endregion
|
|
891
973
|
//#region ../cli/src/terminal.ts
|
|
892
974
|
/** Human CLI output. Colors and live progress only appear on an interactive terminal. */
|
|
@@ -910,8 +992,8 @@ function bytes(size) {
|
|
|
910
992
|
if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} kB`;
|
|
911
993
|
return `${(size / 1024 ** 2).toFixed(1)} MB`;
|
|
912
994
|
}
|
|
913
|
-
function plural(count, noun) {
|
|
914
|
-
return `${count} ${
|
|
995
|
+
function plural(count, noun, nouns = `${noun}s`) {
|
|
996
|
+
return `${count} ${count === 1 ? noun : nouns}`;
|
|
915
997
|
}
|
|
916
998
|
/** Times one build step, showing a live line while it runs. */
|
|
917
999
|
async function step(label, work, describe) {
|
|
@@ -1009,30 +1091,50 @@ async function confirm(question) {
|
|
|
1009
1091
|
//#region ../cli/src/run-reporter.ts
|
|
1010
1092
|
function runOutcome(result) {
|
|
1011
1093
|
if (result.status === "cancelled") return "cancelled";
|
|
1094
|
+
if (result.status === "limited") return "limited";
|
|
1012
1095
|
return result.status === "passed" ? "passed" : "failed";
|
|
1013
1096
|
}
|
|
1014
|
-
function passAtFor(definition, name) {
|
|
1015
|
-
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "predicate" && step.name === name);
|
|
1016
|
-
return rule?.kind === "predicate" ? rule.passAt : void 0;
|
|
1017
|
-
}
|
|
1018
1097
|
function value(score) {
|
|
1019
1098
|
return score.value === void 0 ? "" : score.value.toFixed(1);
|
|
1020
1099
|
}
|
|
1100
|
+
/**
|
|
1101
|
+
* One failed verdict in words: `history 0.0 (passes at 1): got 2 entries`,
|
|
1102
|
+
* or for a choices judge `Correctness C (passes at A or B): wrong product`.
|
|
1103
|
+
*/
|
|
1104
|
+
function verdictReason(definition, verdict) {
|
|
1105
|
+
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "judge" && step.name === verdict.name);
|
|
1106
|
+
const answer = rule?.kind === "judge" ? rule.answer : void 0;
|
|
1107
|
+
const mark = verdict.passAt === void 0 ? "" : answer?.kind === "choices" ? ` (passes at ${passMark(verdict.passAt, answer)})` : ` (passes at ${verdict.passAt})`;
|
|
1108
|
+
const why = [criteriaReason(verdict.criteria), firstLine(verdict.explanation)].filter(Boolean).join(" · ");
|
|
1109
|
+
return `${verdict.name} ${verdict.choice ?? value(verdict)}${mark}${why ? `: ${why}` : ""}`;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Which criteria let the score down, so seven checks the app never reached
|
|
1113
|
+
* read as one cause (`not reached: saves, history`), not seven failures.
|
|
1114
|
+
*/
|
|
1115
|
+
function criteriaReason(criteria) {
|
|
1116
|
+
if (!criteria?.length) return "";
|
|
1117
|
+
const failed = criteria.filter((criterion) => "value" in criterion && criterion.value < 1);
|
|
1118
|
+
const unreached = criteria.filter((criterion) => !("value" in criterion));
|
|
1119
|
+
return [failed.length ? `failed ${failed.map((criterion) => {
|
|
1120
|
+
const why = firstLine(criterion.explanation, 60);
|
|
1121
|
+
return why ? `${criterion.name} (${why})` : criterion.name;
|
|
1122
|
+
}).join(", ")}` : "", unreached.length ? `not reached ${unreached.map((criterion) => criterion.name).join(", ")}` : ""].filter(Boolean).join(" · ");
|
|
1123
|
+
}
|
|
1021
1124
|
function scoreReason(definition, score) {
|
|
1022
|
-
if (score.error) return `${score.name} error: ${score.error.message}`;
|
|
1023
|
-
|
|
1024
|
-
return `${score.name} ${value(score)}${passAt === void 0 ? "" : ` (passes at ${passAt})`}`;
|
|
1125
|
+
if (score.error) return `${score.name} error: ${firstLine(score.error.message)}`;
|
|
1126
|
+
return verdictReason(definition, score);
|
|
1025
1127
|
}
|
|
1026
1128
|
function checkpointReason(definition, checkpoint) {
|
|
1027
|
-
if (checkpoint.status === "error") return `step ${checkpoint.step} ${checkpoint.name} error: ${checkpoint.error?.message
|
|
1028
|
-
|
|
1029
|
-
return `step ${checkpoint.step} ${
|
|
1129
|
+
if (checkpoint.status === "error") return `step ${checkpoint.step} ${checkpoint.name} error: ${firstLine(checkpoint.error?.message)}`;
|
|
1130
|
+
if (checkpoint.value === void 0 && !checkpoint.choice) return `step ${checkpoint.step} ${checkpoint.name} ${checkpoint.status}`;
|
|
1131
|
+
return `step ${checkpoint.step} ${verdictReason(definition, checkpoint)}`;
|
|
1030
1132
|
}
|
|
1031
1133
|
/** What went wrong in a run. */
|
|
1032
1134
|
function reasonsFor(definition, run) {
|
|
1033
1135
|
const reasons = /* @__PURE__ */ new Set();
|
|
1034
1136
|
const skipped = /* @__PURE__ */ new Set();
|
|
1035
|
-
if (run.status === "errored" && run.error) reasons.add(`error: ${run.error.message}`);
|
|
1137
|
+
if (run.status === "errored" && run.error) reasons.add(`error: ${firstLine(run.error.message, 160)}`);
|
|
1036
1138
|
for (const checkpoint of run.scoring?.checkpoints ?? []) if (checkpoint.status === "failed" || checkpoint.status === "error") reasons.add(checkpointReason(definition, checkpoint));
|
|
1037
1139
|
for (const score of run.scoring?.results ?? []) if (score.skipped) skipped.add(score.name);
|
|
1038
1140
|
else if (!score.passed) reasons.add(scoreReason(definition, score));
|
|
@@ -1060,17 +1162,26 @@ function parameterLabel(parameters) {
|
|
|
1060
1162
|
}
|
|
1061
1163
|
/** One printed line for a finished run; failing reasons on the same line. */
|
|
1062
1164
|
function runLine(scenario) {
|
|
1063
|
-
const mark = scenario.outcome === "passed" ? green("✓") : scenario.outcome === "cancelled" ? yellow("◌") : red("✗");
|
|
1165
|
+
const mark = scenario.outcome === "passed" ? green("✓") : scenario.outcome === "cancelled" ? yellow("◌") : scenario.outcome === "limited" ? yellow("✗") : red("✗");
|
|
1064
1166
|
const parameters = parameterLabel(scenario.parameters);
|
|
1065
1167
|
const batch = scenario.result.batch;
|
|
1066
1168
|
const position = batch ? dim(` #${batch.index + 1}/${batch.size}`) : "";
|
|
1067
1169
|
const details = [
|
|
1068
|
-
...scenario.outcome === "
|
|
1170
|
+
...scenario.outcome === "limited" ? [limitReason(scenario.result)] : [],
|
|
1171
|
+
...scenario.outcome === "failed" || scenario.outcome === "limited" ? scenario.reasons : [],
|
|
1069
1172
|
...scenario.outcome === "cancelled" ? ["cancelled"] : [],
|
|
1070
1173
|
...scenario.skipped.map((name) => `${name} skipped`)
|
|
1071
1174
|
];
|
|
1072
1175
|
return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${position}${details.length ? ` ${dim(details.join(" · "))}` : ""}`;
|
|
1073
1176
|
}
|
|
1177
|
+
/** `limited: 41 of 40 steps · score 0.40 (graded what it left)`. */
|
|
1178
|
+
function limitReason(result) {
|
|
1179
|
+
const limit = result.limit;
|
|
1180
|
+
if (!limit) return "limited";
|
|
1181
|
+
const used = limit.limit === "time" ? `${formatLimit("time", Math.round(limit.used / 1e3) * 1e3)} of ${formatLimit("time", limit.value)}` : `${limit.used.toLocaleString("en-US")} of ${formatLimit(limit.limit, limit.value)}`;
|
|
1182
|
+
const overall = result.scoring?.overall;
|
|
1183
|
+
return `limited: ${used}${overall === void 0 || !result.scoring?.results.length ? "" : ` · score ${overall.toFixed(2)} (graded what it left)`}`;
|
|
1184
|
+
}
|
|
1074
1185
|
/**
|
|
1075
1186
|
* Prints run progress. On a terminal, failures print as they happen and a live
|
|
1076
1187
|
* line shows counts and what is running. Elsewhere (CI), every finished
|
|
@@ -1115,8 +1226,15 @@ function createRunReporter(options) {
|
|
|
1115
1226
|
timer.unref?.();
|
|
1116
1227
|
}
|
|
1117
1228
|
},
|
|
1118
|
-
|
|
1119
|
-
if (
|
|
1229
|
+
onUpdate(run, update) {
|
|
1230
|
+
if (update.kind === "record") {
|
|
1231
|
+
const activity = running.get(run.runId);
|
|
1232
|
+
const call = update.record.type === "message" && update.record.role === "assistant" ? update.record.content.filter((block) => block.type === "tool_call").at(-1) : void 0;
|
|
1233
|
+
if (activity && call?.type === "tool_call") activity.doing = `tool ${call.name}`;
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const event = update.event;
|
|
1237
|
+
if (event.kind === "run-started") {
|
|
1120
1238
|
running.set(run.runId, {
|
|
1121
1239
|
label: options.labelFor(run),
|
|
1122
1240
|
startedAt: now()
|
|
@@ -1125,10 +1243,10 @@ function createRunReporter(options) {
|
|
|
1125
1243
|
}
|
|
1126
1244
|
const activity = running.get(run.runId);
|
|
1127
1245
|
if (!activity) return;
|
|
1128
|
-
if (event.
|
|
1129
|
-
else if (event.
|
|
1130
|
-
else if (event.
|
|
1131
|
-
else if (event.
|
|
1246
|
+
if (event.kind === "run-completed" || event.kind === "error") running.delete(run.runId);
|
|
1247
|
+
else if (event.kind === "limit-reached") activity.doing = `stopping at its ${event.limit} limit`;
|
|
1248
|
+
else if (event.kind === "scorer-started") activity.doing = `scoring ${event.scorer}`;
|
|
1249
|
+
else if (event.kind === "checkpoint-started") activity.doing = `check ${event.name}`;
|
|
1132
1250
|
},
|
|
1133
1251
|
onRun(scenario) {
|
|
1134
1252
|
scenarios.push(scenario);
|
|
@@ -1145,6 +1263,7 @@ function createRunReporter(options) {
|
|
|
1145
1263
|
return {
|
|
1146
1264
|
passed: scenarios.filter((s) => s.outcome === "passed").length,
|
|
1147
1265
|
failed: scenarios.filter((s) => s.outcome === "failed").length,
|
|
1266
|
+
limited: scenarios.filter((s) => s.outcome === "limited").length,
|
|
1148
1267
|
cancelled: scenarios.filter((s) => s.outcome === "cancelled").length,
|
|
1149
1268
|
runs: scenarios.length,
|
|
1150
1269
|
scenarios: new Set(scenarios.map((s) => `${s.label} ${parameterLabel(s.parameters)}`)).size,
|
|
@@ -1158,6 +1277,7 @@ function totalsLine(totals) {
|
|
|
1158
1277
|
const parts = [
|
|
1159
1278
|
green(`${totals.passed} passed`),
|
|
1160
1279
|
totals.failed ? red(`${totals.failed} failed`) : "",
|
|
1280
|
+
totals.limited ? yellow(`${totals.limited} limited`) : "",
|
|
1161
1281
|
totals.cancelled ? yellow(`${totals.cancelled} cancelled`) : ""
|
|
1162
1282
|
].filter(Boolean);
|
|
1163
1283
|
const coverage = totals.runs === totals.scenarios ? plural(totals.runs, "run") : `${plural(totals.runs, "run")} of ${plural(totals.scenarios, "scenario")}`;
|
|
@@ -1181,13 +1301,17 @@ function junitReport(scenarios) {
|
|
|
1181
1301
|
bySuite.set(suite, [...bySuite.get(suite) ?? [], scenario]);
|
|
1182
1302
|
}
|
|
1183
1303
|
const suites = [...bySuite].map(([suite, cases]) => {
|
|
1184
|
-
const failures = cases.filter((c) => c.outcome === "failed").length;
|
|
1304
|
+
const failures = cases.filter((c) => c.outcome === "failed" || c.outcome === "limited").length;
|
|
1185
1305
|
const skipped = cases.filter((c) => c.outcome === "cancelled" || c.outcome === "passed" && c.skipped.length > 0 && allSkipped(c)).length;
|
|
1186
1306
|
const time = cases.reduce((sum, c) => sum + (c.durationMs ?? 0), 0);
|
|
1187
1307
|
const testcases = cases.map((scenario) => {
|
|
1188
1308
|
const parameters = parameterLabel(scenario.parameters);
|
|
1189
1309
|
const batch = scenario.result.batch;
|
|
1190
1310
|
const attributes = `name="${escape((parameters ? `${scenario.evalId} [${parameters}]` : scenario.evalId) + (batch ? ` #${batch.index + 1}` : ""))}" classname="${escape(suite)}" time="${seconds(scenario.durationMs)}"`;
|
|
1311
|
+
if (scenario.outcome === "limited") {
|
|
1312
|
+
const reasons = [limitReason(scenario.result), ...scenario.reasons];
|
|
1313
|
+
return ` <testcase ${attributes}>\n <failure message="${escape(reasons.join("; "))}" type="limited">${escape(reasons.join("\n"))}</failure>\n </testcase>`;
|
|
1314
|
+
}
|
|
1191
1315
|
if (scenario.outcome === "failed") return ` <testcase ${attributes}>\n <failure message="${escape(scenario.reasons.join("; ") || "failed")}">${escape(scenario.reasons.join("\n"))}</failure>\n </testcase>`;
|
|
1192
1316
|
if (scenario.outcome === "cancelled") return ` <testcase ${attributes}>\n <skipped message="cancelled"/>\n </testcase>`;
|
|
1193
1317
|
if (allSkipped(scenario)) return ` <testcase ${attributes}>\n <skipped message="${escape(`all scorers skipped: ${scenario.skipped.join(", ")}`)}"/>\n </testcase>`;
|
|
@@ -1195,7 +1319,7 @@ function junitReport(scenarios) {
|
|
|
1195
1319
|
});
|
|
1196
1320
|
return ` <testsuite name="${escape(suite)}" tests="${cases.length}" failures="${failures}" errors="0" skipped="${skipped}" time="${seconds(time)}">\n${testcases.join("\n")}\n </testsuite>`;
|
|
1197
1321
|
});
|
|
1198
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites name="evalkit" tests="${scenarios.length}" failures="${scenarios.filter((c) => c.outcome === "failed").length}">\n${suites.join("\n")}\n</testsuites>\n`;
|
|
1322
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites name="evalkit" tests="${scenarios.length}" failures="${scenarios.filter((c) => c.outcome === "failed" || c.outcome === "limited").length}">\n${suites.join("\n")}\n</testsuites>\n`;
|
|
1199
1323
|
}
|
|
1200
1324
|
/** True when every score was skip()ped. */
|
|
1201
1325
|
function allSkipped(scenario) {
|
|
@@ -1203,9 +1327,71 @@ function allSkipped(scenario) {
|
|
|
1203
1327
|
return results.length > 0 && results.every((score) => score.skipped);
|
|
1204
1328
|
}
|
|
1205
1329
|
//#endregion
|
|
1330
|
+
//#region ../cli/src/run-options.ts
|
|
1331
|
+
/**
|
|
1332
|
+
* What every run of the project shares, from its config, wherever it is
|
|
1333
|
+
* launched: the CLI's `run` and the dashboard's buttons.
|
|
1334
|
+
*/
|
|
1335
|
+
function projectRunOptions(root, config) {
|
|
1336
|
+
return {
|
|
1337
|
+
root,
|
|
1338
|
+
...config?.isolation ? { isolation: config.isolation } : {},
|
|
1339
|
+
limits: { base: parseLimits(config?.limits, "The config") },
|
|
1340
|
+
...config?.keepSandboxes ? { keepWorkspaces: config.keepSandboxes } : {},
|
|
1341
|
+
...config?.artifacts?.exclude ? { artifactExclude: config.artifacts.exclude } : {},
|
|
1342
|
+
...config?.artifacts?.attachments ? { attachmentLimits: config.artifacts.attachments } : {},
|
|
1343
|
+
...config?.judge ? { judge: config.judge } : {}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* The project's fixtures for one invocation, or undefined when it has
|
|
1348
|
+
* none. The caller stops them when its runs are done.
|
|
1349
|
+
*/
|
|
1350
|
+
function projectFixtures(root, config) {
|
|
1351
|
+
const fixtures = config?.fixtures;
|
|
1352
|
+
if (!fixtures || !Object.keys(fixtures).length) return void 0;
|
|
1353
|
+
return fixtureSet(fixtures, { root });
|
|
1354
|
+
}
|
|
1355
|
+
/** Runs `work` with the project's fixtures, stopping them afterwards. */
|
|
1356
|
+
async function withFixtures(root, config, work) {
|
|
1357
|
+
const fixtures = projectFixtures(root, config);
|
|
1358
|
+
try {
|
|
1359
|
+
return await work(fixtures);
|
|
1360
|
+
} finally {
|
|
1361
|
+
await fixtures?.stop();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
//#endregion
|
|
1365
|
+
//#region ../cli/src/package-manager.ts
|
|
1366
|
+
const LOCKFILES = [
|
|
1367
|
+
["bun.lock", "bun"],
|
|
1368
|
+
["bun.lockb", "bun"],
|
|
1369
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1370
|
+
["yarn.lock", "yarn"],
|
|
1371
|
+
["package-lock.json", "npm"]
|
|
1372
|
+
];
|
|
1373
|
+
/**
|
|
1374
|
+
* The package manager a project uses: the one that started this process
|
|
1375
|
+
* (`npm_config_user_agent`), else the nearest lockfile at or above `root`
|
|
1376
|
+
* (a workspace keeps it at its root), else bun. Suggesting another manager's
|
|
1377
|
+
* command can make someone run its install and rewrite `node_modules`.
|
|
1378
|
+
*/
|
|
1379
|
+
function detectPackageManager(root, userAgent = process.env.npm_config_user_agent) {
|
|
1380
|
+
const started = userAgent?.split("/")[0];
|
|
1381
|
+
if (started === "bun" || started === "pnpm" || started === "yarn" || started === "npm") return started;
|
|
1382
|
+
for (let dir = root;; dir = dirname(dir)) {
|
|
1383
|
+
for (const [file, manager] of LOCKFILES) if (existsSync(join(dir, file))) return manager;
|
|
1384
|
+
if (dirname(dir) === dir) return "bun";
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/** `pnpm run evalkit dev` (or `npm run evalkit -- dev`: npm needs the `--` for flags). */
|
|
1388
|
+
function evalkitCommand(manager, args) {
|
|
1389
|
+
return manager === "npm" ? `npm run evalkit -- ${args}` : `${manager} run evalkit ${args}`;
|
|
1390
|
+
}
|
|
1391
|
+
//#endregion
|
|
1206
1392
|
//#region ../cli/src/version.ts
|
|
1207
1393
|
/** The CLI version; kept in sync with package.json when releasing. */
|
|
1208
|
-
const VERSION = "0.
|
|
1394
|
+
const VERSION = "0.11.0";
|
|
1209
1395
|
//#endregion
|
|
1210
1396
|
//#region ../cli/src/run-command.ts
|
|
1211
1397
|
/** Validates `run` flags into a selection, parameter overrides and limits. */
|
|
@@ -1218,14 +1404,7 @@ function normalizeRunOptions(values, positionals) {
|
|
|
1218
1404
|
if (!Number.isSafeInteger(number) || number < 1) throw new Error(`--${name} must be a positive integer`);
|
|
1219
1405
|
return number;
|
|
1220
1406
|
}
|
|
1221
|
-
|
|
1222
|
-
["max-tokens", "maxTokens"],
|
|
1223
|
-
["chat-timeout-ms", "chatTimeoutMs"],
|
|
1224
|
-
["turn-budget", "turnBudget"]
|
|
1225
|
-
]) {
|
|
1226
|
-
const value = integer(flag);
|
|
1227
|
-
if (value !== void 0) parameters[key] = value;
|
|
1228
|
-
}
|
|
1407
|
+
const limits = resolveLimits(...(values.limit ?? []).flatMap((value) => value.split(",")).map(parseLimitFlag));
|
|
1229
1408
|
const selection = {};
|
|
1230
1409
|
for (const key of ["model", "mode"]) if (values[key]?.length) selection[key] = values[key].flatMap((value) => value.split(","));
|
|
1231
1410
|
for (const value of values.select ?? []) {
|
|
@@ -1251,6 +1430,7 @@ function normalizeRunOptions(values, positionals) {
|
|
|
1251
1430
|
positionals,
|
|
1252
1431
|
parameters,
|
|
1253
1432
|
selection,
|
|
1433
|
+
limits,
|
|
1254
1434
|
concurrency: integer("concurrency"),
|
|
1255
1435
|
repeat: integer("repeat")
|
|
1256
1436
|
};
|
|
@@ -1323,6 +1503,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1323
1503
|
const cells = matrix.count(selection);
|
|
1324
1504
|
if (!cells) throw new Error("Selection contains no eligible cells; check cases and exclusions");
|
|
1325
1505
|
const repeat = parsed.repeat ?? project.config.execution?.repeat;
|
|
1506
|
+
const configLimits = parseLimits(project.config.limits, "The config");
|
|
1326
1507
|
let runs = 0;
|
|
1327
1508
|
for (const cell of matrix.cells(selection)) runs += repeat ?? cell.eval.repeat ?? 1;
|
|
1328
1509
|
const plan = {
|
|
@@ -1332,7 +1513,14 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1332
1513
|
runs,
|
|
1333
1514
|
parameters: selection.parameters,
|
|
1334
1515
|
overrides: selection.overrides,
|
|
1335
|
-
repeat: repeat ?? "scenario"
|
|
1516
|
+
repeat: repeat ?? "scenario",
|
|
1517
|
+
limits: {
|
|
1518
|
+
config: configLimits,
|
|
1519
|
+
cli: parsed.limits
|
|
1520
|
+
},
|
|
1521
|
+
isolation: project.config.isolation ?? { kind: "none" },
|
|
1522
|
+
fixtures: Object.keys(project.config.fixtures ?? {}),
|
|
1523
|
+
scoring: Object.fromEntries(registry.catalog({ root: project.root }).filter((entry) => matrix.evals.some((definition) => definition.id === entry.id)).map((entry) => [entry.id, plannedChecks(entry)]))
|
|
1336
1524
|
};
|
|
1337
1525
|
if (values["dry-run"]) {
|
|
1338
1526
|
console.log(JSON.stringify(plan, null, 2));
|
|
@@ -1341,7 +1529,9 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1341
1529
|
const sweeps = Object.keys(matrix.parameters).length > 0 || Boolean(matrix.cases?.length);
|
|
1342
1530
|
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.`);
|
|
1343
1531
|
const reportDir = resolve(project.root, project.config.reportDir ?? "_evalkit-results");
|
|
1344
|
-
const
|
|
1532
|
+
const sandbox = sandboxLocation(project);
|
|
1533
|
+
const sandboxDir = sandbox.path;
|
|
1534
|
+
if (sandbox.inside) console.error(` ${yellow("!")} ${SANDBOX_INSIDE_WARNING}`);
|
|
1345
1535
|
const source = await detectSource(project.root, { outputDirs: [
|
|
1346
1536
|
reportDir,
|
|
1347
1537
|
sandboxDir,
|
|
@@ -1371,7 +1561,9 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1371
1561
|
reporter.start();
|
|
1372
1562
|
let summary;
|
|
1373
1563
|
try {
|
|
1374
|
-
summary = await Effect.runPromise(runnerLogs(runMatrix(matrix, {
|
|
1564
|
+
summary = await withFixtures(project.root, project.config, (fixtures) => Effect.runPromise(runnerLogs(runMatrix(matrix, {
|
|
1565
|
+
...projectRunOptions(project.root, project.config),
|
|
1566
|
+
...fixtures ? { fixtures } : {},
|
|
1375
1567
|
selection,
|
|
1376
1568
|
concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
|
|
1377
1569
|
...repeat ? { repeat } : {},
|
|
@@ -1382,11 +1574,12 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1382
1574
|
catalog: runCatalog(registry, project.root),
|
|
1383
1575
|
report: localReportStore(reportDir),
|
|
1384
1576
|
workspaceRoot: sandboxDir,
|
|
1385
|
-
keepWorkspaces: project.config.keepSandboxes,
|
|
1386
|
-
artifactExclude: project.config.artifacts?.exclude,
|
|
1387
|
-
attachmentLimits: project.config.artifacts?.attachments,
|
|
1388
1577
|
signal: controller.signal,
|
|
1389
|
-
|
|
1578
|
+
limits: {
|
|
1579
|
+
base: configLimits,
|
|
1580
|
+
override: parsed.limits
|
|
1581
|
+
},
|
|
1582
|
+
onUpdate: (run, update) => reporter.onUpdate(run, update),
|
|
1390
1583
|
onResult: (cell, results) => {
|
|
1391
1584
|
for (const result of results) {
|
|
1392
1585
|
const report = runReport(cell, result, suiteFor(authoringId(cell.eval)));
|
|
@@ -1402,7 +1595,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1402
1595
|
reporter.onRun(report);
|
|
1403
1596
|
}
|
|
1404
1597
|
}
|
|
1405
|
-
})));
|
|
1598
|
+
}))));
|
|
1406
1599
|
} finally {
|
|
1407
1600
|
process.off("SIGINT", onInterrupt);
|
|
1408
1601
|
}
|
|
@@ -1416,12 +1609,31 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1416
1609
|
else {
|
|
1417
1610
|
console.log(`\n${totalsLine(totals)}`);
|
|
1418
1611
|
if (values.junit) console.log(` ${dim(`JUnit report: ${values.junit}`)}`);
|
|
1419
|
-
|
|
1420
|
-
|
|
1612
|
+
const manager = detectPackageManager(project.root);
|
|
1613
|
+
const hint = (args, why) => console.log(` ${cyan("➜")} ${evalkitCommand(manager, args).padEnd(24)} ${dim(why)}`);
|
|
1614
|
+
hint("dev", "to inspect");
|
|
1615
|
+
if (project.config.share) hint("share", "to publish");
|
|
1421
1616
|
console.log("");
|
|
1422
1617
|
}
|
|
1423
1618
|
if (controller.signal.aborted) process.exitCode = 130;
|
|
1424
|
-
else if (summary.failed || broken.length) process.exitCode = 1;
|
|
1619
|
+
else if (summary.failed || summary.limited || broken.length) process.exitCode = 1;
|
|
1620
|
+
}
|
|
1621
|
+
/** A scenario's checkpoints and final scorers, as `--dry-run` lists them. */
|
|
1622
|
+
function plannedChecks(entry) {
|
|
1623
|
+
return [...entry.transcript.flatMap((step, index) => step.kind === "checkpoint" ? [{
|
|
1624
|
+
scorer: step.scorer,
|
|
1625
|
+
at: `step ${index}`
|
|
1626
|
+
}] : []), ...entry.scorers.map((scorer) => ({
|
|
1627
|
+
scorer,
|
|
1628
|
+
at: "final"
|
|
1629
|
+
}))].map(({ scorer, at }) => ({
|
|
1630
|
+
name: scorer.name,
|
|
1631
|
+
kind: scorer.kind,
|
|
1632
|
+
at,
|
|
1633
|
+
...scorer.description ? { description: scorer.description } : {},
|
|
1634
|
+
passes: passMark(scorer.passAt, scorer.answer),
|
|
1635
|
+
...scorer.agent ? { agent: scorer.agent.name ?? scorer.agent.id ?? scorer.agent.kind } : {}
|
|
1636
|
+
}));
|
|
1425
1637
|
}
|
|
1426
1638
|
//#endregion
|
|
1427
1639
|
//#region ../cli/src/tidy.ts
|
|
@@ -1473,8 +1685,8 @@ async function tidyReports(options) {
|
|
|
1473
1685
|
return result;
|
|
1474
1686
|
}
|
|
1475
1687
|
/**
|
|
1476
|
-
* Remove the run workspaces `keep` would not have kept
|
|
1477
|
-
* by default. Unfinished runs are never touched, so a run in progress keeps
|
|
1688
|
+
* Remove the run workspaces `keep` would not have kept, and their kept
|
|
1689
|
+
* containers: those of passed runs by default. Unfinished runs are never touched, so a run in progress keeps
|
|
1478
1690
|
* its workspace; workspaces without a report are left alone.
|
|
1479
1691
|
*/
|
|
1480
1692
|
async function tidySandboxes(options) {
|
|
@@ -1491,6 +1703,7 @@ async function tidySandboxes(options) {
|
|
|
1491
1703
|
recursive: true,
|
|
1492
1704
|
force: true
|
|
1493
1705
|
});
|
|
1706
|
+
if (run.isolation?.kind === "container") await removeRunContainer(run.id, options.runtime);
|
|
1494
1707
|
removed += 1;
|
|
1495
1708
|
}
|
|
1496
1709
|
return { removed };
|
|
@@ -1569,7 +1782,7 @@ async function runBundle(options) {
|
|
|
1569
1782
|
files.push({
|
|
1570
1783
|
path: "run.json",
|
|
1571
1784
|
data: encode({
|
|
1572
|
-
snapshotVersion:
|
|
1785
|
+
snapshotVersion: 3,
|
|
1573
1786
|
...options.run
|
|
1574
1787
|
})
|
|
1575
1788
|
});
|
|
@@ -1764,7 +1977,7 @@ export const greetingAgent = defineAgent({
|
|
|
1764
1977
|
identity: { id: 'greeting-agent', kind: 'in-process' },
|
|
1765
1978
|
async start({ context }) {
|
|
1766
1979
|
return {
|
|
1767
|
-
// Return the reply; EvalKit records it.
|
|
1980
|
+
// Return the reply; EvalKit records it. Use the transcript writer in start() to record tool calls and usage.
|
|
1768
1981
|
async send(message: string) {
|
|
1769
1982
|
const file = join(context.workspace.root, 'greeting', 'greeting.txt');
|
|
1770
1983
|
const prefix = (await readFile(file, 'utf8')).trim();
|
|
@@ -1831,11 +2044,10 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
|
|
|
1831
2044
|
}
|
|
1832
2045
|
//#endregion
|
|
1833
2046
|
//#region ../cli/src/new-project.ts
|
|
1834
|
-
const npmDependency = "^0.
|
|
2047
|
+
const npmDependency = "^0.11.0";
|
|
1835
2048
|
const ignored = [
|
|
1836
2049
|
"node_modules/",
|
|
1837
2050
|
"_evalkit-results/",
|
|
1838
|
-
"_evalkit-sandbox/",
|
|
1839
2051
|
"_evalkit-dashboard/",
|
|
1840
2052
|
"_evalkit-share/"
|
|
1841
2053
|
];
|
|
@@ -1947,9 +2159,11 @@ async function writeStarterReadme(root, name, inPlace) {
|
|
|
1947
2159
|
}
|
|
1948
2160
|
//#endregion
|
|
1949
2161
|
//#region ../cli/src/compare.ts
|
|
2162
|
+
/** Runs that ended on their own; a limited run is a finished failure. */
|
|
1950
2163
|
const FINISHED = /* @__PURE__ */ new Set([
|
|
1951
2164
|
"passed",
|
|
1952
2165
|
"failed",
|
|
2166
|
+
"limited",
|
|
1953
2167
|
"errored"
|
|
1954
2168
|
]);
|
|
1955
2169
|
function unitKey(run) {
|
|
@@ -5256,12 +5470,13 @@ function createProgram(actions) {
|
|
|
5256
5470
|
showGlobalOptions: false
|
|
5257
5471
|
}).addHelpText("after", `\nRun from your eval project's directory. Docs: https://evalkit.leostera.dev/docs/\n`);
|
|
5258
5472
|
program.command("new").description("create an eval project (never overwrites files)").argument("<directory>", "where to create it; use . for the current directory").addHelpText("after", examples(["bunx @leostera/evalkit new ./agent-evals", "evalkit new ."])).action((directory) => actions.create(directory));
|
|
5259
|
-
program.command("run").description("run scenarios and save the results in _evalkit-results/").argument("[eval-ids...]", "scenarios to run (default: all)").addOption(configOption()).option("-e, --eval <id>", "run this scenario; repeatable, or comma-separated", collect).option("-s, --suite <id>", "run the scenarios in this suite; with -m, only those in both").option("-m, --matrix <id>", "run this matrix").option("--model <value>", "--select model=<value> for each comma-separated value", collect).option("--mode <value>", "--select mode=<value> for each comma-separated value", collect).option("-a, --select <axis=value>", "keep only these values of a matrix axis (a for axis); repeatable, comma-separated", collect).option("--param <name=json>", "set a parameter for every scenario; repeatable", collect).option("-n, --repeat <n>", "runs of each scenario, as one batch; overrides config and scenario").option("-j, --concurrency <n>", "matrix cells to run at once (default: 4)").option("--
|
|
5473
|
+
program.command("run").description("run scenarios and save the results in _evalkit-results/").argument("[eval-ids...]", "scenarios to run (default: all)").addOption(configOption()).option("-e, --eval <id>", "run this scenario; repeatable, or comma-separated", collect).option("-s, --suite <id>", "run the scenarios in this suite; with -m, only those in both").option("-m, --matrix <id>", "run this matrix").option("--model <value>", "--select model=<value> for each comma-separated value", collect).option("--mode <value>", "--select mode=<value> for each comma-separated value", collect).option("-a, --select <axis=value>", "keep only these values of a matrix axis (a for axis); repeatable, comma-separated; one value per axis runs one cell", collect).option("--param <name=json>", "set a parameter for every scenario; repeatable", collect).option("-n, --repeat <n>", "runs of each scenario, as one batch; overrides config and scenario").option("-j, --concurrency <n>", "matrix cells to run at once (default: 4)").option("--limit <kind=value>", "stop each run past this limit: steps=80, tokens=2000000, time=20m; repeatable; overrides config, scenario and matrix", collect).option("--local", "require the agent's local runtime").option("--all", "allow more matrix cells than execution.maxCells").option("--dry-run", "print the plan as JSON; run nothing").option("--json", "print one JSON result per matrix cell").option("--junit <file>", "write a JUnit XML report").option("--skip-broken", "run healthy scenarios when some files fail to load, then exit 1").option("-v, --verbose", "print every scenario, not only failures").addOption(logLevelOption()).addHelpText("after", examples([
|
|
5260
5474
|
"evalkit run",
|
|
5261
5475
|
"evalkit run greeting -n 5",
|
|
5262
5476
|
"evalkit run -s billing --junit _evalkit-results/junit.xml",
|
|
5263
5477
|
"evalkit run -m models --model model-a,model-b --dry-run",
|
|
5264
|
-
"evalkit run -m models -s testing -a model=luna -a toolchain=riot"
|
|
5478
|
+
"evalkit run -m models -s testing -a model=luna -a toolchain=riot",
|
|
5479
|
+
"evalkit run todo-app -a model=luna -a skills=none -a docs=search"
|
|
5265
5480
|
])).action((evalIds, options) => {
|
|
5266
5481
|
setRunnerLogLevel(options.logLevel);
|
|
5267
5482
|
return actions.run(runInput(evalIds, options));
|
|
@@ -5300,9 +5515,7 @@ function runInput(evalIds, flags) {
|
|
|
5300
5515
|
mode: flags.mode,
|
|
5301
5516
|
select: flags.select,
|
|
5302
5517
|
param: flags.param,
|
|
5303
|
-
|
|
5304
|
-
"chat-timeout-ms": flags.chatTimeoutMs,
|
|
5305
|
-
"turn-budget": flags.turnBudget,
|
|
5518
|
+
limit: flags.limit,
|
|
5306
5519
|
concurrency: flags.concurrency,
|
|
5307
5520
|
repeat: flags.repeat,
|
|
5308
5521
|
json: flags.json,
|
|
@@ -5318,7 +5531,7 @@ function runInput(evalIds, flags) {
|
|
|
5318
5531
|
//#region ../cli/src/index.ts
|
|
5319
5532
|
let projectRoot = process.cwd();
|
|
5320
5533
|
let reportRoot = resolve(projectRoot, "_evalkit-results");
|
|
5321
|
-
let sandboxRoot =
|
|
5534
|
+
let sandboxRoot = defaultSandboxDir(projectRoot);
|
|
5322
5535
|
let project;
|
|
5323
5536
|
async function loadRegistry() {
|
|
5324
5537
|
project ??= await loadProject(projectRoot);
|
|
@@ -5336,21 +5549,20 @@ async function runEvals(options) {
|
|
|
5336
5549
|
const evaluations = options.evalIds.map((id) => registry.get(id));
|
|
5337
5550
|
if (evaluations.some((e) => !e)) throw new Error("Unknown eval");
|
|
5338
5551
|
const provenance = await runProvenance(registry);
|
|
5339
|
-
await Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
|
|
5552
|
+
await withFixtures(projectRoot, project?.config, (fixtures) => Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
|
|
5340
5553
|
const name = evaluation.name ?? authoringId(evaluation);
|
|
5341
5554
|
const started = performance.now();
|
|
5342
5555
|
return Effect.sync(() => runLog.started(name, {}, options.suiteId)).pipe(Effect.andThen(runEval(evaluation, {
|
|
5556
|
+
...projectRunOptions(projectRoot, project?.config),
|
|
5557
|
+
...fixtures ? { fixtures } : {},
|
|
5343
5558
|
report: localReportStore(reportRoot),
|
|
5344
5559
|
workspaceRoot: sandboxRoot,
|
|
5345
|
-
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5346
|
-
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5347
|
-
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5348
5560
|
suiteId: options.suiteId,
|
|
5349
5561
|
runtime: runtimeFor(evaluation),
|
|
5350
5562
|
concurrency: options.concurrency,
|
|
5351
5563
|
...provenance
|
|
5352
5564
|
})), Effect.tap((result) => runLog.finished(name, {}, result, performance.now() - started)), Effect.tapError((error) => Effect.sync(() => runLog.failed(name, error))));
|
|
5353
|
-
}), { concurrency: options.concurrency })));
|
|
5565
|
+
}), { concurrency: options.concurrency }))));
|
|
5354
5566
|
}
|
|
5355
5567
|
/** Detected per launch: `evalkit dev` keeps running while commits change. */
|
|
5356
5568
|
async function runProvenance(registry) {
|
|
@@ -5408,19 +5620,18 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, repeat) {
|
|
|
5408
5620
|
const name = evaluation.name ?? authoringId(evaluation);
|
|
5409
5621
|
const started = performance.now();
|
|
5410
5622
|
runLog.started(name, body.parameters ?? {}, suiteId);
|
|
5411
|
-
runProvenance(registry).then((provenance) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
|
|
5623
|
+
runProvenance(registry).then((provenance) => withFixtures(projectRoot, project?.config, (fixtures) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
|
|
5624
|
+
...projectRunOptions(projectRoot, project?.config),
|
|
5625
|
+
...fixtures ? { fixtures } : {},
|
|
5412
5626
|
selection,
|
|
5413
5627
|
concurrency: 1,
|
|
5414
5628
|
...repeat ? { repeat } : {},
|
|
5415
5629
|
...suiteId ? { suiteId } : {},
|
|
5416
5630
|
report: localReportStore(reportRoot),
|
|
5417
5631
|
workspaceRoot: sandboxRoot,
|
|
5418
|
-
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5419
|
-
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5420
|
-
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5421
5632
|
...provenance,
|
|
5422
5633
|
onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
|
|
5423
|
-
})))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
5634
|
+
}))))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
5424
5635
|
matrixRunning = false;
|
|
5425
5636
|
});
|
|
5426
5637
|
} else {
|
|
@@ -5515,9 +5726,9 @@ function registerReportRoutes(app, registry, reports) {
|
|
|
5515
5726
|
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5516
5727
|
}
|
|
5517
5728
|
});
|
|
5518
|
-
app.get("/v1/runs/:runId/
|
|
5729
|
+
app.get("/v1/runs/:runId/transcript", async (context) => {
|
|
5519
5730
|
try {
|
|
5520
|
-
return context.json(
|
|
5731
|
+
return context.json(await reports.transcript(context.req.param("runId")));
|
|
5521
5732
|
} catch (error) {
|
|
5522
5733
|
return context.text(error instanceof Error ? error.message : "Not found", 404);
|
|
5523
5734
|
}
|
|
@@ -5550,7 +5761,7 @@ async function useProject(configPath) {
|
|
|
5550
5761
|
project = await loadProject(projectRoot, configPath);
|
|
5551
5762
|
projectRoot = project.root;
|
|
5552
5763
|
reportRoot = resolve(projectRoot, project.config.reportDir ?? "_evalkit-results");
|
|
5553
|
-
sandboxRoot =
|
|
5764
|
+
sandboxRoot = sandboxLocation(project).path;
|
|
5554
5765
|
}
|
|
5555
5766
|
/**
|
|
5556
5767
|
* `--port` is honoured exactly; without it the server takes the first free
|
|
@@ -5671,7 +5882,8 @@ async function tidyAction(options) {
|
|
|
5671
5882
|
registry: project.registry,
|
|
5672
5883
|
reportRoot,
|
|
5673
5884
|
sandboxRoot,
|
|
5674
|
-
keep: project.config.keepSandboxes
|
|
5885
|
+
keep: project.config.keepSandboxes,
|
|
5886
|
+
...project.config.isolation?.kind === "container" ? { runtime: project.config.isolation.runtime } : {}
|
|
5675
5887
|
}), (value) => {
|
|
5676
5888
|
const keep = project.config.keepSandboxes ?? "failed";
|
|
5677
5889
|
if (keep === "all") return "all kept (keepSandboxes: 'all')";
|
|
@@ -5690,14 +5902,21 @@ async function upgradeAction(options) {
|
|
|
5690
5902
|
banner("upgrade", VERSION);
|
|
5691
5903
|
const { root, config } = await loadConfig(projectRoot, options.config);
|
|
5692
5904
|
reportRoot = resolve(root, config.reportDir ?? "_evalkit-results");
|
|
5693
|
-
const report = await step(options.dryRun ? "Checking results" : "Upgrading results", () => upgradeReports(reportRoot, { dryRun: options.dryRun }), (value) => `${plural(value.
|
|
5905
|
+
const report = await step(options.dryRun ? "Checking results" : "Upgrading results", () => upgradeReports(reportRoot, { dryRun: options.dryRun }), (value) => `${plural(value.transcripts, "run")} ${options.dryRun ? "to upgrade" : "upgraded"}`);
|
|
5694
5906
|
console.log("");
|
|
5695
|
-
if (!report.upgraded) console.log(` ${cyan("✓")} Every run is already in the current format.`);
|
|
5696
|
-
|
|
5697
|
-
const into = `${plural(report.upgraded, "run")}
|
|
5698
|
-
console.log(options.dryRun ? ` ${cyan("◆")} Would upgrade ${into}
|
|
5907
|
+
if (!report.upgraded && !report.transcripts && !report.failed.length) console.log(` ${cyan("✓")} Every run is already in the current format.`);
|
|
5908
|
+
if (report.upgraded) {
|
|
5909
|
+
const into = `${plural(report.upgraded, "run")} with trials into ${plural(report.runs, "run")}`;
|
|
5910
|
+
console.log(options.dryRun ? ` ${cyan("◆")} Would upgrade ${into}.` : ` ${cyan("✓")} Upgraded ${into}.`);
|
|
5699
5911
|
if (report.batches) console.log(` ${dim(`${plural(report.batches, "run")} had more than one trial; each trial is now a run in a batch.`)}`);
|
|
5700
5912
|
}
|
|
5913
|
+
if (report.transcripts) console.log(options.dryRun ? ` ${cyan("◆")} Would convert ${plural(report.transcripts, "trajectory", "trajectories")} to ATXF transcripts.` : ` ${cyan("✓")} Converted ${plural(report.transcripts, "trajectory", "trajectories")} to ATXF transcripts.`);
|
|
5914
|
+
if (options.dryRun && (report.upgraded || report.transcripts)) console.log(` ${dim("Nothing was changed.")}`);
|
|
5915
|
+
if (report.failed.length) {
|
|
5916
|
+
console.log(` ${red("✗")} Left ${plural(report.failed.length, "run")} as ${report.failed.length === 1 ? "it was" : "they were"}; ${report.failed.length === 1 ? "it" : "they"} could not be converted:`);
|
|
5917
|
+
for (const { runId, reason } of report.failed) console.log(` ${runId} ${dim(reason)}`);
|
|
5918
|
+
process.exitCode = 1;
|
|
5919
|
+
}
|
|
5701
5920
|
if (report.running.length) console.log(` ${yellow("◆")} Left ${plural(report.running.length, "run")} still running alone; upgrade again when they finish.`);
|
|
5702
5921
|
console.log("");
|
|
5703
5922
|
}
|