@hue-run/sdk 0.19.0 → 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.
- package/CLI.md +27 -10
- package/ENVIRONMENTS.md +5 -2
- package/EVALUATIONS.md +6 -1
- package/README.md +12 -4
- package/WORLDS.md +122 -0
- package/dist/blobs.d.ts +2 -1
- package/dist/blobs.js +7 -5
- package/dist/cli/eval.d.ts +4 -0
- package/dist/cli/eval.js +434 -186
- package/dist/cli/listen.js +1 -1
- package/dist/environment/client.d.ts +2 -95
- package/dist/environment/client.js +2 -283
- package/dist/environment/tools.d.ts +2 -35
- package/dist/environment/tools.js +2 -30
- package/dist/environment/types.d.ts +2 -623
- package/dist/environment/types.js +2 -1
- package/dist/environment/world.d.ts +2 -65
- package/dist/environment/world.js +2 -171
- package/dist/environment.d.ts +7 -7
- package/dist/environment.js +3 -3
- package/dist/evals/attempt.d.ts +1 -1
- package/dist/evals/client.d.ts +3 -2
- package/dist/evals/client.js +2 -1
- package/dist/evals/environment-target.d.ts +3 -3
- package/dist/evals/environment-target.js +3 -3
- package/dist/evals/failure.js +1 -1
- package/dist/evals/local-worker.d.ts +8 -3
- package/dist/evals/local-worker.js +10 -2
- package/dist/evals/simulation.d.ts +6 -3
- package/dist/evals/simulation.js +2 -1
- package/dist/evals/types.d.ts +2 -2
- package/dist/inline-files.d.ts +21 -4
- package/dist/inline-files.js +100 -14
- package/dist/large-media.d.ts +2 -1
- package/dist/large-media.js +7 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/world/client.d.ts +150 -0
- package/dist/world/client.js +368 -0
- package/dist/world/definitions.d.ts +47 -0
- package/dist/world/definitions.js +1 -0
- package/dist/world/model-types.d.ts +117 -0
- package/dist/world/model-types.js +1 -0
- package/dist/world/models.d.ts +40 -0
- package/dist/world/models.js +56 -0
- package/dist/world/tools.d.ts +35 -0
- package/dist/world/tools.js +30 -0
- package/dist/world/types.d.ts +625 -0
- package/dist/world/types.js +1 -0
- package/dist/world/world.d.ts +65 -0
- package/dist/world/world.js +175 -0
- package/dist/world.d.ts +13 -0
- package/dist/world.js +5 -0
- 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 "../
|
|
11
|
-
import { agentEnvironment, isHueControlPlaneCredential, stripHueControlPlaneCredentials, withoutWorldVariables, writeMcpConfig, } from "../
|
|
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
|
-
/**
|
|
1058
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1134
|
-
pins,
|
|
1135
|
-
runName,
|
|
1136
|
-
projectId: project.id,
|
|
1137
|
-
agent,
|
|
1138
|
-
agents,
|
|
1308
|
+
environmentClient,
|
|
1139
1309
|
hue,
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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 (
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
output.
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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;
|