@leostera/evalkit 0.10.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/dist/cli.mjs CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/bin/sh
2
2
  ':' //; exec "${EVALKIT_RUNTIME:-$(command -v bun || command -v node)}" "$0" "$@"
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";
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";
8
9
  import { tmpdir } from "node:os";
9
10
  import { cp, lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
10
11
  import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
- import { createHash } from "node:crypto";
12
+ import childProcess, { execFile, spawn } from "node:child_process";
12
13
  import fs, { existsSync } from "node:fs";
13
14
  import { fileURLToPath, pathToFileURL } from "node:url";
14
15
  import { Hono } from "hono";
15
- import childProcess, { execFile, spawn } from "node:child_process";
16
16
  import { promisify, stripVTControlCharacters } from "node:util";
17
17
  import { createServer } from "node:http";
18
18
  import { EventEmitter } from "node:events";
@@ -241,7 +241,7 @@ function createDashboardReportReader(root) {
241
241
  } catch {
242
242
  return false;
243
243
  }
244
- return typeof version === "number" && version < 6;
244
+ return typeof version === "number" && version < 7;
245
245
  }
246
246
  async function readRun(id) {
247
247
  if (await isLegacyRun(id)) {
@@ -271,6 +271,9 @@ function createDashboardReportReader(root) {
271
271
  manifest.aut.version
272
272
  ].filter(Boolean).join(" / ") } : {},
273
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 } : {},
274
277
  ...interrupted ? { interrupted: true } : {},
275
278
  startedAt: manifest.startedAt,
276
279
  ...summary?.endedAt ? { completedAt: summary.endedAt } : {},
@@ -332,9 +335,19 @@ function createDashboardReportReader(root) {
332
335
  }
333
336
  const candidateFile = (runId, path) => artifactFile(runId, "candidate", path);
334
337
  const attachmentFile = (runId, name) => artifactFile(runId, "attachments", name);
335
- async function trajectory(runId) {
338
+ /** The agent's transcript and EvalKit's events, as far as a running run has got. */
339
+ async function transcript(runId) {
336
340
  runPath(root, runId);
337
- return await ifPresent(() => readRunEvents(root, runId)) ?? [];
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
+ };
338
351
  }
339
352
  return {
340
353
  listRuns,
@@ -345,7 +358,7 @@ function createDashboardReportReader(root) {
345
358
  listCandidateWorkspace,
346
359
  candidateFile,
347
360
  attachmentFile,
348
- trajectory
361
+ transcript
349
362
  };
350
363
  }
351
364
  /** Both artifact endpoints walk a specific root; never broaden the candidate-file read root. */
@@ -522,7 +535,7 @@ function directorySink(root) {
522
535
  }
523
536
  /**
524
537
  * One run's snapshot, shared by `evalkit build` (files on disk) and
525
- * `evalkit share` (an upload bundle): `detail.json`, `events.json`,
538
+ * `evalkit share` (an upload bundle): `detail.json`, `transcript.json`,
526
539
  * `artifacts.json` and `workspace.json`, the candidate files under
527
540
  * `workspace/`, and attachments under `attachments/` (both left out with
528
541
  * `workspaceFiles: false`; they stay listed). Evaluator workspaces are never
@@ -531,7 +544,7 @@ function directorySink(root) {
531
544
  async function snapshotRun(reports, runId, sink, options = {}) {
532
545
  const detail = await reports.runDetail(runId);
533
546
  await sink.json("detail.json", detail);
534
- await sink.json("events.json", { events: await reports.trajectory(runId) });
547
+ await sink.json("transcript.json", await reports.transcript(runId));
535
548
  await sink.json("artifacts.json", { artifacts: await reports.listArtifacts(runId) });
536
549
  const workspace = await reports.listCandidateWorkspace(runId);
537
550
  await sink.json("workspace.json", workspace);
@@ -979,8 +992,8 @@ function bytes(size) {
979
992
  if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} kB`;
980
993
  return `${(size / 1024 ** 2).toFixed(1)} MB`;
981
994
  }
982
- function plural(count, noun) {
983
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
995
+ function plural(count, noun, nouns = `${noun}s`) {
996
+ return `${count} ${count === 1 ? noun : nouns}`;
984
997
  }
985
998
  /** Times one build step, showing a live line while it runs. */
