@leostera/evalkit 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -1
- package/README.md +3 -2
- package/dist/cli.mjs +139 -29
- package/dist/dashboard/assets/{index-By1SJ1on.js → index-DNR4shrM.js} +32 -31
- package/dist/dashboard/assets/index-TPYprg75.css +2 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/{index-db-BbVblaHz.mjs → index-db-B1FeXzmS.mjs} +1 -1
- package/dist/index.d.mts +313 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +222 -6
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +1 -1
- package/dist/share-worker.mjs +1 -1
- package/dist/share.mjs +2 -2
- package/dist/skills/evalkit/SKILL.md +2 -2
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +3 -3
- package/dist/skills/evalkit-results/SKILL.md +1 -1
- package/dist/{src-BrfIXfO2.mjs → src-B14fXAGp.mjs} +264 -21
- package/dist/{src-Bn6fnZnF.mjs → src-Bccv9igQ.mjs} +1 -1
- package/dist/{src-CcdLZqFk.mjs → src-BfSQKOJP.mjs} +260 -97
- package/dist/{upgrade-60SIRius.mjs → upgrade-G-uFPJKg.mjs} +1 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-geXMXCx9.css +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@leostera/evalkit`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/). Before 1.0, a minor version may include breaking changes; they are marked **Breaking**.
|
|
4
4
|
|
|
5
|
+
## [0.10.0] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
You can see what a judge checks before a run and why it passed or failed after. A judge is now a rubric plus what it sees and how it answers: EvalKit builds the prompt, enforces the answer, and records the prompt, reply, model and cost, which a matrix axis can vary. Run pages put why a run failed beside what it did, a scorer can report several checks from one exercise, and run workspaces move out of the project.
|
|
8
|
+
|
|
9
|
+
### Breaking: judges name their agent and EvalKit builds the prompt
|
|
10
|
+
|
|
11
|
+
- **`judge()` takes `agent`, `inputs`, `answer` and `passAt`.** A scenario's `judge:` field is gone. Each judge uses its own `agent`, or `judge` in `evalkit.config.ts` for every judge that names none. `inputs` (`{ name: (ctx) => value }`, default `conversation` and `reply`) says what the judge sees, and `answer` how it answers: `choices({ A: [label, value], ... })`, `criteria({ name: weight })`, or by default a value from 0 to 1. EvalKit writes the prompt from these and the rubric, reads the reply, and asks once more if it cannot. The judge agent receives the prompt as its message and replies in plain text; it no longer has to return a JSON verdict, except for the default answer.
|
|
12
|
+
- **Judges pass at `passAt`** (default only 1), as predicates do. A judge that returned `passed` itself is no longer trusted with the pass mark.
|
|
13
|
+
- **The judge agent gets the run's `parameters`**, so a matrix axis such as `judgeModel` chooses its model. It should return `{ reply, usage, model }`; the model is recorded with each verdict.
|
|
14
|
+
- `EvalCatalogEntry.judgeAgent` is gone; each catalog scorer has its own `agent`, and judges add `inputs`, `answer` and `prompt` (the template).
|
|
15
|
+
|
|
16
|
+
### Breaking: run workspaces live outside the project
|
|
17
|
+
|
|
18
|
+
Run workspaces used to live in `_evalkit-sandbox/` inside the project, so an agent that looked around found the project's source, its `AGENTS.md` and skills, its results, and other runs' workspaces, and some read them unprompted. They now default to the system temp directory, `$TMPDIR/evalkit/<project>-<hash>/<run>/{candidate,evaluator}`, one directory per checkout so `tidy`, `dev` and the Workspaces page still find the ones a run kept. Setting `sandboxDir` inside the project still works, and `run` warns. This removes what an agent finds by accident; it is not confinement. Runs of a project still share a parent directory, and the evaluator workspace sits beside the candidate one, so an agent that goes looking can reach them. Confine the agent in its adapter, with a container or an OS sandbox.
|
|
19
|
+
|
|
20
|
+
### Migrating from 0.9
|
|
21
|
+
|
|
22
|
+
1. Move the scenario's `judge: myJudge` to `judge: myJudge` in `evalkit.config.ts`, or to `agent: myJudge` on each `judge()` rule. A scenario that still sets it fails with a message saying where it went.
|
|
23
|
+
2. If your judge agent parsed EvalKit's old JSON prompt, make it send the message it receives to its model as it is, and return the model's reply. Return `model` and `usage` too.
|
|
24
|
+
3. If a judge returned partial values and relied on its own `passed`, set `passAt` to the lowest value that should pass.
|
|
25
|
+
4. If a script reads `_evalkit-sandbox/` by path, read the dashboard's Workspaces page or the run's saved artifacts instead, or set `sandboxDir: '_evalkit-sandbox'` to keep the old place (and its warning). Delete the old `_evalkit-sandbox/` and its Git ignore line.
|
|
26
|
+
5. Several predicates sharing one cached exercise can become one predicate that returns `criteria`, declared with `predicate(name, fn, { criteria })`; see below.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Descriptions.** `predicate(name, fn, { description })` and `judge(name, { description })` say what a scorer checks, in the dashboard, `--dry-run` (which now lists each scenario's scorers, pass marks and judge agents) and the Judges page.
|
|
31
|
+
- **Criteria: one exercise, several checks.** A predicate can return `criteria` instead of `value`, one per check: `{ name, value, weight?, explanation?, evidence? }`. The score is their weighted mean (weight 1 by default), and the run page shows the formula. `criteria()` judges answer the same way.
|
|
32
|
+
- `predicate(name, fn, { criteria: { 'cannot push': 'the share token gets 403' } })` declares the checks, so they show before a run.
|
|
33
|
+
- A declared check that the result leaves out, or one returned as `{ name, reached: false }`, is **not reached**: it counts as 0 and shows as "not reached", with the score's explanation as the one root cause. A check that wasn't declared is an error.
|
|
34
|
+
- Each criterion's evidence opens under it on the run page.
|
|
35
|
+
- **A run page that answers why, in one window.** A rail lists what the run ran (status, scenario, agent, each parameter, commit, time, agent and judge tokens) above a menu of its sections: **Trajectory & scoring**, **Judging**, **Metrics**, **Workspace files**, **Logs**, **Datasets** and **Raw report**, each with its own URL (`/runs/:id/metrics`); `[` and `]` step through them. The first section puts what the agent did beside every verdict, failures first and open, under "Why it failed", "How it scored" or "Why it errored" with "2 of 3 checks did not pass"; on a narrower window they become tabs, opening on the verdicts when the run did not pass. The page is never taller than the window: each column scrolls on its own. Clicking a checkpoint in the trajectory points at its verdict, and `?verdict=` links to one.
|
|
36
|
+
- **Workspace files and logs, named for what they hold.** The run page calls the snapshot of what the agent left in its workspace **Workspace files** (one browser; the separate artifacts table is gone), and the files saved on purpose with `attach()` **Logs**, at `/runs/:id/logs`; `/runs/:id/attachments` redirects there. The API keeps `attach()` and `attachments`.
|
|
37
|
+
- **No title bar.** The bar at the top of every page, which repeated the section name, is gone; the breadcrumb says where you are. A shared page's version picker moved beside the "Shared results for…" line.
|
|
38
|
+
- **Verdicts.** Each scorer and checkpoint opens (failed ones open by default) to the verdict in words ("answered C (0), passes at A or B → failed"), the explanation, criteria, and for a judge the agent, model, tokens, time and retry, then tabs with the prompt it saw, its reply, its conversation and the evidence as a tree or JSON.
|
|
39
|
+
- **Judging cost apart from the agent's.** Results save the judge's prompt, reply, conversation, usage, model and the `passAt` applied. A **Judging** section on the run page totals judge tokens and time by judge, and the built-in `judge tokens` metric puts it on the scoreboard and in Compare.
|
|
40
|
+
- **Judges page.** The list shows each judge's description, pass mark, latest model and last 10 runs. A judge's page shows its agent, what it sees, how it answers, the choice or criteria table, the rubric, the prompt EvalKit sends (as a template or as a recent run sent it), and recent verdicts with their reasons.
|
|
41
|
+
- **"How it is scored"** on a scenario's page: every scorer, what it checks, its pass mark and how it did in the last 10 runs. Checkpoints in the transcript show their description and pass mark.
|
|
42
|
+
- **Failure lines say why, in one line.** The CLI's failure line and JUnit messages give each failing scorer's value, pass mark and choice, which criteria failed or weren't reached, and the first line of its explanation, without terminal colours. A 300-line stderr in an explanation or error no longer floods the terminal.
|
|
43
|
+
- **Attachments next to the verdict.** A score or checkpoint records the files its scorer saved with `attach()` (`attachments`), even when the scorer then failed. Its verdict links them, and a failed one also links the run's other attachments, such as the agent's transcript.
|
|
44
|
+
- **Graders that read external state**, documented: reset before you exercise, or compare before and after, so the agent's own test leftovers don't decide a check.
|
|
45
|
+
- [`examples/judges`](https://github.com/leostera/evalkit/tree/main/examples/judges): choices and criteria judges on a chat assistant, one predicate with declared checks (failed, and not reached) on a built service, and a judge model chosen by the matrix, all provider-free.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- A run's score on its page is rounded to two places.
|
|
50
|
+
|
|
5
51
|
## [0.9.0] - 2026-11-02
|
|
6
52
|
|
|
7
53
|
A run is one execution now: `repeat` starts several as a batch, and `evalkit upgrade` converts older results. Also attachments, typed checkpoint lookups, Markdown replies, one definition of `tokens`, and a share server that can live below a path on your own site and show a single repository.
|
|
@@ -235,7 +281,8 @@ Commit provenance and team sharing ([RFD0006](docs/rfds/RFD0006-commit-provenanc
|
|
|
235
281
|
- **Breaking:** the npm package moved to `@leostera/evalkit`. Replace the `@leostera-js/evalkit` dependency and update imports, including `/runner`. Eval definitions and local reports are unchanged.
|
|
236
282
|
- Dashboard workflow refinements.
|
|
237
283
|
|
|
238
|
-
[0.
|
|
284
|
+
[0.10.0]: https://github.com/leostera/evalkit/compare/f06b26a...main
|
|
285
|
+
[0.9.0]: https://github.com/leostera/evalkit/compare/2c62b85...f06b26a
|
|
239
286
|
[0.8.0]: https://github.com/leostera/evalkit/compare/7e8c3e5...2c62b85
|
|
240
287
|
[0.7.0]: https://github.com/leostera/evalkit/compare/1ca784e...7e8c3e5
|
|
241
288
|
[0.6.1]: https://github.com/leostera/evalkit/compare/91c2c86...1ca784e
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
EvalKit lets you write agent evaluations as TypeScript: give an Agent Under Test (AUT) a scenario, inspect what it actually said and did, and score the result. Write each scenario as a small file, share agents, judges and datasets between them, and keep them in Git alongside your code; run them locally and inspect the evidence in a dashboard or ordinary report files.
|
|
6
6
|
|
|
7
7
|
- **Test the journey, not just the final answer.** Put `predicate(...)`, `judge(...)`, or `expectToolCall(...)` between user turns to check an intermediate reply, observed tool call, or live file state before continuing. `policy.failfast` can skip later turns after a failed checkpoint.
|
|
8
|
-
- **Choose how to score.** Run deterministic predicates without model credentials, or
|
|
8
|
+
- **Choose how to score.** Run deterministic predicates without model credentials, or write a rubric and let a judge agent grade it: EvalKit builds the prompt, enforces the answer format (a value, `choices` or weighted `criteria`) and records the prompt, reply, model and judging cost apart from the agent's. A matrix axis can pick the judge's model. The same rule can run inline and in final scoring.
|
|
9
9
|
- **Make failures inspectable.** Reports record turns, checkpoints, skipped steps, final scores, and errors. Runs have isolated candidate and evaluator workspaces, so private expected answers need not be shown to the AUT.
|
|
10
10
|
- **Repeat and compare.** Run multiple runs or define a matrix of parameters. An adapter must explicitly apply those parameters to its AUT; EvalKit does not silently switch models.
|
|
11
11
|
- **Gate merge requests.** `--junit` gives CI a test report, and `evalkit compare` shows what regressed or improved against the base branch and can fail the pipeline.
|
|
@@ -42,13 +42,14 @@ The generator creates `scenarios/`, `agents/`, `datasets/`, and `judges/`, plus
|
|
|
42
42
|
|
|
43
43
|
In-place setup preserves existing dependencies, scripts, README, and scenario files; it refuses to overwrite any of the generated paths, including an existing config. The project installs `@leostera/evalkit` from npm. For more runnable examples, see [`examples/starter`](examples/starter/) and [`examples/interleaved-scenario`](examples/interleaved-scenario/).
|
|
44
44
|
|
|
45
|
-
Runs
|
|
45
|
+
Runs write reports to `_evalkit-results/` in the project, and run their agents in workspaces outside it (`$TMPDIR/evalkit/<project>-<hash>/`), so an agent cannot read the project's source, instructions or other runs by looking around. Scenario definitions can be versioned in Git; reports and workspaces remain local and are **not automatically shared**. Review them for sensitive data before sharing.
|
|
46
46
|
|
|
47
47
|
## Explore more
|
|
48
48
|
|
|
49
49
|
- [Get started](sites/evalkit.leostera.dev/src/content/docs/index.md) and the [manual](sites/evalkit.leostera.dev/src/content/docs/manual/) explain adapters, datasets, scoring, reports, and the CLI. Upgrading from 0.5? See the [migration guide](CHANGELOG.md#migrating-from-05).
|
|
50
50
|
- [Interleaved scenario](examples/interleaved-scenario/) has a passing write/revise flow, an intentional failfast failure, and a provider-free fake judge agent.
|
|
51
51
|
- [Configured matrix](examples/configured-matrix/) shows a runnable parameter sweep without model credentials.
|
|
52
|
+
- [Judges](examples/judges/) grades a chat assistant with A/B/C and weighted-criteria judges and checks a built service with evidence-returning predicates, a stronger judge model chosen by the matrix, all provider-free.
|
|
52
53
|
- [Support suite](examples/support-suite/) is a few dozen short scenarios in three suites, sharing one agent, its judges, and static, computed and evaluator-only datasets.
|
|
53
54
|
- [HACKING.md](HACKING.md) covers the repository layout, builds, and tests for contributors.
|
|
54
55
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
':' //; exec "${EVALKIT_RUNTIME:-$(command -v bun || command -v node)}" "$0" "$@"
|
|
3
|
-
import { H as canonicalParameters,
|
|
4
|
-
import { a as readRunEvents, c as readRunSummary, d as staticDatasetFile, f as staticDatasetFiles, i as ownerIsGone, l as localReportStore, n as upgradeReports, o as readRunManifest, p as globMatcher, r as runMatrix, t as runEval, u as runWorkspacePath } from "./src-
|
|
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 { H as defineEvalMatrix, U as canonicalParameters, h as registerEvals, i as agentRuntimes, l as defineSuite, m as recordError, o as authoringId, tt as passMark } from "./src-B14fXAGp.mjs";
|
|
4
|
+
import { a as readRunEvents, c as readRunSummary, d as staticDatasetFile, f as staticDatasetFiles, i as ownerIsGone, l as localReportStore, n as upgradeReports, o as readRunManifest, p as globMatcher, r as runMatrix, t as runEval, u as runWorkspacePath } from "./src-BfSQKOJP.mjs";
|
|
5
|
+
import { f as encodeBundle, i as normalizeBasePath, l as dashboardIndex, s as repoKey } from "./index-db-B1FeXzmS.mjs";
|
|
6
|
+
import { t as createDirectoryShareApp } from "./src-Bccv9igQ.mjs";
|
|
7
7
|
import { Effect, LogLevel, Logger } from "effect";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
8
9
|
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";
|
|
10
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
10
12
|
import fs, { existsSync } from "node:fs";
|
|
11
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
14
|
import { Hono } from "hono";
|
|
@@ -166,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) {
|
|
@@ -266,7 +281,12 @@ function createDashboardReportReader(root) {
|
|
|
266
281
|
name: result.name,
|
|
267
282
|
kind: result.kind,
|
|
268
283
|
...result.skipped ? { skipped: true } : {},
|
|
269
|
-
...result.value === void 0 || result.skipped ? {} : { value: result.value }
|
|
284
|
+
...result.value === void 0 || result.skipped ? {} : { value: result.value },
|
|
285
|
+
...result.passed === void 0 || result.skipped ? {} : { passed: result.passed },
|
|
286
|
+
...result.error ? { errored: true } : {},
|
|
287
|
+
...result.choice ? { choice: result.choice } : {},
|
|
288
|
+
...result.explanation ? { explanation: firstLine(result.explanation, 200) } : {},
|
|
289
|
+
...result.judge?.model ? { model: result.judge.model } : {}
|
|
270
290
|
})) } : {},
|
|
271
291
|
...scoring?.metrics?.length ? { metrics: scoring.metrics.map((metric) => ({
|
|
272
292
|
name: metric.name,
|
|
@@ -456,13 +476,22 @@ function staticRoutes(ids) {
|
|
|
456
476
|
...under("library/scenarios", ids.scenarios),
|
|
457
477
|
...under("library/agents", ids.agents),
|
|
458
478
|
...under("library/datasets", ids.datasets),
|
|
459
|
-
...ids.runs.filter(safe).flatMap((id) => [
|
|
460
|
-
`runs/${id}`,
|
|
461
|
-
`runs/${id}/workspace`,
|
|
462
|
-
`runs/${id}/attachments`
|
|
463
|
-
])
|
|
479
|
+
...ids.runs.filter(safe).flatMap((id) => [`runs/${id}`, ...RUN_SECTIONS.map((section) => `runs/${id}/${section}`)])
|
|
464
480
|
];
|
|
465
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* A run's sections, each its own page; `attachments` is the logs' URL from
|
|
484
|
+
* before 0.10, kept so old links redirect.
|
|
485
|
+
*/
|
|
486
|
+
const RUN_SECTIONS = [
|
|
487
|
+
"judging",
|
|
488
|
+
"metrics",
|
|
489
|
+
"workspace",
|
|
490
|
+
"logs",
|
|
491
|
+
"datasets",
|
|
492
|
+
"raw",
|
|
493
|
+
"attachments"
|
|
494
|
+
];
|
|
466
495
|
/** `runs/abc.html` is served at `<mount>/runs/abc`; its base is one level up. */
|
|
467
496
|
function baseFor(route) {
|
|
468
497
|
const depth = route.split("/").length - 1;
|
|
@@ -843,6 +872,7 @@ async function loadConfig(cwd, configPath) {
|
|
|
843
872
|
const config = file ? (await importProjectModule(file)).default : {};
|
|
844
873
|
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Config must default-export defineConfig({...})");
|
|
845
874
|
if (config.execution && "trials" in config.execution) throw new Error("execution.trials was renamed to execution.repeat in EvalKit 0.9");
|
|
875
|
+
if (config.judge !== void 0 && typeof config.judge?.start !== "function") throw new Error("judge in the config must be an agent made with defineAgent()");
|
|
846
876
|
for (const [key, value] of Object.entries(config.execution ?? {})) if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new Error(`execution.${key} must be a positive integer`);
|
|
847
877
|
return {
|
|
848
878
|
root,
|
|
@@ -880,6 +910,17 @@ async function loadProject(cwd, configPath) {
|
|
|
880
910
|
matrices: [...registry.matrices, matrix]
|
|
881
911
|
};
|
|
882
912
|
}
|
|
913
|
+
const judge = config.judge;
|
|
914
|
+
if (judge) {
|
|
915
|
+
const catalog = registry.catalog;
|
|
916
|
+
registry = {
|
|
917
|
+
...registry,
|
|
918
|
+
catalog: (options) => catalog({
|
|
919
|
+
judge,
|
|
920
|
+
...options
|
|
921
|
+
})
|
|
922
|
+
};
|
|
923
|
+
}
|
|
883
924
|
return {
|
|
884
925
|
root,
|
|
885
926
|
config,
|
|
@@ -887,6 +928,34 @@ async function loadProject(cwd, configPath) {
|
|
|
887
928
|
loadErrors
|
|
888
929
|
};
|
|
889
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* Where a project's run workspaces live. By default outside the project, in
|
|
933
|
+
* the system temp directory (one directory per project, so `tidy` and `dev`
|
|
934
|
+
* find the ones it kept): an agent that looks around its workspace finds no
|
|
935
|
+
* project source, no `AGENTS.md` or skills above it, and no results.
|
|
936
|
+
* `sandboxDir` overrides it; `inside` says when that puts them back in the
|
|
937
|
+
* project, which `run` warns about.
|
|
938
|
+
*/
|
|
939
|
+
function sandboxLocation(project) {
|
|
940
|
+
if (project.config.sandboxDir === void 0) return {
|
|
941
|
+
path: defaultSandboxDir(project.root),
|
|
942
|
+
inside: false
|
|
943
|
+
};
|
|
944
|
+
const path = resolve(project.root, project.config.sandboxDir);
|
|
945
|
+
const fromRoot = relative(project.root, path);
|
|
946
|
+
return {
|
|
947
|
+
path,
|
|
948
|
+
inside: !fromRoot.startsWith("..") && !isAbsolute(fromRoot)
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/** `<tmp>/evalkit/<project name>-<hash of its path>`: stable per checkout. */
|
|
952
|
+
function defaultSandboxDir(root) {
|
|
953
|
+
const hash = createHash("sha256").update(root).digest("hex").slice(0, 10);
|
|
954
|
+
const name = basename(root).replace(/[^A-Za-z0-9._-]/g, "-") || "project";
|
|
955
|
+
return join(tmpdir(), "evalkit", `${name}-${hash}`);
|
|
956
|
+
}
|
|
957
|
+
/** The warning `run` prints when workspaces sit inside the project. */
|
|
958
|
+
const SANDBOX_INSIDE_WARNING = "sandboxDir is inside the project: agents can read its source, its AGENTS.md and skills, its results and other runs’ workspaces from there. Leave sandboxDir unset to keep workspaces in the system temp directory.";
|
|
890
959
|
//#endregion
|
|
891
960
|
//#region ../cli/src/terminal.ts
|
|
892
961
|
/** Human CLI output. Colors and live progress only appear on an interactive terminal. */
|
|
@@ -1011,28 +1080,47 @@ function runOutcome(result) {
|
|
|
1011
1080
|
if (result.status === "cancelled") return "cancelled";
|
|
1012
1081
|
return result.status === "passed" ? "passed" : "failed";
|
|
1013
1082
|
}
|
|
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
1083
|
function value(score) {
|
|
1019
1084
|
return score.value === void 0 ? "" : score.value.toFixed(1);
|
|
1020
1085
|
}
|
|
1086
|
+
/**
|
|
1087
|
+
* One failed verdict in words: `history 0.0 (passes at 1): got 2 entries`,
|
|
1088
|
+
* or for a choices judge `Correctness C (passes at A or B): wrong product`.
|
|
1089
|
+
*/
|
|
1090
|
+
function verdictReason(definition, verdict) {
|
|
1091
|
+
const rule = [...definition.transcript, ...definition.scoring].find((step) => step.kind === "judge" && step.name === verdict.name);
|
|
1092
|
+
const answer = rule?.kind === "judge" ? rule.answer : void 0;
|
|
1093
|
+
const mark = verdict.passAt === void 0 ? "" : answer?.kind === "choices" ? ` (passes at ${passMark(verdict.passAt, answer)})` : ` (passes at ${verdict.passAt})`;
|
|
1094
|
+
const why = [criteriaReason(verdict.criteria), firstLine(verdict.explanation)].filter(Boolean).join(" · ");
|
|
1095
|
+
return `${verdict.name} ${verdict.choice ?? value(verdict)}${mark}${why ? `: ${why}` : ""}`;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Which criteria let the score down, so seven checks the app never reached
|
|
1099
|
+
* read as one cause (`not reached: saves, history`), not seven failures.
|
|
1100
|
+
*/
|
|
1101
|
+
function criteriaReason(criteria) {
|
|
1102
|
+
if (!criteria?.length) return "";
|
|
1103
|
+
const failed = criteria.filter((criterion) => "value" in criterion && criterion.value < 1);
|
|
1104
|
+
const unreached = criteria.filter((criterion) => !("value" in criterion));
|
|
1105
|
+
return [failed.length ? `failed ${failed.map((criterion) => {
|
|
1106
|
+
const why = firstLine(criterion.explanation, 60);
|
|
1107
|
+
return why ? `${criterion.name} (${why})` : criterion.name;
|
|
1108
|
+
}).join(", ")}` : "", unreached.length ? `not reached ${unreached.map((criterion) => criterion.name).join(", ")}` : ""].filter(Boolean).join(" · ");
|
|
1109
|
+
}
|
|
1021
1110
|
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})`}`;
|
|
1111
|
+
if (score.error) return `${score.name} error: ${firstLine(score.error.message)}`;
|
|
1112
|
+
return verdictReason(definition, score);
|
|
1025
1113
|
}
|
|
1026
1114
|
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} ${
|
|
1115
|
+
if (checkpoint.status === "error") return `step ${checkpoint.step} ${checkpoint.name} error: ${firstLine(checkpoint.error?.message)}`;
|
|
1116
|
+
if (checkpoint.value === void 0 && !checkpoint.choice) return `step ${checkpoint.step} ${checkpoint.name} ${checkpoint.status}`;
|
|
1117
|
+
return `step ${checkpoint.step} ${verdictReason(definition, checkpoint)}`;
|
|
1030
1118
|
}
|
|
1031
1119
|
/** What went wrong in a run. */
|
|
1032
1120
|
function reasonsFor(definition, run) {
|
|
1033
1121
|
const reasons = /* @__PURE__ */ new Set();
|
|
1034
1122
|
const skipped = /* @__PURE__ */ new Set();
|
|
1035
|
-
if (run.status === "errored" && run.error) reasons.add(`error: ${run.error.message}`);
|
|
1123
|
+
if (run.status === "errored" && run.error) reasons.add(`error: ${firstLine(run.error.message, 160)}`);
|
|
1036
1124
|
for (const checkpoint of run.scoring?.checkpoints ?? []) if (checkpoint.status === "failed" || checkpoint.status === "error") reasons.add(checkpointReason(definition, checkpoint));
|
|
1037
1125
|
for (const score of run.scoring?.results ?? []) if (score.skipped) skipped.add(score.name);
|
|
1038
1126
|
else if (!score.passed) reasons.add(scoreReason(definition, score));
|
|
@@ -1205,7 +1293,7 @@ function allSkipped(scenario) {
|
|
|
1205
1293
|
//#endregion
|
|
1206
1294
|
//#region ../cli/src/version.ts
|
|
1207
1295
|
/** The CLI version; kept in sync with package.json when releasing. */
|
|
1208
|
-
const VERSION = "0.
|
|
1296
|
+
const VERSION = "0.10.0";
|
|
1209
1297
|
//#endregion
|
|
1210
1298
|
//#region ../cli/src/run-command.ts
|
|
1211
1299
|
/** Validates `run` flags into a selection, parameter overrides and limits. */
|
|
@@ -1332,7 +1420,8 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1332
1420
|
runs,
|
|
1333
1421
|
parameters: selection.parameters,
|
|
1334
1422
|
overrides: selection.overrides,
|
|
1335
|
-
repeat: repeat ?? "scenario"
|
|
1423
|
+
repeat: repeat ?? "scenario",
|
|
1424
|
+
scoring: Object.fromEntries(registry.catalog({ root: project.root }).filter((entry) => matrix.evals.some((definition) => definition.id === entry.id)).map((entry) => [entry.id, plannedChecks(entry)]))
|
|
1336
1425
|
};
|
|
1337
1426
|
if (values["dry-run"]) {
|
|
1338
1427
|
console.log(JSON.stringify(plan, null, 2));
|
|
@@ -1341,7 +1430,9 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1341
1430
|
const sweeps = Object.keys(matrix.parameters).length > 0 || Boolean(matrix.cases?.length);
|
|
1342
1431
|
if (sweeps && cells > (project.config.execution?.maxCells ?? 100) && !values.all) throw new Error(`Selected ${cells} cells. Narrow the selection, inspect --dry-run, or pass --all.`);
|
|
1343
1432
|
const reportDir = resolve(project.root, project.config.reportDir ?? "_evalkit-results");
|
|
1344
|
-
const
|
|
1433
|
+
const sandbox = sandboxLocation(project);
|
|
1434
|
+
const sandboxDir = sandbox.path;
|
|
1435
|
+
if (sandbox.inside) console.error(` ${yellow("!")} ${SANDBOX_INSIDE_WARNING}`);
|
|
1345
1436
|
const source = await detectSource(project.root, { outputDirs: [
|
|
1346
1437
|
reportDir,
|
|
1347
1438
|
sandboxDir,
|
|
@@ -1385,6 +1476,7 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1385
1476
|
keepWorkspaces: project.config.keepSandboxes,
|
|
1386
1477
|
artifactExclude: project.config.artifacts?.exclude,
|
|
1387
1478
|
attachmentLimits: project.config.artifacts?.attachments,
|
|
1479
|
+
judge: project.config.judge,
|
|
1388
1480
|
signal: controller.signal,
|
|
1389
1481
|
onEvent: (run, event) => reporter.onEvent(run, event),
|
|
1390
1482
|
onResult: (cell, results) => {
|
|
@@ -1423,6 +1515,23 @@ async function runProjectCommand(cwd, parsed) {
|
|
|
1423
1515
|
if (controller.signal.aborted) process.exitCode = 130;
|
|
1424
1516
|
else if (summary.failed || broken.length) process.exitCode = 1;
|
|
1425
1517
|
}
|
|
1518
|
+
/** A scenario's checkpoints and final scorers, as `--dry-run` lists them. */
|
|
1519
|
+
function plannedChecks(entry) {
|
|
1520
|
+
return [...entry.transcript.flatMap((step, index) => step.kind === "checkpoint" ? [{
|
|
1521
|
+
scorer: step.scorer,
|
|
1522
|
+
at: `step ${index}`
|
|
1523
|
+
}] : []), ...entry.scorers.map((scorer) => ({
|
|
1524
|
+
scorer,
|
|
1525
|
+
at: "final"
|
|
1526
|
+
}))].map(({ scorer, at }) => ({
|
|
1527
|
+
name: scorer.name,
|
|
1528
|
+
kind: scorer.kind,
|
|
1529
|
+
at,
|
|
1530
|
+
...scorer.description ? { description: scorer.description } : {},
|
|
1531
|
+
passes: passMark(scorer.passAt, scorer.answer),
|
|
1532
|
+
...scorer.agent ? { agent: scorer.agent.name ?? scorer.agent.id ?? scorer.agent.kind } : {}
|
|
1533
|
+
}));
|
|
1534
|
+
}
|
|
1426
1535
|
//#endregion
|
|
1427
1536
|
//#region ../cli/src/tidy.ts
|
|
1428
1537
|
/**
|
|
@@ -1831,11 +1940,10 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
|
|
|
1831
1940
|
}
|
|
1832
1941
|
//#endregion
|
|
1833
1942
|
//#region ../cli/src/new-project.ts
|
|
1834
|
-
const npmDependency = "^0.
|
|
1943
|
+
const npmDependency = "^0.10.0";
|
|
1835
1944
|
const ignored = [
|
|
1836
1945
|
"node_modules/",
|
|
1837
1946
|
"_evalkit-results/",
|
|
1838
|
-
"_evalkit-sandbox/",
|
|
1839
1947
|
"_evalkit-dashboard/",
|
|
1840
1948
|
"_evalkit-share/"
|
|
1841
1949
|
];
|
|
@@ -5318,7 +5426,7 @@ function runInput(evalIds, flags) {
|
|
|
5318
5426
|
//#region ../cli/src/index.ts
|
|
5319
5427
|
let projectRoot = process.cwd();
|
|
5320
5428
|
let reportRoot = resolve(projectRoot, "_evalkit-results");
|
|
5321
|
-
let sandboxRoot =
|
|
5429
|
+
let sandboxRoot = defaultSandboxDir(projectRoot);
|
|
5322
5430
|
let project;
|
|
5323
5431
|
async function loadRegistry() {
|
|
5324
5432
|
project ??= await loadProject(projectRoot);
|
|
@@ -5345,6 +5453,7 @@ async function runEvals(options) {
|
|
|
5345
5453
|
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5346
5454
|
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5347
5455
|
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5456
|
+
judge: project?.config?.judge,
|
|
5348
5457
|
suiteId: options.suiteId,
|
|
5349
5458
|
runtime: runtimeFor(evaluation),
|
|
5350
5459
|
concurrency: options.concurrency,
|
|
@@ -5418,6 +5527,7 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, repeat) {
|
|
|
5418
5527
|
keepWorkspaces: project?.config?.keepSandboxes,
|
|
5419
5528
|
artifactExclude: project?.config?.artifacts?.exclude,
|
|
5420
5529
|
attachmentLimits: project?.config?.artifacts?.attachments,
|
|
5530
|
+
judge: project?.config?.judge,
|
|
5421
5531
|
...provenance,
|
|
5422
5532
|
onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
|
|
5423
5533
|
})))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
@@ -5550,7 +5660,7 @@ async function useProject(configPath) {
|
|
|
5550
5660
|
project = await loadProject(projectRoot, configPath);
|
|
5551
5661
|
projectRoot = project.root;
|
|
5552
5662
|
reportRoot = resolve(projectRoot, project.config.reportDir ?? "_evalkit-results");
|
|
5553
|
-
sandboxRoot =
|
|
5663
|
+
sandboxRoot = sandboxLocation(project).path;
|
|
5554
5664
|
}
|
|
5555
5665
|
/**
|
|
5556
5666
|
* `--port` is honoured exactly; without it the server takes the first free
|