@hue-run/sdk 0.19.1 → 0.20.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.
Files changed (48) hide show
  1. package/CLI.md +27 -10
  2. package/ENVIRONMENTS.md +5 -2
  3. package/EVALUATIONS.md +6 -1
  4. package/README.md +2 -2
  5. package/WORLDS.md +122 -0
  6. package/dist/cli/eval.d.ts +4 -0
  7. package/dist/cli/eval.js +434 -186
  8. package/dist/cli/listen.js +1 -1
  9. package/dist/environment/client.d.ts +2 -95
  10. package/dist/environment/client.js +2 -283
  11. package/dist/environment/tools.d.ts +2 -35
  12. package/dist/environment/tools.js +2 -30
  13. package/dist/environment/types.d.ts +2 -623
  14. package/dist/environment/types.js +2 -1
  15. package/dist/environment/world.d.ts +2 -65
  16. package/dist/environment/world.js +2 -171
  17. package/dist/environment.d.ts +7 -7
  18. package/dist/environment.js +3 -3
  19. package/dist/evals/attempt.d.ts +1 -1
  20. package/dist/evals/client.d.ts +3 -2
  21. package/dist/evals/client.js +2 -1
  22. package/dist/evals/environment-target.d.ts +3 -3
  23. package/dist/evals/environment-target.js +3 -3
  24. package/dist/evals/failure.js +1 -1
  25. package/dist/evals/local-worker.d.ts +8 -3
  26. package/dist/evals/local-worker.js +10 -2
  27. package/dist/evals/simulation.d.ts +6 -3
  28. package/dist/evals/simulation.js +2 -1
  29. package/dist/evals/types.d.ts +2 -2
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/dist/world/client.d.ts +150 -0
  33. package/dist/world/client.js +368 -0
  34. package/dist/world/definitions.d.ts +47 -0
  35. package/dist/world/definitions.js +1 -0
  36. package/dist/world/model-types.d.ts +117 -0
  37. package/dist/world/model-types.js +1 -0
  38. package/dist/world/models.d.ts +40 -0
  39. package/dist/world/models.js +56 -0
  40. package/dist/world/tools.d.ts +35 -0
  41. package/dist/world/tools.js +30 -0
  42. package/dist/world/types.d.ts +625 -0
  43. package/dist/world/types.js +1 -0
  44. package/dist/world/world.d.ts +65 -0
  45. package/dist/world/world.js +175 -0
  46. package/dist/world.d.ts +13 -0
  47. package/dist/world.js +5 -0
  48. package/package.json +8 -2
package/dist/cli/eval.js CHANGED
@@ -7,8 +7,8 @@ import { pathToFileURL } from "node:url";
7
7
  import { parseArgs } from "node:util";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { createHue } from "../client.js";
10
- import { createEnvironmentClient } from "../environment/client.js";
11
- import { agentEnvironment, isHueControlPlaneCredential, stripHueControlPlaneCredentials, withoutWorldVariables, writeMcpConfig, } from "../environment/world.js";
10
+ import { createEnvironmentClient } from "../world/client.js";
11
+ import { agentEnvironment, isHueControlPlaneCredential, stripHueControlPlaneCredentials, withoutWorldVariables, writeMcpConfig, } from "../world/world.js";
12
12
  import { EvaluationClient, HueApiError } from "../evals/client.js";
13
13
  import { TargetResult } from "../evals/types.js";
14
14
  import { CheckpointIdentityError, CheckpointStore, checkpointPath } from "../evals/checkpoint.js";
@@ -96,6 +96,8 @@ Output and limits:
96
96
  arrive, which Hue attaches once its export is complete
97
97
  --timeout <seconds> Per-case --command timeout (default: 600)
98
98
  --wait <seconds> Verdict wait after the run finishes (default: 300)