986
999
  async function step(label, work, describe) {
@@ -1078,6 +1091,7 @@ async function confirm(question) {
1078
1091
  //#region ../cli/src/run-reporter.ts
1079
1092
  function runOutcome(result) {
1080
1093
  if (result.status === "cancelled") return "cancelled";
1094
+ if (result.status === "limited") return "limited";
1081
1095
  return result.status === "passed" ? "passed" : "failed";
1082
1096
  }
1083
1097
  function value(score) {
@@ -1148,17 +1162,26 @@ function parameterLabel(parameters) {
1148
1162
  }
1149
1163
  /** One printed line for a finished run; failing reasons on the same line. */
1150
1164
  function runLine(scenario) {
1151
- 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("✗");
1152
1166
  const parameters = parameterLabel(scenario.parameters);
1153
1167
  const batch = scenario.result.batch;
1154
1168
  const position = batch ? dim(` #${batch.index + 1}/${batch.size}`) : "";
1155
1169
  const details = [
1156
- ...scenario.outcome === "failed" ? scenario.reasons : [],
1170
+ ...scenario.outcome === "limited" ? [limitReason(scenario.result)] : [],
1171
+ ...scenario.outcome === "failed" || scenario.outcome === "limited" ? scenario.reasons : [],
1157
1172
  ...scenario.outcome === "cancelled" ? ["cancelled"] : [],
1158
1173
  ...scenario.skipped.map((name) => `${name} skipped`)
1159
1174
  ];
1160
1175
  return ` ${mark} ${bold(scenario.label)}${parameters ? ` ${dim(parameters)}` : ""}${position}${details.length ? ` ${dim(details.join(" · "))}` : ""}`;
1161
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
+ }
1162
1185
  /**
1163
1186
  * Prints run progress. On a terminal, failures print as they happen and a live
1164
1187
  * line shows counts and what is running. Elsewhere (CI), every finished
@@ -1203,8 +1226,15 @@ function createRunReporter(options) {
1203
1226
  timer.unref?.();
1204
1227
  }
1205
1228
  },
1206
- onEvent(run, event) {
1207
- if (event.source === "runner" && event.kind === "run-started") {
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") {
1208
1238
  running.set(run.runId, {
1209
1239
  label: options.labelFor(run),
1210
1240
  startedAt: now()
@@ -1213,10 +1243,10 @@ function createRunReporter(options) {
1213
1243
  }
1214
1244
  const activity = running.get(run.runId);
1215
1245
  if (!activity) return;
1216
- if (event.source === "runner" && (event.kind === "run-completed" || event.kind === "error")) running.delete(run.runId);
1217
- else if (event.source === "aut" && event.kind === "tool-call") activity.doing = `tool ${event.name}`;
1218
- else if (event.source === "runner" && event.kind === "scorer-started") activity.doing = `scoring ${event.scorer}`;
1219
- else if (event.source === "runner" && event.kind === "checkpoint-started") activity.doing = `check ${event.name}`;
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}`;
1220
1250
  },
1221
1251
  onRun(scenario) {
1222
1252
  scenarios.push(scenario);
@@ -1233,6 +1263,7 @@ function createRunReporter(options) {
1233
1263
  return {
1234
1264
  passed: scenarios.filter((s) => s.outcome === "passed").length,
1235
1265
  failed: scenarios.filter((s) => s.outcome === "failed").length,
1266
+ limited: scenarios.filter((s) => s.outcome === "limited").length,
1236
1267
  cancelled: scenarios.filter((s) => s.outcome === "cancelled").length,
1237
1268
  runs: scenarios.length,
1238
1269
  scenarios: new Set(scenarios.map((s) => `${s.label} ${parameterLabel(s.parameters)}`)).size,
@@ -1246,6 +1277,7 @@ function totalsLine(totals) {
1246
1277
  const parts = [
1247
1278
  green(`${totals.passed} passed`),
1248
1279
  totals.failed ? red(`${totals.failed} failed`) : "",
1280
+ totals.limited ? yellow(`${totals.limited} limited`) : "",
1249
1281
  totals.cancelled ? yellow(`${totals.cancelled} cancelled`) : ""
1250
1282
  ].filter(Boolean);
1251
1283
  const coverage = totals.runs === totals.scenarios ? plural(totals.runs, "run") : `${plural(totals.runs, "run")} of ${plural(totals.scenarios, "scenario")}`;
@@ -1269,13 +1301,17 @@ function junitReport(scenarios) {
1269
1301
  bySuite.set(suite, [...bySuite.get(suite) ?? [], scenario]);
1270
1302
  }
1271
1303
  const suites = [...bySuite].map(([suite, cases]) => {
1272
- const failures = cases.filter((c) => c.outcome === "failed").length;
1304
+ const failures = cases.filter((c) => c.outcome === "failed" || c.outcome === "limited").length;
1273
1305
  const skipped = cases.filter((c) => c.outcome === "cancelled" || c.outcome === "passed" && c.skipped.length > 0 && allSkipped(c)).length;
1274
1306
  const time = cases.reduce((sum, c) => sum + (c.durationMs ?? 0), 0);
1275
1307
  const testcases = cases.map((scenario) => {
1276
1308
  const parameters = parameterLabel(scenario.parameters);
1277
1309
  const batch = scenario.result.batch;
1278
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
+ }
1279
1315
  if (scenario.outcome === "failed") return ` <testcase ${attributes}>\n <failure message="${escape(scenario.reasons.join("; ") || "failed")}">${escape(scenario.reasons.join("\n"))}</failure>\n </testcase>`;
1280
1316
  if (scenario.outcome === "cancelled") return ` <testcase ${attributes}>\n <skipped message="cancelled"/>\n </testcase>`;
1281
1317
  if (allSkipped(scenario)) return ` <testcase ${attributes}>\n <skipped message="${escape(`all scorers skipped: ${scenario.skipped.join(", ")}`)}"/>\n </testcase>`;
@@ -1283,7 +1319,7 @@ function junitReport(scenarios) {
1283
1319
  });
1284
1320
  return ` <testsuite name="${escape(suite)}" tests="${cases.length}" failures="${failures}" errors="0" skipped="${skipped}" time="${seconds(time)}">\n${testcases.join("\n")}\n </testsuite>`;
1285
1321
  });
1286
- return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites name="evalkit" tests="${scenarios.length}" failures="${scenarios.filter((c) => c.outcome === "failed").length}">\n${suites.join("\n")}\n</testsuites>\n`;
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`;
1287
1323
  }
1288
1324
  /** True when every score was skip()ped. */
1289
1325
  function allSkipped(scenario) {
@@ -1291,9 +1327,71 @@ function allSkipped(scenario) {
1291
1327
  return results.length > 0 && results.every((score) => score.skipped);
1292
1328
  }
1293
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
1294
1392
  //#region ../cli/src/version.ts
1295
1393
  /** The CLI version; kept in sync with package.json when releasing. */
1296
- const VERSION = "0.10.0";
1394
+ const VERSION = "0.11.0";
1297
1395
  //#endregion
1298
1396
  //#region ../cli/src/run-command.ts
1299
1397
  /** Validates `run` flags into a selection, parameter overrides and limits. */
@@ -1306,14 +1404,7 @@ function normalizeRunOptions(values, positionals) {
1306
1404
  if (!Number.isSafeInteger(number) || number < 1) throw new Error(`--${name} must be a positive integer`);
1307
1405
  return number;
1308
1406
  }
1309
- for (const [flag, key] of [
1310
- ["max-tokens", "maxTokens"],
1311
- ["chat-timeout-ms", "chatTimeoutMs"],
1312
- ["turn-budget", "turnBudget"]
1313
- ]) {
1314
- const value = integer(flag);
1315
- if (value !== void 0) parameters[key] = value;
1316
- }
1407
+ const limits = resolveLimits(...(values.limit ?? []).flatMap((value) => value.split(",")).map(parseLimitFlag));
1317
1408
  const selection = {};
1318
1409
  for (const key of ["model", "mode"]) if (values[key]?.length) selection[key] = values[key].flatMap((value) => value.split(","));
1319
1410
  for (const value of values.select ?? []) {
@@ -1339,6 +1430,7 @@ function normalizeRunOptions(values, positionals) {
1339
1430
  positionals,
1340
1431
  parameters,
1341
1432
  selection,
1433
+ limits,
1342
1434
  concurrency: integer("concurrency"),
1343
1435
  repeat: integer("repeat")
1344
1436
  };
@@ -1411,6 +1503,7 @@ async function runProjectCommand(cwd, parsed) {
1411
1503
  const cells = matrix.count(selection);
1412
1504
  if (!cells) throw new Error("Selection contains no eligible cells; check cases and exclusions");
1413
1505
  const repeat = parsed.repeat ?? project.config.execution?.repeat;
1506
+ const configLimits = parseLimits(project.config.limits, "The config");
1414
1507
  let runs = 0;
1415
1508
  for (const cell of matrix.cells(selection)) runs += repeat ?? cell.eval.repeat ?? 1;
1416
1509
  const plan = {
@@ -1421,6 +1514,12 @@ async function runProjectCommand(cwd, parsed) {
1421
1514
  parameters: selection.parameters,
1422
1515
  overrides: selection.overrides,
1423
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 ?? {}),
1424
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)]))
1425
1524
  };
1426
1525
  if (values["dry-run"]) {
@@ -1462,7 +1561,9 @@ async function runProjectCommand(cwd, parsed) {
1462
1561
  reporter.start();
1463
1562
  let summary;
1464
1563
  try {
1465
- 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 } : {},
1466
1567
  selection,
1467
1568
  concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
1468
1569
  ...repeat ? { repeat } : {},
@@ -1473,12 +1574,12 @@ async function runProjectCommand(cwd, parsed) {
1473
1574
  catalog: runCatalog(registry, project.root),
1474
1575
  report: localReportStore(reportDir),
1475
1576
  workspaceRoot: sandboxDir,
1476
- keepWorkspaces: project.config.keepSandboxes,
1477
- artifactExclude: project.config.artifacts?.exclude,
1478
- attachmentLimits: project.config.artifacts?.attachments,
1479
- judge: project.config.judge,
1480
1577
  signal: controller.signal,
1481
- onEvent: (run, event) => reporter.onEvent(run, event),
1578
+ limits: {
1579
+ base: configLimits,
1580
+ override: parsed.limits
1581
+ },
1582
+ onUpdate: (run, update) => reporter.onUpdate(run, update),
1482
1583
  onResult: (cell, results) => {
1483
1584
  for (const result of results) {
1484
1585
  const report = runReport(cell, result, suiteFor(authoringId(cell.eval)));
@@ -1494,7 +1595,7 @@ async function runProjectCommand(cwd, parsed) {
1494
1595
  reporter.onRun(report);
1495
1596
  }
1496
1597
  }
1497
- })));
1598
+ }))));
1498
1599
  } finally {
1499
1600
  process.off("SIGINT", onInterrupt);
1500
1601
  }
@@ -1508,12 +1609,14 @@ async function runProjectCommand(cwd, parsed) {
1508
1609
  else {
1509
1610
  console.log(`\n${totalsLine(totals)}`);
1510
1611
  if (values.junit) console.log(` ${dim(`JUnit report: ${values.junit}`)}`);
1511
- console.log(` ${cyan("➜")} bun run evalkit dev ${dim("to inspect")}`);
1512
- if (project.config.share) console.log(` ${cyan("➜")} bun run evalkit share ${dim("to publish")}`);
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");
1513
1616
  console.log("");
1514
1617
  }
1515
1618
  if (controller.signal.aborted) process.exitCode = 130;
1516
- else if (summary.failed || broken.length) process.exitCode = 1;
1619
+ else if (summary.failed || summary.limited || broken.length) process.exitCode = 1;
1517
1620
  }
1518
1621
  /** A scenario's checkpoints and final scorers, as `--dry-run` lists them. */
1519
1622
  function plannedChecks(entry) {
@@ -1582,8 +1685,8 @@ async function tidyReports(options) {
1582
1685
  return result;
1583
1686
  }
1584
1687
  /**
1585
- * Remove the run workspaces `keep` would not have kept: those of passed runs
1586
- * by default. Unfinished runs are never touched, so a run in progress keeps
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
1587
1690
  * its workspace; workspaces without a report are left alone.
1588
1691
  */
1589
1692
  async function tidySandboxes(options) {
@@ -1600,6 +1703,7 @@ async function tidySandboxes(options) {
1600
1703
  recursive: true,
1601
1704
  force: true
1602
1705
  });
1706
+ if (run.isolation?.kind === "container") await removeRunContainer(run.id, options.runtime);
1603
1707
  removed += 1;
1604
1708
  }