99
+ --judges Request pinned hosted model judges after outputs upload
100
+ (off by default; uses the project's judge budget)
99
101
  -h, --help Show this help
100
102
 
101
103
  HUE_API_KEY must be a "Read and write" project key; it is never printed.
@@ -149,6 +151,7 @@ function parse(argv) {
149
151
  content: { type: "boolean" },
150
152
  "no-content": { type: "boolean", default: false },
151
153
  "no-output": { type: "boolean", default: false },
154
+ judges: { type: "boolean", default: false },
152
155
  "trace-not-accepted": { type: "string" },
153
156
  "save-version": { type: "boolean", default: false },
154
157
  check: { type: "boolean", default: false },
@@ -1054,8 +1057,133 @@ function withTelemetryFailures(verdicts, telemetry = []) {
1054
1057
  totals[item.state]++;
1055
1058
  return { ...verdicts, summary: { cases, totals } };
1056
1059
  }
1057
- /** Waits for Hue's verdicts, prints them (table or JSON, with the optional baseline) and returns the exit code. */
1058
- async function reportVerdicts(client, values, run, baselineId, output, signal) {
1060
+ /** Requests only the run's pinned model judges. Stable batches let an interrupted CLI resume
1061
+ * dispatch without creating a second paid job for the same scoring item and evaluator. */
1062
+ export async function dispatchPinnedJudges(client, experimentId, subjectIds, signal) {
1063
+ signal?.throwIfAborted();
1064
+ const experiment = await client.getExperiment(experimentId);
1065
+ const run = experiment.evaluation;
1066
+ const scorerVersionIds = [
1067
+ ...new Set(run.scorerVersions
1068
+ .filter((version) => version.definition.kind === "llm_judge")
1069
+ .map((version) => version.id)),
1070
+ ].sort();
1071
+ if (!scorerVersionIds.length)
1072
+ return 0;
1073
+ const subjects = new Set(subjectIds);
1074
+ const items = new Set();
1075
+ let after;
1076
+ do {
1077
+ signal?.throwIfAborted();
1078
+ const page = await client.listEvaluationItems(run.id, { after });
1079
+ for (const item of page.items)
1080
+ if (item.hasOutput && subjects.has(item.subjectId))
1081
+ items.add(item.id);
1082
+ after = page.nextCursor ?? undefined;
1083
+ } while (after);
1084
+ if (!items.size)
1085
+ return 0;
1086
+ let count = 0;
1087
+ let jobs = [];
1088
+ const dispatch = async () => {
1089
+ signal?.throwIfAborted();
1090
+ try {
1091
+ // Admission belongs to the server: an accepted keyed request must remain replayable
1092
+ // even if the project's budget is disabled before its acknowledgement reaches us.
1093
+ await client.createJudgeJobs(run.id, {
1094
+ idempotencyKey: `cli-judges-${digest({ runId: run.id, jobs })}`,
1095
+ jobs,
1096
+ });
1097
+ }
1098
+ catch (error) {
1099
+ if (error instanceof HueApiError && error.status === 402)
1100
+ throw new Error("Hosted judges are unavailable for this project; check its judge budget in Hue", { cause: error });
1101
+ throw error;
1102
+ }
1103
+ count += jobs.length;
1104
+ jobs = [];
1105
+ };
1106
+ for (const evaluationItemId of [...items].sort()) {
1107
+ for (const scorerVersionId of scorerVersionIds) {
1108
+ jobs.push({ evaluationItemId, scorerVersionId });
1109
+ if (jobs.length === 100)
1110
+ await dispatch();
1111
+ }
1112
+ }
1113
+ if (jobs.length)
1114
+ await dispatch();
1115
+ return count;
1116
+ }
1117
+ /** Durable handoff before the original attempt marker or queue acknowledgement. */
1118
+ async function checkpointJudges(judging, run, completion) {
1119
+ await judging.store.write("pending", {
1120
+ selectionDigest: judging.selectionDigest,
1121
+ run: {
1122
+ experimentId: run.experimentId,
1123
+ subjectIds: run.subjectIds,
1124
+ runUrl: run.runUrl,
1125
+ ...(run.extra ? { extra: run.extra } : {}),
1126
+ ...(run.telemetryNotAccepted ? { telemetryNotAccepted: run.telemetryNotAccepted } : {}),
1127
+ },
1128
+ completion,
1129
+ });
1130
+ }
1131
+ /** Finish the original handoff if the process stopped between the two durable writes. */
1132
+ async function completeJudgeHandoff(client, judging, pending) {
1133
+ const store = await CheckpointStore.acquire(judging.source.directory, judging.source.identity);
1134
+ try {
1135
+ if (pending.completion.kind === "worker") {
1136
+ try {
1137
+ await client.completeLocalAgentRun({
1138
+ runId: pending.completion.runId,
1139
+ workerId: pending.completion.workerId,
1140
+ state: "completed",
1141
+ });
1142
+ }
1143
+ catch (error) {
1144
+ // Released or cancelled work cannot be completed by this worker. Temporary failures
1145
+ // still throw, leaving the durable handoff available for the unchanged command.
1146
+ if (error instanceof HueApiError && error.status === 409)
1147
+ return false;
1148
+ throw error;
1149
+ }
1150
+ }
1151
+ else {
1152
+ const attempt = await store.read("active-attempt");
1153
+ if (!attempt || attempt.experimentId !== pending.run.experimentId)
1154
+ throw new Error("Hosted judge checkpoint does not match the uploaded attempt");
1155
+ if (attempt.stage !== "completed")
1156
+ await store.write("active-attempt", { ...attempt, stage: "completed" });
1157
+ }
1158
+ return true;
1159
+ }
1160
+ finally {
1161
+ await store.release();
1162
+ }
1163
+ }
1164
+ /** Discards only the matching terminal worker claim; a retry retains its handoff. */
1165
+ async function discardWorkerJudgeHandoff(judging, runId) {
1166
+ const pending = await judging.store.read("pending");
1167
+ if (pending?.completion.kind === "worker" && pending.completion.runId === runId)
1168
+ await judging.store.write("pending", null);
1169
+ }
1170
+ async function requestRunJudges(client, run, output, signal, judging) {
1171
+ const count = await dispatchPinnedJudges(client, run.experimentId, run.subjectIds, signal);
1172
+ await judging.store.write("pending", null);
1173
+ if (count)
1174
+ output.log(`Requested ${count} hosted judge check${count === 1 ? "" : "s"}.`);
1175
+ }
1176
+ /** Dispatch failures retain the handoff; reading verdicts does not dispatch again. */
1177
+ async function reportVerdicts(client, values, run, baselineId, output, signal, judging) {
1178
+ if (values.judges) {
1179
+ if (!judging)
1180
+ throw new Error("Hosted judge recovery checkpoint is unavailable");
1181
+ await requestRunJudges(client, run, output, signal, judging);
1182
+ }
1183
+ return readAndReportVerdicts(client, values, run, baselineId, output, signal);
1184
+ }
1185
+ /** Waits for Hue's verdicts, prints them and returns the exit code. */
1186
+ async function readAndReportVerdicts(client, values, run, baselineId, output, signal) {
1059
1187
  const wait = integer("wait", values.wait, 300, 0, 86_400);
1060
1188
  output.log("Waiting for Hue checks...");
1061
1189
  const verdicts = withTelemetryFailures(await collectExperimentVerdicts(client, {
@@ -1127,88 +1255,125 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
1127
1255
  }
1128
1256
  const runName = values.name ?? defaultRunName(agent);
1129
1257
  const project = await client.checkConnection();
1130
- if (selection.direct)
1131
- return runDirect({
1258
+ const checkpointDirectory = await prepareCheckpointDirectory(values["checkpoint-dir"], agent.key, project.id, selection.direct ? "direct" : "simulation");
1259
+ const judging = values.judges
1260
+ ? {
1261
+ store: await CheckpointStore.acquire(checkpointPath(checkpointDirectory, ["judge-dispatch", "judge checkpoint"]), { kind: "cli-judge-dispatch", projectId: project.id, baseUrl: connection.baseUrl }),
1262
+ selectionDigest: digest({
1263
+ datasetVersionId: pins.datasetVersionId,
1264
+ scorerVersionIds: [...pins.scorerVersionIds].sort(),
1265
+ agent,
1266
+ }),
1267
+ source: {
1268
+ directory: checkpointDirectory,
1269
+ identity: {
1270
+ kind: selection.direct ? "direct" : "simulation",
1271
+ projectId: project.id,
1272
+ baseUrl: connection.baseUrl,
1273
+ },
1274
+ },
1275
+ }
1276
+ : undefined;
1277
+ try {
1278
+ const pending = await judging?.store.read("pending");
1279
+ if (pending) {
1280
+ if (pending.selectionDigest !== judging.selectionDigest)
1281
+ throw new Error("Recover the pending judge dispatch with the unchanged command or use another --checkpoint-dir");
1282
+ output.log(`Resuming hosted checks: ${pending.run.runUrl}`);
1283
+ await completeJudgeHandoff(client, judging, pending);
1284
+ return await reportVerdicts(client, values, pending.run, baselineId, output, signal, judging);
1285
+ }
1286
+ if (selection.direct)
1287
+ return await runDirect({
1288
+ client,
1289
+ values,
1290
+ pins,
1291
+ runName,
1292
+ projectId: project.id,
1293
+ agent,
1294
+ agents,
1295
+ hue,
1296
+ output,
1297
+ signal,
1298
+ concurrency,
1299
+ baselineId,
1300
+ judging,
1301
+ }, connection);
1302
+ const environmentClient = createEnvironmentClient(connection);
1303
+ const caseKeys = new Map();
1304
+ let runUrl = "";
1305
+ const telemetry = telemetryReporter(output);
1306
+ const report = await runSimulation({
1132
1307
  client,
1133
- values,
1134
- pins,
1135
- runName,
1136
- projectId: project.id,
1137
- agent,
1138
- agents,
1308
+ environmentClient,
1139
1309
  hue,
1140
- output,
1141
- signal,
1310
+ checkpointDirectory,
1311
+ definition: {
1312
+ kind: "pins",
1313
+ datasetVersionId: pins.datasetVersionId,
1314
+ scorerVersionIds: pins.scorerVersionIds,
1315
+ },
1316
+ runName,
1317
+ // Outputs, error messages and explanations are stored unless opted out; --content governs
1318
+ // only the telemetry.
1319
+ persistResultContent: !values["no-output"],
1320
+ traceEvidence: { mode: "required" },
1321
+ // A case whose telemetry Hue did not accept fails instead of staying started.
1322
+ traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
1323
+ onTelemetryNotAccepted: telemetry.report,
1142
1324
  concurrency,
1143
- baselineId,
1144
- }, connection);
1145
- const environmentClient = createEnvironmentClient(connection);
1146
- const checkpointDirectory = await prepareCheckpointDirectory(values["checkpoint-dir"], agent.key, project.id, "simulation");
1147
- const caseKeys = new Map();
1148
- let runUrl = "";
1149
- const telemetry = telemetryReporter(output);
1150
- const report = await runSimulation({
1151
- client,
1152
- environmentClient,
1153
- hue,
1154
- checkpointDirectory,
1155
- definition: {
1156
- kind: "pins",
1157
- datasetVersionId: pins.datasetVersionId,
1158
- scorerVersionIds: pins.scorerVersionIds,
1159
- },
1160
- runName,
1161
- // Outputs, error messages and explanations are stored unless opted out; --content governs
1162
- // only the telemetry.
1163
- persistResultContent: !values["no-output"],
1164
- traceEvidence: { mode: "required" },
1165
- // A case whose telemetry Hue did not accept fails instead of staying started.
1166
- traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
1167
- onTelemetryNotAccepted: telemetry.report,
1168
- concurrency,
1169
- agentRevision: agent.revision,
1170
- // The CLI adapts to whatever the deployment serves; the library warning is for code that
1171
- // still reads the legacy capability itself.
1172
- deprecationWarnings: false,
1173
- signal,
1174
- target: agents.simulation,
1175
- async onProgress(event) {
1176
- if (event.type === "run_created") {
1177
- runUrl = event.runUrl;
1178
- output.log(`Run: ${event.runUrl}`);
1179
- output.log(`Experiment: ${event.experimentId}`);
1180
- try {
1181
- let after;
1182
- do {
1183
- const page = await client.listExperimentItems(event.experimentId, { after });
1184
- for (const item of page.items)
1185
- caseKeys.set(item.id, item.externalKey);
1186
- after = page.nextCursor ?? undefined;
1187
- } while (after);
1325
+ agentRevision: agent.revision,
1326
+ // The CLI adapts to whatever the deployment serves; the library warning is for code that
1327
+ // still reads the legacy capability itself.
1328
+ deprecationWarnings: false,
1329
+ signal,
1330
+ target: agents.simulation,
1331
+ ...(judging
1332
+ ? {
1333
+ onUploaded: (report) => checkpointJudges(judging, report, { kind: "attempt" }),
1188
1334
  }
1189
- catch {
1190
- // Progress labels fall back to case IDs; the run itself is unaffected.
1335
+ : {}),
1336
+ async onProgress(event) {
1337
+ if (event.type === "run_created") {
1338
+ runUrl = event.runUrl;
1339
+ output.log(`Run: ${event.runUrl}`);
1340
+ output.log(`Experiment: ${event.experimentId}`);
1341
+ try {
1342
+ let after;
1343
+ do {
1344
+ const page = await client.listExperimentItems(event.experimentId, { after });
1345
+ for (const item of page.items)
1346
+ caseKeys.set(item.id, item.externalKey);
1347
+ after = page.nextCursor ?? undefined;
1348
+ } while (after);
1349
+ }
1350
+ catch {
1351
+ // Progress labels fall back to case IDs; the run itself is unaffected.
1352
+ }
1353
+ return;
1191
1354
  }
1192
- return;
1193
- }
1194
- const label = caseKeys.get(event.caseId) ?? event.caseId;
1195
- if (event.type === "world_created")
1196
- output.log(`[${label}] world created`);
1197
- else if (event.type === "target_started")
1198
- output.log(`[${label}] agent started`);
1199
- else if (event.type === "world_sealed")
1200
- output.log(`[${label}] world sealed`);
1201
- else if (event.type === "attempt_prepared")
1202
- output.log(`[${label}] attempt prepared: ${event.status}`);
1203
- },
1204
- });
1205
- telemetry.rest(report.telemetryNotAccepted);
1206
- return reportVerdicts(client, values, {
1207
- experimentId: report.experimentId,
1208
- subjectIds: report.subjectIds,
1209
- runUrl: runUrl || report.runUrl,
1210
- telemetryNotAccepted: report.telemetryNotAccepted,
1211
- }, baselineId, output, signal);
1355
+ const label = caseKeys.get(event.caseId) ?? event.caseId;
1356
+ if (event.type === "world_created")
1357
+ output.log(`[${label}] world created`);
1358
+ else if (event.type === "target_started")
1359
+ output.log(`[${label}] agent started`);
1360
+ else if (event.type === "world_sealed")
1361
+ output.log(`[${label}] world sealed`);
1362
+ else if (event.type === "attempt_prepared")
1363
+ output.log(`[${label}] attempt prepared: ${event.status}`);
1364
+ },
1365
+ });
1366
+ telemetry.rest(report.telemetryNotAccepted);
1367
+ return await reportVerdicts(client, values, {
1368
+ experimentId: report.experimentId,
1369
+ subjectIds: report.subjectIds,
1370
+ runUrl: runUrl || report.runUrl,
1371
+ telemetryNotAccepted: report.telemetryNotAccepted,
1372
+ }, baselineId, output, signal, judging);
1373
+ }
1374
+ finally {
1375
+ await judging?.store.release();
1376
+ }
1212
1377
  }
1213
1378
  /**
1214
1379
  * `--check`: everything a one-shot run resolves before it creates anything (the key's project,
@@ -1352,6 +1517,14 @@ async function runDirect(run, connection) {
1352
1517
  return result;
1353
1518
  },
1354
1519
  });
1520
+ if (run.judging)
1521
+ await checkpointJudges(run.judging, {
1522
+ experimentId,
1523
+ subjectIds: report.subjectIds,
1524
+ runUrl,
1525
+ extra: { mode: "direct", deferredScorerVersionIds: report.deferredScorerVersionIds },
1526
+ telemetryNotAccepted: report.telemetryNotAccepted,
1527
+ }, { kind: "attempt" });
1355
1528
  attempt.stage = "completed";
1356
1529
  await store.write("active-attempt", attempt);
1357
1530
  }
@@ -1367,126 +1540,199 @@ async function runDirect(run, connection) {
1367
1540
  runUrl,
1368
1541
  extra: { mode: "direct", deferredScorerVersionIds: report.deferredScorerVersionIds },
1369
1542
  telemetryNotAccepted: report.telemetryNotAccepted,
1370
- }, run.baselineId, output, signal);
1543
+ }, run.baselineId, output, signal, run.judging);
1371
1544
  }
1372
1545
  async function runWorker(values, connection, agents, agent, hue, output, signal) {
1373
1546
  const client = new ObservedClient(connection);
1374
1547
  const environmentClient = createEnvironmentClient(connection);
1375
1548
  const wait = integer("wait", values.wait, 300, 0, 86_400);
1376
1549
  const concurrency = integer("concurrency", values.concurrency, 1, 1, 100);
1377
- const maxRuns = values["max-runs"] === undefined
1550
+ let maxRuns = values["max-runs"] === undefined
1378
1551
  ? undefined
1379
1552
  : integer("max-runs", values["max-runs"], 1, 1, 1_000_000);
1380
1553
  const project = await client.checkConnection();
1381
1554
  const checkpointDirectory = await prepareCheckpointDirectory(values["checkpoint-dir"], agent.key, project.id, "worker", slug(agent.revision) || "dev");
1382
- let current;
1383
- client.onRegistered = (registered) => {
1384
- output.log(`Registered agent ${registered.key} (revision ${registered.revision}) with ${connection.baseUrl}; waiting for runs launched from Hue${maxRuns ? ` (stops after ${maxRuns})` : " (Ctrl+C to stop)"}`);
1385
- };
1386
- client.onClaimed = (claim) => {
1387
- current = claim;
1388
- output.log(`Claimed run ${claim.runId}: ${new URL(`/runs/${claim.experimentId}`, connection.baseUrl).toString()}`);
1555
+ const judging = values.judges
1556
+ ? {
1557
+ store: await CheckpointStore.acquire(checkpointPath(checkpointDirectory, ["judge-dispatch", "judge checkpoint"]), {
1558
+ kind: "cli-judge-dispatch",
1559
+ projectId: project.id,
1560
+ baseUrl: connection.baseUrl,
1561
+ }),
1562
+ selectionDigest: digest(agent),
1563
+ source: {
1564
+ directory: checkpointDirectory,
1565
+ identity: {
1566
+ kind: "local-agent-worker",
1567
+ projectId: project.id,
1568
+ baseUrl: connection.baseUrl,
1569
+ agent: { key: agent.key, revision: agent.revision },
1570
+ },
1571
+ },
1572
+ }
1573
+ : undefined;
1574
+ const reportJudgedRun = async (run) => {
1575
+ if (!judging)
1576
+ throw new Error("Hosted judge recovery checkpoint is unavailable");
1577
+ await requestRunJudges(client, run, output, signal, judging);
1578
+ try {
1579
+ await readAndReportVerdicts(client, values, run, undefined, output, signal);
1580
+ }
1581
+ catch (error) {
1582
+ output.error(`Unable to read verdicts: ${explain(error)}`);
1583
+ }
1389
1584
  };
1390
- const telemetry = telemetryReporter(output);
1391
- // A command can use the same stdin contract for both case kinds. Existing adapter files
1392
- // may require a world, so their direct context remains an explicit capability opt-in.
1393
- const direct = values.command !== undefined || values.capability?.includes("direct:v1");
1394
- await runLocalAgent({
1395
- client,
1396
- environmentClient,
1397
- hue,
1398
- checkpointDirectory,
1399
- agent: {
1400
- key: agent.key,
1401
- name: agent.name,
1402
- revision: agent.revision,
1403
- capabilities: ["environment:v1", ...(values.capability ?? [])],
1404
- },
1405
- scorers: [],
1406
- concurrency,
1407
- traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
1408
- onTelemetryNotAccepted: telemetry.report,
1409
- deprecationWarnings: false,
1410
- signal,
1411
- ...(maxRuns === undefined ? {} : { maxRuns }),
1412
- onPollError(error) {
1413
- output.error(`Hue did not answer the poll (${explain(error)}); polling again`);
1414
- },
1415
- onRunFailed(failure) {
1416
- const run = `Run ${failure.runId}`;
1417
- if (failure.outcome === "retry") {
1418
- const waitMillis = failure.waitMillis ?? 0;
1419
- output.error(`${run} attempt ${failure.attempt} failed (${explain(failure.error)}); resuming in ${Math.round(waitMillis / 1000)} s`);
1585
+ try {
1586
+ const pending = await judging?.store.read("pending");
1587
+ if (pending) {
1588
+ if (pending.selectionDigest !== judging.selectionDigest)
1589
+ throw new Error("Recover the pending judge dispatch with the unchanged command or use another --checkpoint-dir");
1590
+ output.log(`Resuming hosted checks: ${pending.run.runUrl}`);
1591
+ if (await completeJudgeHandoff(client, judging, pending)) {
1592
+ await reportJudgedRun(pending.run);
1420
1593
  }
1421
- else if (failure.outcome === "attention")
1422
- output.error(`${run} needs attention (${explain(failure.error)}); requeue or cancel it from its run page`);
1423
- else
1424
- output.error(`${run} was released or cancelled in Hue; its outcome here is not recorded`);
1425
- },
1426
- target(inputs, tools, context) {
1427
- output.log(`[${context.item.externalKey}] agent started`);
1428
- return agents.simulation(inputs, {
1429
- config: context.config,
1430
- item: context.item,
1431
- executionId: context.executionId,
1432
- environmentRunId: context.environmentRunId,
1433
- tools,
1434
- ...(context.world ? { world: context.world } : {}),
1435
- ...(context.mcp ? { mcp: context.mcp } : {}),
1436
- ...(context.connectionBundle ? { connectionBundle: context.connectionBundle } : {}),
1437
- files: context.files,
1438
- outputDirectory: context.outputDirectory,
1439
- // The case's own signal: the worker stopping, or the world's deadline passing, both
1440
- // end a command that would otherwise keep running past its timed-out case.
1441
- signal: context.signal ?? signal,
1442
- });
1443
- },
1444
- ...(direct
1445
- ? {
1446
- directTarget(inputs, context) {
1447
- output.log(`[${context.item.externalKey}] agent started`);
1448
- return agents.direct(inputs, {
1449
- mode: "direct",
1450
- config: context.config,
1451
- item: context.item,
1452
- executionId: context.executionId,
1453
- files: context.files,
1454
- outputDirectory: context.outputDirectory,
1455
- signal: context.signal ?? signal,
1456
- });
1457
- },
1594
+ else if (pending.completion.kind === "worker") {
1595
+ await discardWorkerJudgeHandoff(judging, pending.completion.runId);
1596
+ output.error(`Run ${pending.completion.runId} was released or cancelled in Hue`);
1458
1597
  }
1459
- : {}),
1460
- async onCompleted(report) {
1461
- output.log(`Run ${report.runId} completed: ${report.subjectIds.length} case${report.subjectIds.length === 1 ? "" : "s"}`);
1462
- telemetry.rest(report.telemetryNotAccepted);
1463
- if (!current)
1464
- return;
1465
- output.log("Waiting for Hue checks...");
1466
- try {
1467
- const verdicts = withTelemetryFailures(await collectExperimentVerdicts(client, {
1468
- experimentId: current.experimentId,
1469
- subjectIds: report.subjectIds,
1470
- timeoutMillis: wait * 1000,
1471
- signal,
1472
- }), report.telemetryNotAccepted);
1473
- if (signal.aborted)
1474
- return;
1475
- if (values.json)
1476
- process.stdout.write(`${JSON.stringify(toJson(verdicts, new URL(`/runs/${current.experimentId}`, connection.baseUrl).toString(), undefined, {}, report.telemetryNotAccepted))}\n`);
1598
+ if (signal.aborted)
1599
+ return 130;
1600
+ if (maxRuns !== undefined && --maxRuns === 0)
1601
+ return 0;
1602
+ }
1603
+ let current;
1604
+ client.onRegistered = (registered) => {
1605
+ output.log(`Registered agent ${registered.key} (revision ${registered.revision}) with ${connection.baseUrl}; waiting for runs launched from Hue${maxRuns ? ` (stops after ${maxRuns})` : " (Ctrl+C to stop)"}`);
1606
+ };
1607
+ client.onClaimed = (claim) => {
1608
+ current = claim;
1609
+ output.log(`Claimed run ${claim.runId}: ${new URL(`/runs/${claim.experimentId}`, connection.baseUrl).toString()}`);
1610
+ };
1611
+ const telemetry = telemetryReporter(output);
1612
+ // A command can use the same stdin contract for both case kinds. Existing adapter files
1613
+ // may require a world, so their direct context remains an explicit capability opt-in.
1614
+ const direct = values.command !== undefined || values.capability?.includes("direct:v1");
1615
+ await runLocalAgent({
1616
+ client,
1617
+ environmentClient,
1618
+ hue,
1619
+ checkpointDirectory,
1620
+ agent: {
1621
+ key: agent.key,
1622
+ name: agent.name,
1623
+ revision: agent.revision,
1624
+ capabilities: ["environment:v1", ...(values.capability ?? [])],
1625
+ },
1626
+ scorers: [],
1627
+ concurrency,
1628
+ traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
1629
+ onTelemetryNotAccepted: telemetry.report,
1630
+ deprecationWarnings: false,
1631
+ signal,
1632
+ ...(maxRuns === undefined ? {} : { maxRuns }),
1633
+ onPollError(error) {
1634
+ output.error(`Hue did not answer the poll (${explain(error)}); polling again`);
1635
+ },
1636
+ async onRunFailed(failure) {
1637
+ if (judging && failure.outcome !== "retry")
1638
+ await discardWorkerJudgeHandoff(judging, failure.runId);
1639
+ const run = `Run ${failure.runId}`;
1640
+ if (failure.outcome === "retry") {
1641
+ const waitMillis = failure.waitMillis ?? 0;
1642
+ output.error(`${run} attempt ${failure.attempt} failed (${explain(failure.error)}); resuming in ${Math.round(waitMillis / 1000)} s`);
1643
+ }
1644
+ else if (failure.outcome === "attention")
1645
+ output.error(`${run} needs attention (${explain(failure.error)}); requeue or cancel it from its run page`);
1477
1646
  else
1478
- renderTable(verdicts, output);
1479
- }
1480
- catch (error) {
1481
- output.error(`Unable to read verdicts: ${explain(error)}`);
1482
- }
1483
- },
1484
- });
1485
- if (signal.aborted) {
1486
- process.stderr.write("Interrupted.\n");
1487
- return 130;
1647
+ output.error(`${run} was released or cancelled in Hue; its outcome here is not recorded`);
1648
+ },
1649
+ target(inputs, tools, context) {
1650
+ output.log(`[${context.item.externalKey}] agent started`);
1651
+ return agents.simulation(inputs, {
1652
+ config: context.config,
1653
+ item: context.item,
1654
+ executionId: context.executionId,
1655
+ environmentRunId: context.environmentRunId,
1656
+ tools,
1657
+ ...(context.world ? { world: context.world } : {}),
1658
+ ...(context.mcp ? { mcp: context.mcp } : {}),
1659
+ ...(context.connectionBundle ? { connectionBundle: context.connectionBundle } : {}),
1660
+ files: context.files,
1661
+ outputDirectory: context.outputDirectory,
1662
+ // The case's own signal: the worker stopping, or the world's deadline passing, both
1663
+ // end a command that would otherwise keep running past its timed-out case.
1664
+ signal: context.signal ?? signal,
1665
+ });
1666
+ },
1667
+ ...(direct
1668
+ ? {
1669
+ directTarget(inputs, context) {
1670
+ output.log(`[${context.item.externalKey}] agent started`);
1671
+ return agents.direct(inputs, {
1672
+ mode: "direct",
1673
+ config: context.config,
1674
+ item: context.item,
1675
+ executionId: context.executionId,
1676
+ files: context.files,
1677
+ outputDirectory: context.outputDirectory,
1678
+ signal: context.signal ?? signal,
1679
+ });
1680
+ },
1681
+ }
1682
+ : {}),
1683
+ ...(judging
1684
+ ? {
1685
+ onUploaded: (report, claim) => checkpointJudges(judging, {
1686
+ experimentId: claim.experimentId,
1687
+ subjectIds: report.subjectIds,
1688
+ runUrl: new URL(`/runs/${claim.experimentId}`, connection.baseUrl).toString(),
1689
+ telemetryNotAccepted: report.telemetryNotAccepted,
1690
+ }, { kind: "worker", runId: claim.runId, workerId: claim.workerId }),
1691
+ }
1692
+ : {}),
1693
+ async onCompleted(report) {
1694
+ output.log(`Run ${report.runId} completed: ${report.subjectIds.length} case${report.subjectIds.length === 1 ? "" : "s"}`);
1695
+ telemetry.rest(report.telemetryNotAccepted);
1696
+ if (!current)
1697
+ return;
1698
+ if (values.judges) {
1699
+ await reportJudgedRun({
1700
+ experimentId: current.experimentId,
1701
+ subjectIds: report.subjectIds,
1702
+ runUrl: new URL(`/runs/${current.experimentId}`, connection.baseUrl).toString(),
1703
+ telemetryNotAccepted: report.telemetryNotAccepted,
1704
+ });
1705
+ return;
1706
+ }
1707
+ try {
1708
+ output.log("Waiting for Hue checks...");
1709
+ const verdicts = withTelemetryFailures(await collectExperimentVerdicts(client, {
1710
+ experimentId: current.experimentId,
1711
+ subjectIds: report.subjectIds,
1712
+ timeoutMillis: wait * 1000,
1713
+ signal,
1714
+ }), report.telemetryNotAccepted);
1715
+ if (signal.aborted)
1716
+ return;
1717
+ if (values.json)
1718
+ process.stdout.write(`${JSON.stringify(toJson(verdicts, new URL(`/runs/${current.experimentId}`, connection.baseUrl).toString(), undefined, {}, report.telemetryNotAccepted))}\n`);
1719
+ else
1720
+ renderTable(verdicts, output);
1721
+ }
1722
+ catch (error) {
1723
+ output.error(`Unable to read verdicts: ${explain(error)}`);
1724
+ }
1725
+ },
1726
+ });
1727
+ if (signal.aborted) {
1728
+ process.stderr.write("Interrupted.\n");
1729
+ return 130;
1730
+ }
1731
+ return 0;
1732
+ }
1733
+ finally {
1734
+ await judging?.store.release();
1488
1735
  }
1489
- return 0;
1490
1736
  }
1491
1737
  /**
1492
1738
  * `hue eval`: runs a local adapter or command against a Scenario or eval set through
@@ -1529,6 +1775,8 @@ export async function runEvalCommand(argv) {
1529
1775
  worker = values.worker;
1530
1776
  if (values.content && values["no-content"])
1531
1777
  throw new UsageError("Choose either --content or --no-content");
1778
+ if (values.judges && values["no-output"])
1779
+ throw new UsageError("--judges requires stored case outputs; remove --no-output");
1532
1780
  // A usage error before anything is prepared or created: no run is left to recover from it.
1533
1781
  traceNotAcceptedPolicy(values["trace-not-accepted"]);
1534
1782
  json = values.json;