1605
1709
  return { removed };
@@ -1678,7 +1782,7 @@ async function runBundle(options) {
1678
1782
  files.push({
1679
1783
  path: "run.json",
1680
1784
  data: encode({
1681
- snapshotVersion: 2,
1785
+ snapshotVersion: 3,
1682
1786
  ...options.run
1683
1787
  })
1684
1788
  });
@@ -1873,7 +1977,7 @@ export const greetingAgent = defineAgent({
1873
1977
  identity: { id: 'greeting-agent', kind: 'in-process' },
1874
1978
  async start({ context }) {
1875
1979
  return {
1876
- // Return the reply; EvalKit records it. Call onEvent instead to stream events.
1980
+ // Return the reply; EvalKit records it. Use the transcript writer in start() to record tool calls and usage.
1877
1981
  async send(message: string) {
1878
1982
  const file = join(context.workspace.root, 'greeting', 'greeting.txt');
1879
1983
  const prefix = (await readFile(file, 'utf8')).trim();
@@ -1940,7 +2044,7 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
1940
2044
  }
1941
2045
  //#endregion
1942
2046
  //#region ../cli/src/new-project.ts
1943
- const npmDependency = "^0.10.0";
2047
+ const npmDependency = "^0.11.0";
1944
2048
  const ignored = [
1945
2049
  "node_modules/",
1946
2050
  "_evalkit-results/",
@@ -2055,9 +2159,11 @@ async function writeStarterReadme(root, name, inPlace) {
2055
2159
  }
2056
2160
  //#endregion
2057
2161
  //#region ../cli/src/compare.ts
2162
+ /** Runs that ended on their own; a limited run is a finished failure. */
2058
2163
  const FINISHED = /* @__PURE__ */ new Set([
2059
2164
  "passed",
2060
2165
  "failed",
2166
+ "limited",
2061
2167
  "errored"
2062
2168
  ]);
2063
2169
  function unitKey(run) {
@@ -5364,12 +5470,13 @@ function createProgram(actions) {
5364
5470
  showGlobalOptions: false
5365
5471
  }).addHelpText("after", `\nRun from your eval project's directory. Docs: https://evalkit.leostera.dev/docs/\n`);
5366
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));
5367
- program.command("run").description("run scenarios and save the results in _evalkit-results/").argument("[eval-ids...]", "scenarios to run (default: all)").addOption(configOption()).option("-e, --eval <id>", "run this scenario; repeatable, or comma-separated", collect).option("-s, --suite <id>", "run the scenarios in this suite; with -m, only those in both").option("-m, --matrix <id>", "run this matrix").option("--model <value>", "--select model=<value> for each comma-separated value", collect).option("--mode <value>", "--select mode=<value> for each comma-separated value", collect).option("-a, --select <axis=value>", "keep only these values of a matrix axis (a for axis); repeatable, comma-separated", collect).option("--param <name=json>", "set a parameter for every scenario; repeatable", collect).option("-n, --repeat <n>", "runs of each scenario, as one batch; overrides config and scenario").option("-j, --concurrency <n>", "matrix cells to run at once (default: 4)").option("--max-tokens <n>", "pass maxTokens to the agent in context.parameters").option("--turn-budget <n>", "pass turnBudget to the agent in context.parameters").option("--chat-timeout-ms <n>", "pass chatTimeoutMs to the agent in context.parameters").option("--local", "require the agent's local runtime").option("--all", "allow more matrix cells than execution.maxCells").option("--dry-run", "print the plan as JSON; run nothing").option("--json", "print one JSON result per matrix cell").option("--junit <file>", "write a JUnit XML report").option("--skip-broken", "run healthy scenarios when some files fail to load, then exit 1").option("-v, --verbose", "print every scenario, not only failures").addOption(logLevelOption()).addHelpText("after", examples([
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([
5368
5474
  "evalkit run",
5369
5475
  "evalkit run greeting -n 5",
5370
5476
  "evalkit run -s billing --junit _evalkit-results/junit.xml",
5371
5477
  "evalkit run -m models --model model-a,model-b --dry-run",
5372
- "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"
5373
5480
  ])).action((evalIds, options) => {
5374
5481
  setRunnerLogLevel(options.logLevel);
5375
5482
  return actions.run(runInput(evalIds, options));
@@ -5408,9 +5515,7 @@ function runInput(evalIds, flags) {
5408
5515
  mode: flags.mode,
5409
5516
  select: flags.select,
5410
5517
  param: flags.param,
5411
- "max-tokens": flags.maxTokens,
5412
- "chat-timeout-ms": flags.chatTimeoutMs,
5413
- "turn-budget": flags.turnBudget,
5518
+ limit: flags.limit,
5414
5519
  concurrency: flags.concurrency,
5415
5520
  repeat: flags.repeat,
5416
5521
  json: flags.json,
@@ -5444,22 +5549,20 @@ async function runEvals(options) {
5444
5549
  const evaluations = options.evalIds.map((id) => registry.get(id));
5445
5550
  if (evaluations.some((e) => !e)) throw new Error("Unknown eval");
5446
5551
  const provenance = await runProvenance(registry);
5447
- await Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
5552
+ await withFixtures(projectRoot, project?.config, (fixtures) => Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
5448
5553
  const name = evaluation.name ?? authoringId(evaluation);
5449
5554
  const started = performance.now();
5450
5555
  return Effect.sync(() => runLog.started(name, {}, options.suiteId)).pipe(Effect.andThen(runEval(evaluation, {
5556
+ ...projectRunOptions(projectRoot, project?.config),
5557
+ ...fixtures ? { fixtures } : {},
5451
5558
  report: localReportStore(reportRoot),
5452
5559
  workspaceRoot: sandboxRoot,
5453
- keepWorkspaces: project?.config?.keepSandboxes,
5454
- artifactExclude: project?.config?.artifacts?.exclude,
5455
- attachmentLimits: project?.config?.artifacts?.attachments,
5456
- judge: project?.config?.judge,
5457
5560
  suiteId: options.suiteId,
5458
5561
  runtime: runtimeFor(evaluation),
5459
5562
  concurrency: options.concurrency,
5460
5563
  ...provenance
5461
5564
  })), Effect.tap((result) => runLog.finished(name, {}, result, performance.now() - started)), Effect.tapError((error) => Effect.sync(() => runLog.failed(name, error))));
5462
- }), { concurrency: options.concurrency })));
5565
+ }), { concurrency: options.concurrency }))));
5463
5566
  }
5464
5567
  /** Detected per launch: `evalkit dev` keeps running while commits change. */
5465
5568
  async function runProvenance(registry) {
@@ -5517,20 +5620,18 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, repeat) {
5517
5620
  const name = evaluation.name ?? authoringId(evaluation);
5518
5621
  const started = performance.now();
5519
5622
  runLog.started(name, body.parameters ?? {}, suiteId);
5520
- 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 } : {},
5521
5626
  selection,
5522
5627
  concurrency: 1,
5523
5628
  ...repeat ? { repeat } : {},
5524
5629
  ...suiteId ? { suiteId } : {},
5525
5630
  report: localReportStore(reportRoot),
5526
5631
  workspaceRoot: sandboxRoot,
5527
- keepWorkspaces: project?.config?.keepSandboxes,
5528
- artifactExclude: project?.config?.artifacts?.exclude,
5529
- attachmentLimits: project?.config?.artifacts?.attachments,
5530
- judge: project?.config?.judge,
5531
5632
  ...provenance,
5532
5633
  onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
5533
- })))).catch((error) => runLog.failed(name, error)).finally(() => {
5634
+ }))))).catch((error) => runLog.failed(name, error)).finally(() => {
5534
5635
  matrixRunning = false;
5535
5636
  });
5536
5637
  } else {
@@ -5625,9 +5726,9 @@ function registerReportRoutes(app, registry, reports) {
5625
5726
  return context.text(error instanceof Error ? error.message : "Not found", 404);
5626
5727
  }
5627
5728
  });
5628
- app.get("/v1/runs/:runId/events", async (context) => {
5729
+ app.get("/v1/runs/:runId/transcript", async (context) => {
5629
5730
  try {
5630
- return context.json({ events: await reports.trajectory(context.req.param("runId")) });
5731
+ return context.json(await reports.transcript(context.req.param("runId")));
5631
5732
  } catch (error) {
5632
5733
  return context.text(error instanceof Error ? error.message : "Not found", 404);
5633
5734
  }
@@ -5781,7 +5882,8 @@ async function tidyAction(options) {
5781
5882
  registry: project.registry,
5782
5883
  reportRoot,
5783
5884
  sandboxRoot,
5784
- keep: project.config.keepSandboxes
5885
+ keep: project.config.keepSandboxes,
5886
+ ...project.config.isolation?.kind === "container" ? { runtime: project.config.isolation.runtime } : {}
5785
5887
  }), (value) => {
5786
5888
  const keep = project.config.keepSandboxes ?? "failed";
5787
5889
  if (keep === "all") return "all kept (keepSandboxes: 'all')";
@@ -5800,14 +5902,21 @@ async function upgradeAction(options) {
5800
5902
  banner("upgrade", VERSION);
5801
5903
  const { root, config } = await loadConfig(projectRoot, options.config);
5802
5904
  reportRoot = resolve(root, config.reportDir ?? "_evalkit-results");
5803
- const report = await step(options.dryRun ? "Checking results" : "Upgrading results", () => upgradeReports(reportRoot, { dryRun: options.dryRun }), (value) => `${plural(value.upgraded, "run")} from an older EvalKit ${options.dryRun ? "found" : "upgraded"}`);
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"}`);
5804
5906
  console.log("");
5805
- if (!report.upgraded) console.log(` ${cyan("✓")} Every run is already in the current format.`);
5806
- else {
5807
- const into = `${plural(report.upgraded, "run")} from older EvalKit versions into ${plural(report.runs, "run")}`;
5808
- console.log(options.dryRun ? ` ${cyan("◆")} Would upgrade ${into}. Nothing was changed.` : ` ${cyan("✓")} Upgraded ${into}.`);
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}.`);
5809
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.`)}`);
5810
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
+ }
5811
5920
  if (report.running.length) console.log(` ${yellow("◆")} Left ${plural(report.running.length, "run")} still running alone; upgrade again when they finish.`);
5812
5921
  console.log("");
5813
5922
  }