@hue-run/sdk 0.20.2 → 0.20.3
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 +10 -10
- package/EVALUATIONS.md +3 -1
- package/README.md +2 -2
- package/dist/cli/eval.js +90 -34
- package/dist/evals/runner.d.ts +5 -2
- package/dist/evals/runner.js +31 -5
- package/dist/evals/simulation.d.ts +4 -0
- package/dist/evals/simulation.js +24 -26
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
|
|
|
42
42
|
The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
|
|
43
43
|
`capture_content=False`. For a supported application, setup installs the dependency and adds the
|
|
44
44
|
managed import and middleware registration to the existing entrypoint; an unreferenced helper is
|
|
45
|
-
not a completed integration. TypeScript uses `@hue-run/sdk@0.20.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.20.3`, `@opentelemetry/api@1.9.1` and
|
|
46
46
|
`@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
|
|
47
47
|
Content capture requires an ordinary account-managed key and a later explicit application decision.
|
|
48
48
|
|
|
@@ -381,8 +381,8 @@ and Hue never receives or stores them. The command is part of the `hue` executab
|
|
|
381
381
|
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
|
382
382
|
# LANGFUSE_SECRET_KEY=sk-lf-...
|
|
383
383
|
# LANGFUSE_BASE_URL=https://cloud.langfuse.com
|
|
384
|
-
npx -y @hue-run/sdk@0.20.
|
|
385
|
-
npx -y @hue-run/sdk@0.20.
|
|
384
|
+
npx -y @hue-run/sdk@0.20.3 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
385
|
+
npx -y @hue-run/sdk@0.20.3 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
386
386
|
```
|
|
387
387
|
|
|
388
388
|
`LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` come from the environment or `--env-path`
|
|
@@ -687,7 +687,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
687
687
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
688
688
|
project=/absolute/path/to/supported-fixture
|
|
689
689
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
690
|
-
--archive .artifacts/typescript/hue-run-sdk-0.20.
|
|
690
|
+
--archive .artifacts/typescript/hue-run-sdk-0.20.3.tgz \
|
|
691
691
|
--origin https://STAGING_ORIGIN \
|
|
692
692
|
--project "$project" --command setup \
|
|
693
693
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -698,7 +698,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
698
698
|
|
|
699
699
|
```sh
|
|
700
700
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
701
|
-
--archive .artifacts/typescript/hue-run-sdk-0.20.
|
|
701
|
+
--archive .artifacts/typescript/hue-run-sdk-0.20.3.tgz \
|
|
702
702
|
--origin https://STAGING_ORIGIN \
|
|
703
703
|
--project "$project" --command claim \
|
|
704
704
|
--evidence .context/setup-staging-after-claim.json
|
|
@@ -740,10 +740,10 @@ otherwise. Never hand the agent Hue-native tools (`context.tools`, `context.mcp`
|
|
|
740
740
|
production agent lacks. Pin the SDK, so the agent's repository gains no dependency:
|
|
741
741
|
|
|
742
742
|
```sh
|
|
743
|
-
npx -y @hue-run/sdk@0.20.
|
|
744
|
-
npx -y @hue-run/sdk@0.20.
|
|
745
|
-
npx -y @hue-run/sdk@0.20.
|
|
746
|
-
npx -y @hue-run/sdk@0.20.
|
|
743
|
+
npx -y @hue-run/sdk@0.20.3 eval --case "Refund an eligible charge" --command "node agent.js"
|
|
744
|
+
npx -y @hue-run/sdk@0.20.3 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
|
|
745
|
+
npx -y @hue-run/sdk@0.20.3 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
|
|
746
|
+
npx -y @hue-run/sdk@0.20.3 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
|
|
747
747
|
```
|
|
748
748
|
|
|
749
749
|
One invocation creates one run holding every selected case; it prints the run URL
|
|
@@ -781,7 +781,7 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
|
|
|
781
781
|
```
|
|
782
782
|
|
|
783
783
|
```sh
|
|
784
|
-
npx -y @hue-run/sdk@0.20.
|
|
784
|
+
npx -y @hue-run/sdk@0.20.3 eval --case "Refund an eligible charge" ./hue-agent.ts --content
|
|
785
785
|
```
|
|
786
786
|
|
|
787
787
|
`--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
|
package/EVALUATIONS.md
CHANGED
|
@@ -362,7 +362,9 @@ Python SDK's `run_experiment` and `rescore` hand pinned input files to targets a
|
|
|
362
362
|
generated files and the outbound worker are TypeScript-only.
|
|
363
363
|
|
|
364
364
|
Since TypeScript 0.13.2, the `hue eval --worker --command "…"` wrapper supplies both callbacks, so one
|
|
365
|
-
command worker accepts direct and world cases, including mixed eval sets.
|
|
365
|
+
command worker accepts direct and world cases, including mixed eval sets. Since 0.20.3, a one-shot
|
|
366
|
+
`hue eval --set` over such a set runs it as one `mixed` run: each case takes its own kind's path
|
|
367
|
+
under one experiment and its own checkpoint identity (`--mode simulation` over it is refused). It keeps the launch
|
|
366
368
|
directory for relative application entry points; private case inputs/output still use
|
|
367
369
|
`HUE_CASE_*`. An adapter-file worker opts into the direct callback with `--capability direct:v1`
|
|
368
370
|
and must handle `context.mode === "direct"`. Restart an existing environment-only command
|
package/README.md
CHANGED
|
@@ -536,8 +536,8 @@ the Langfuse project's own keys (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, a
|
|
|
536
536
|
`HUE_API_KEY`:
|
|
537
537
|
|
|
538
538
|
```sh
|
|
539
|
-
npx -y @hue-run/sdk@0.20.
|
|
540
|
-
npx -y @hue-run/sdk@0.20.
|
|
539
|
+
npx -y @hue-run/sdk@0.20.3 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
540
|
+
npx -y @hue-run/sdk@0.20.3 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
541
541
|
```
|
|
542
542
|
|
|
543
543
|
- It reads Langfuse Cloud and self-hosted v4 through the Observations API v2, and self-hosted v3
|
package/dist/cli/eval.js
CHANGED
|
@@ -20,7 +20,8 @@ import { describeTelemetryIssues, runExperiment, TargetCancelledError, TELEMETRY
|
|
|
20
20
|
import { HueExportError } from "../transport.js";
|
|
21
21
|
import { matchByName, parseScenarioSelector, resolveEvalSetPins, resolveScenarioPins, } from "../evals/scenarios.js";
|
|
22
22
|
import { collectDirectOutputs, stageDirectCase } from "./eval-direct.js";
|
|
23
|
-
import {
|
|
23
|
+
import { runEnvironmentTarget } from "../evals/environment-target.js";
|
|
24
|
+
import { runSimulation, simulationTargetContext, } from "../evals/simulation.js";
|
|
24
25
|
import { collectExperimentVerdicts, compareVerdicts, metricPassed, } from "../evals/verdicts.js";
|
|
25
26
|
import { DEFAULT_ENV_FILE, envFileArgument, envFileOptions } from "./env-file.js";
|
|
26
27
|
const USAGE = `Usage: hue eval [adapter-file] [options]
|
|
@@ -35,7 +36,9 @@ Selection (exactly one, not used with --worker):
|
|
|
35
36
|
--dataset-version <id> Frozen dataset version; requires --scorer or --scorer-version
|
|
36
37
|
--scorer <slug|name|id> Evaluator to pin at its latest published version (repeatable)
|
|
37
38
|
--scorer-version <id> Scorer version to pin (repeatable)
|
|
38
|
-
--mode <auto|direct|simulation> Case kind; auto picks direct for sets whose cases pin no
|
|
39
|
+
--mode <auto|direct|simulation> Case kind; auto picks direct for sets whose cases pin no
|
|
40
|
+
world and runs a set mixing both kinds as one run, each
|
|
41
|
+
case through its own kind's path
|
|
39
42
|
|
|
40
43
|
Agent (exactly one):
|
|
41
44
|
<adapter-file> Module exporting default or runMyAgent(inputs, context)
|
|
@@ -979,9 +982,11 @@ function parseMode(value) {
|
|
|
979
982
|
return value;
|
|
980
983
|
throw new UsageError("--mode must be auto, direct or simulation");
|
|
981
984
|
}
|
|
982
|
-
/** The selection's cases
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
+
/** The selection's cases and how they run. When `--mode` leaves it open, a version mixing cases
|
|
986
|
+
* that pin a world with cases that pin none runs as one mixed run; direct means nothing pins a
|
|
987
|
+
* simulated world, not a Scenario and no case of the version; anything else is a simulation. A
|
|
988
|
+
* simulation run cannot execute a case that pins no world, so `--mode simulation` over such
|
|
989
|
+
* cases is refused. */
|
|
985
990
|
async function inspectSelection(client, pins, mode) {
|
|
986
991
|
let cases = 0;
|
|
987
992
|
let worldCases = 0;
|
|
@@ -992,14 +997,18 @@ async function inspectSelection(client, pins, mode) {
|
|
|
992
997
|
worldCases += page.items.filter((item) => item.environmentVersionId).length;
|
|
993
998
|
after = page.nextCursor ?? undefined;
|
|
994
999
|
} while (after);
|
|
995
|
-
const direct = mode === "auto"
|
|
996
|
-
? !pins.scenarioId && !pins.environmentVersionId && worldCases === 0
|
|
997
|
-
: mode === "direct";
|
|
998
1000
|
const answerOnly = cases - worldCases;
|
|
999
|
-
const
|
|
1000
|
-
?
|
|
1001
|
+
const kind = mode !== "auto"
|
|
1002
|
+
? mode
|
|
1003
|
+
: worldCases > 0 && answerOnly > 0
|
|
1004
|
+
? "mixed"
|
|
1005
|
+
: worldCases === 0 && !pins.scenarioId && !pins.environmentVersionId
|
|
1006
|
+
? "direct"
|
|
1007
|
+
: "simulation";
|
|
1008
|
+
const refusal = kind === "simulation" && answerOnly > 0
|
|
1009
|
+
? `${answerOnly} of the ${cases} ${cases === 1 ? "case pins" : "cases pin"} no world, so a simulation run would fail ${answerOnly === 1 ? "it" : "them"} before the agent starts. ${mode === "simulation" ? "Drop --mode simulation: --mode auto runs the world cases and the answer-only cases together as one run" : "Put the answer-only cases in their own eval set"}.`
|
|
1001
1010
|
: undefined;
|
|
1002
|
-
return {
|
|
1011
|
+
return { kind, cases, worldCases, ...(refusal ? { refusal } : {}) };
|
|
1003
1012
|
}
|
|
1004
1013
|
function toJson(verdicts, runUrl, baseline, extra = {}, telemetry = []) {
|
|
1005
1014
|
const notAccepted = new Map(telemetry.map((entry) => [entry.caseId, entry.issues]));
|
|
@@ -1255,7 +1264,9 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1255
1264
|
}
|
|
1256
1265
|
const runName = values.name ?? defaultRunName(agent);
|
|
1257
1266
|
const project = await client.checkConnection();
|
|
1258
|
-
|
|
1267
|
+
// Each kind of run has its own checkpoint directory and identity: a mixed run never resumes
|
|
1268
|
+
// from, or is resumed by, a direct or simulation run's checkpoint.
|
|
1269
|
+
const checkpointDirectory = await prepareCheckpointDirectory(values["checkpoint-dir"], agent.key, project.id, selection.kind);
|
|
1259
1270
|
const judging = values.judges
|
|
1260
1271
|
? {
|
|
1261
1272
|
store: await CheckpointStore.acquire(checkpointPath(checkpointDirectory, ["judge-dispatch", "judge checkpoint"]), { kind: "cli-judge-dispatch", projectId: project.id, baseUrl: connection.baseUrl }),
|
|
@@ -1267,7 +1278,7 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1267
1278
|
source: {
|
|
1268
1279
|
directory: checkpointDirectory,
|
|
1269
1280
|
identity: {
|
|
1270
|
-
kind: selection.
|
|
1281
|
+
kind: selection.kind,
|
|
1271
1282
|
projectId: project.id,
|
|
1272
1283
|
baseUrl: connection.baseUrl,
|
|
1273
1284
|
},
|
|
@@ -1283,8 +1294,9 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1283
1294
|
await completeJudgeHandoff(client, judging, pending);
|
|
1284
1295
|
return await reportVerdicts(client, values, pending.run, baselineId, output, signal, judging);
|
|
1285
1296
|
}
|
|
1286
|
-
if (selection.
|
|
1287
|
-
return await
|
|
1297
|
+
if (selection.kind !== "simulation")
|
|
1298
|
+
return await runExperimentAttempt({
|
|
1299
|
+
kind: selection.kind,
|
|
1288
1300
|
client,
|
|
1289
1301
|
values,
|
|
1290
1302
|
pins,
|
|
@@ -1298,6 +1310,7 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1298
1310
|
concurrency,
|
|
1299
1311
|
baselineId,
|
|
1300
1312
|
judging,
|
|
1313
|
+
checkpointDirectory,
|
|
1301
1314
|
}, connection);
|
|
1302
1315
|
const environmentClient = createEnvironmentClient(connection);
|
|
1303
1316
|
const caseKeys = new Map();
|
|
@@ -1393,8 +1406,11 @@ async function checkRun(client, values, pins, selection, agent, output) {
|
|
|
1393
1406
|
return 1;
|
|
1394
1407
|
}
|
|
1395
1408
|
}
|
|
1396
|
-
const {
|
|
1409
|
+
const { kind, cases, worldCases, refusal } = selection;
|
|
1397
1410
|
const unsaved = !pins.saved && !values["save-version"];
|
|
1411
|
+
// What each kind of run gives its cases: a direct run gives no case a world, a simulation
|
|
1412
|
+
// run gives every case one (and is refused over cases that pin none), a mixed run gives one
|
|
1413
|
+
// to each case that pins one.
|
|
1398
1414
|
const check = {
|
|
1399
1415
|
ok: !unsaved && !refusal,
|
|
1400
1416
|
project: { id: project.id, name: project.name },
|
|
@@ -1406,8 +1422,9 @@ async function checkRun(client, values, pins, selection, agent, output) {
|
|
|
1406
1422
|
cases,
|
|
1407
1423
|
evaluators: pins.scorerVersionIds.length,
|
|
1408
1424
|
},
|
|
1409
|
-
mode:
|
|
1410
|
-
worldCases: direct ? 0 : worldCases,
|
|
1425
|
+
mode: kind,
|
|
1426
|
+
worldCases: kind === "direct" ? 0 : worldCases,
|
|
1427
|
+
directCases: kind === "direct" ? cases : cases - worldCases,
|
|
1411
1428
|
agent: { key: agent.key, revision: agent.revision, concurrency: values.concurrency ?? "1" },
|
|
1412
1429
|
};
|
|
1413
1430
|
if (values.json)
|
|
@@ -1415,9 +1432,12 @@ async function checkRun(client, values, pins, selection, agent, output) {
|
|
|
1415
1432
|
else {
|
|
1416
1433
|
output.log(`Project: ${project.name} (${project.id})`);
|
|
1417
1434
|
output.log(`Selection: ${check.selection.kind === "case" ? "case" : "eval set"} "${pins.name}", ${cases} ${cases === 1 ? "case" : "cases"}, ${pins.scorerVersionIds.length} ${pins.scorerVersionIds.length === 1 ? "evaluator" : "evaluators"}`);
|
|
1418
|
-
|
|
1435
|
+
const worlds = `${worldCases} ${worldCases === 1 ? "case gets" : "cases get"} a fresh world, reached through the HUE_SIM_*_URL mirrors and HUE_WORLD_TOKEN`;
|
|
1436
|
+
output.log(kind === "direct"
|
|
1419
1437
|
? "Mode: direct; the agent answers without a simulated world or app mirrors"
|
|
1420
|
-
:
|
|
1438
|
+
: kind === "simulation"
|
|
1439
|
+
? `Mode: simulation; ${worlds}`
|
|
1440
|
+
: `Mode: mixed; ${worlds}, and ${check.directCases} ${check.directCases === 1 ? "case answers" : "cases answer"} without one`);
|
|
1421
1441
|
output.log(`Agent: ${agent.key} @ ${agent.revision}`);
|
|
1422
1442
|
}
|
|
1423
1443
|
if (refusal) {
|
|
@@ -1432,21 +1452,26 @@ async function checkRun(client, values, pins, selection, agent, output) {
|
|
|
1432
1452
|
return 0;
|
|
1433
1453
|
}
|
|
1434
1454
|
/**
|
|
1435
|
-
*
|
|
1436
|
-
* pinned files and returns generated documents
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1455
|
+
* One ordinary experiment through `runExperiment`, for a direct run and for a mixed one. A
|
|
1456
|
+
* direct run's agent receives the case's pinned files and returns generated documents. A mixed
|
|
1457
|
+
* run routes each case by its pin: a case pinning no world goes to the direct adapter, a case
|
|
1458
|
+
* pinning one gets a fresh world through the environment target and the simulation adapter, as
|
|
1459
|
+
* a simulation run gives it. Code evaluators pinned to the run stay deferred for Hue's executor
|
|
1460
|
+
* (`deferUnboundLocalScorers`), so no evaluator source runs on this machine. The attempt is
|
|
1461
|
+
* checkpointed as in simulation mode, under the run kind's own identity: rerunning the same
|
|
1462
|
+
* selection resumes the saved experiment without invoking the agent again for its finished
|
|
1463
|
+
* cases.
|
|
1440
1464
|
*/
|
|
1441
|
-
async function
|
|
1442
|
-
const { client, values, pins, output, signal } = run;
|
|
1465
|
+
async function runExperimentAttempt(run, connection) {
|
|
1466
|
+
const { client, kind, values, pins, output, signal } = run;
|
|
1443
1467
|
const config = { agentKey: run.agent.key, agentRevision: run.agent.revision };
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
kind: "direct",
|
|
1468
|
+
const store = await CheckpointStore.acquire(run.checkpointDirectory, {
|
|
1469
|
+
kind,
|
|
1447
1470
|
projectId: run.projectId,
|
|
1448
1471
|
baseUrl: connection.baseUrl,
|
|
1449
1472
|
});
|
|
1473
|
+
// Only a mixed run creates worlds; its world cases reach Hue through the environment client.
|
|
1474
|
+
const environmentClient = kind === "mixed" ? createEnvironmentClient(connection) : undefined;
|
|
1450
1475
|
let experimentId = "";
|
|
1451
1476
|
let runUrl = "";
|
|
1452
1477
|
let report;
|
|
@@ -1459,7 +1484,7 @@ async function runDirect(run, connection) {
|
|
|
1459
1484
|
});
|
|
1460
1485
|
let attempt = await store.read("active-attempt");
|
|
1461
1486
|
if (attempt && attempt.stage !== "completed" && attempt.selectionDigest !== selectionDigest)
|
|
1462
|
-
throw new Error(
|
|
1487
|
+
throw new Error(`Recover the unfinished ${kind} run before running a changed selection: run the unchanged command again to finish it, or pass another --checkpoint-dir`);
|
|
1463
1488
|
if (!attempt || attempt.stage === "completed") {
|
|
1464
1489
|
attempt = { selectionDigest, idempotencyKey: randomUUID(), stage: "preparing" };
|
|
1465
1490
|
await store.write("active-attempt", attempt);
|
|
@@ -1496,10 +1521,41 @@ async function runDirect(run, connection) {
|
|
|
1496
1521
|
scorers: [],
|
|
1497
1522
|
deferUnboundLocalScorers: true,
|
|
1498
1523
|
environmentEvidence: "when_pinned",
|
|
1524
|
+
// A world case that a process which died here left running runs again in a fresh world;
|
|
1525
|
+
// an answer-only case it left running stays uncertain, since its agent may still be
|
|
1526
|
+
// acting. A direct run gives no case a world, so none of its cases reruns.
|
|
1527
|
+
...(environmentClient
|
|
1528
|
+
? { rerunCrashedCases: (item) => Boolean(item.environmentVersionId) }
|
|
1529
|
+
: {}),
|
|
1499
1530
|
async target(inputs, context) {
|
|
1500
1531
|
if (signal.aborted)
|
|
1501
1532
|
throw new TargetCancelledError();
|
|
1502
|
-
|
|
1533
|
+
const label = context.item.externalKey;
|
|
1534
|
+
if (environmentClient && context.item.environmentVersionId)
|
|
1535
|
+
return runEnvironmentTarget({
|
|
1536
|
+
client,
|
|
1537
|
+
environmentClient,
|
|
1538
|
+
hue: run.hue,
|
|
1539
|
+
inputs,
|
|
1540
|
+
context,
|
|
1541
|
+
agentRevision: run.agent.revision,
|
|
1542
|
+
// The CLI adapts to whatever the deployment serves; the library warning is for
|
|
1543
|
+
// code that still reads the legacy capability itself.
|
|
1544
|
+
deprecationWarnings: false,
|
|
1545
|
+
signal,
|
|
1546
|
+
onProgress(event) {
|
|
1547
|
+
if (event.type === "world_created")
|
|
1548
|
+
output.log(`[${label}] world created`);
|
|
1549
|
+
else if (event.type === "target_started")
|
|
1550
|
+
output.log(`[${label}] agent started`);
|
|
1551
|
+
else if (event.type === "world_sealed")
|
|
1552
|
+
output.log(`[${label}] world sealed`);
|
|
1553
|
+
else if (event.type === "attempt_prepared")
|
|
1554
|
+
output.log(`[${label}] attempt prepared: ${event.status}`);
|
|
1555
|
+
},
|
|
1556
|
+
target: (targetInputs, targetContext) => run.agents.simulation(structuredClone(targetInputs), simulationTargetContext(targetContext)),
|
|
1557
|
+
});
|
|
1558
|
+
output.log(`[${label}] agent started`);
|
|
1503
1559
|
const result = await run.agents.direct(inputs, {
|
|
1504
1560
|
mode: "direct",
|
|
1505
1561
|
config: structuredClone(context.config),
|
|
@@ -1522,7 +1578,7 @@ async function runDirect(run, connection) {
|
|
|
1522
1578
|
experimentId,
|
|
1523
1579
|
subjectIds: report.subjectIds,
|
|
1524
1580
|
runUrl,
|
|
1525
|
-
extra: { mode:
|
|
1581
|
+
extra: { mode: kind, deferredScorerVersionIds: report.deferredScorerVersionIds },
|
|
1526
1582
|
telemetryNotAccepted: report.telemetryNotAccepted,
|
|
1527
1583
|
}, { kind: "attempt" });
|
|
1528
1584
|
attempt.stage = "completed";
|
|
@@ -1538,7 +1594,7 @@ async function runDirect(run, connection) {
|
|
|
1538
1594
|
experimentId,
|
|
1539
1595
|
subjectIds: report.subjectIds,
|
|
1540
1596
|
runUrl,
|
|
1541
|
-
extra: { mode:
|
|
1597
|
+
extra: { mode: kind, deferredScorerVersionIds: report.deferredScorerVersionIds },
|
|
1542
1598
|
telemetryNotAccepted: report.telemetryNotAccepted,
|
|
1543
1599
|
}, run.baselineId, output, signal, run.judging);
|
|
1544
1600
|
}
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -112,8 +112,11 @@ export interface RunExperimentOptions extends RunnerOptions {
|
|
|
112
112
|
signal?: AbortSignal;
|
|
113
113
|
/** Rerun, as a new attempt, a case that a process which died on this machine left running.
|
|
114
114
|
* Only for targets that give every attempt a fresh world: elsewhere the dead run's agent may
|
|
115
|
-
* still be acting, so such a case stays uncertain.
|
|
116
|
-
|
|
115
|
+
* still be acting, so such a case stays uncertain. A predicate decides per frozen case, for a
|
|
116
|
+
* target that gives a fresh world only to the cases that pin one: those rerun, the others stay
|
|
117
|
+
* uncertain. The crash is recorded in the case's checkpoint before the case is read, so a read
|
|
118
|
+
* that fails leaves the decision to the next invocation. */
|
|
119
|
+
rerunCrashedCases?: boolean | ((item: ExperimentCase) => boolean);
|
|
117
120
|
/** Experiment to run; its dataset version must be frozen. */
|
|
118
121
|
experimentId: string;
|
|
119
122
|
/** Whether each case waits for acknowledged trace export or explicitly omits evidence. Required. */
|
package/dist/evals/runner.js
CHANGED
|
@@ -442,6 +442,24 @@ export async function runExperiment(options) {
|
|
|
442
442
|
.map((version) => version.id),
|
|
443
443
|
};
|
|
444
444
|
try {
|
|
445
|
+
// A run that reclaimed a dead process's lock first records that a crash sweep is due, then
|
|
446
|
+
// marks every case that process left running as crashed, before anything else happens and
|
|
447
|
+
// under the same release as the rest. The record outlives this run: a sweep that stops part
|
|
448
|
+
// way (a write the disk refuses) is finished by the next run, which holds no reclaimed lock
|
|
449
|
+
// but reads the record, so each case's recovery decision survives, and a failure further on
|
|
450
|
+
// (a case read Hue refuses stops the run) leaves it to the next run the same way.
|
|
451
|
+
const sweep = "crash-sweep";
|
|
452
|
+
if (store.reclaimed)
|
|
453
|
+
await store.write(sweep, { pending: true });
|
|
454
|
+
if ((await store.read(sweep))?.pending) {
|
|
455
|
+
for (const item of items) {
|
|
456
|
+
const file = `case-${uuid(item.id)}`;
|
|
457
|
+
const checkpoint = await store.read(file).catch(() => undefined);
|
|
458
|
+
if (checkpoint?.stage === "running" && !checkpoint.crashed)
|
|
459
|
+
await store.write(file, { ...checkpoint, crashed: true });
|
|
460
|
+
}
|
|
461
|
+
await store.write(sweep, { pending: false });
|
|
462
|
+
}
|
|
445
463
|
const filesRoot = resolve(options.filesDirectory ?? join(options.checkpointDirectory, "files"));
|
|
446
464
|
const sanitize = (message) => message.slice(0, 4000).toWellFormed().replaceAll("\u0000", "");
|
|
447
465
|
// A target stopped at its world's deadline is a `TargetTimeout`, told apart from an error the
|
|
@@ -539,10 +557,18 @@ export async function runExperiment(options) {
|
|
|
539
557
|
files.keepOutputs = checkpoint?.stage === "uploading";
|
|
540
558
|
if (checkpoint && "attempt" in checkpoint)
|
|
541
559
|
files.directory = worldDirectoryOf(item.id, checkpoint.attempt);
|
|
560
|
+
// The frozen case, read once per case: the crash decision below and the start need it.
|
|
561
|
+
let frozen;
|
|
562
|
+
const frozenCaseOf = async () => (frozen ??= await options.client.getExperimentCase(experiment.id, item.id));
|
|
542
563
|
let retryOf;
|
|
543
|
-
// A case left running by a process that died here
|
|
544
|
-
// stopped; a fresh world isolates it from
|
|
545
|
-
|
|
564
|
+
// A case left running by a process that died here (recorded above, before anything was
|
|
565
|
+
// read) is as interrupted as one the caller stopped; a fresh world isolates it from
|
|
566
|
+
// anything the dead run's agent still does.
|
|
567
|
+
if (checkpoint?.stage === "running" &&
|
|
568
|
+
checkpoint.crashed &&
|
|
569
|
+
options.rerunCrashedCases &&
|
|
570
|
+
(typeof options.rerunCrashedCases !== "function" ||
|
|
571
|
+
options.rerunCrashedCases(structuredClone(await frozenCaseOf())))) {
|
|
546
572
|
checkpoint = { ...checkpoint, stage: "interrupted" };
|
|
547
573
|
await store.write(file, checkpoint);
|
|
548
574
|
}
|
|
@@ -572,7 +598,7 @@ export async function runExperiment(options) {
|
|
|
572
598
|
// second invocation. Metadata-only mode discarded the output, so its outcome is lost.
|
|
573
599
|
if (checkpoint.hasOutput && checkpoint.output === undefined)
|
|
574
600
|
throw new UncertainExecutionError(item.id, checkpoint.executionId);
|
|
575
|
-
const frozenCase = await
|
|
601
|
+
const frozenCase = await frozenCaseOf();
|
|
576
602
|
checkpoint = await prepare(file, checkpoint, frozenCase, caseDirectoryOf(frozenCase), checkpoint.output);
|
|
577
603
|
files.keepOutputs = false;
|
|
578
604
|
}
|
|
@@ -581,7 +607,7 @@ export async function runExperiment(options) {
|
|
|
581
607
|
throw new TargetCancelledError();
|
|
582
608
|
if (item.execution && item.execution.id !== retryOf)
|
|
583
609
|
throw new UncertainExecutionError(item.id, item.execution.id);
|
|
584
|
-
const frozenCase = await
|
|
610
|
+
const frozenCase = await frozenCaseOf();
|
|
585
611
|
if (frozenCase.datasetVersionId !== version.id)
|
|
586
612
|
throw new Error("Case is not from the pinned dataset version");
|
|
587
613
|
// Validate before creating a remote execution. SDK/input failures are not
|
|
@@ -4,6 +4,7 @@ import type { EnvironmentTool } from "../world/tools.js";
|
|
|
4
4
|
import type { EnvironmentIdentity, PublishableEnvironmentDefinition, WorldHandoff } from "../world/types.js";
|
|
5
5
|
import { type EvaluationClient } from "./client.js";
|
|
6
6
|
import type { ActualAgentManifestInputV2, AttemptConnectionBundleV2, RequestedAttemptProviderV2 } from "./attempt.js";
|
|
7
|
+
import { type EnvironmentTargetContext } from "./environment-target.js";
|
|
7
8
|
import { type RunnerReport, type TelemetryNotAccepted } from "./runner.js";
|
|
8
9
|
import type { ExperimentCase, Identity, JsonValue, LocalFile, LocalScorer, ScorerDefinition, SimulationMcpCapability, TargetResult } from "./types.js";
|
|
9
10
|
/** Repository-authored scorer identity and its public definition or local binding. */
|
|
@@ -222,6 +223,9 @@ export interface SimulationReport extends RunnerReport {
|
|
|
222
223
|
* @deprecated Use {@link SimulationDefinition}.
|
|
223
224
|
*/
|
|
224
225
|
export type SimulationScenario = SimulationDefinition;
|
|
226
|
+
/** The candidate-visible context of a world case, cloned from the environment target's: the
|
|
227
|
+
* frozen case's identity alone, never its expected outcome, metadata or source pins. */
|
|
228
|
+
export declare function simulationTargetContext(context: EnvironmentTargetContext): SimulationTargetContext;
|
|
225
229
|
/** Run an existing agent callback against one fresh hosted world per case. The helper
|
|
226
230
|
* owns immutable resolution, execution linkage, finalization, scoring and resumable uploads.
|
|
227
231
|
*/
|
package/dist/evals/simulation.js
CHANGED
|
@@ -344,6 +344,29 @@ async function resolveExperiment(options, definition, idempotencyKey) {
|
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
346
|
let scenarioDeprecationWarned = false;
|
|
347
|
+
/** The candidate-visible context of a world case, cloned from the environment target's: the
|
|
348
|
+
* frozen case's identity alone, never its expected outcome, metadata or source pins. */
|
|
349
|
+
export function simulationTargetContext(context) {
|
|
350
|
+
return {
|
|
351
|
+
config: structuredClone(context.config),
|
|
352
|
+
item: { id: context.item.id, externalKey: context.item.externalKey },
|
|
353
|
+
executionId: context.executionId,
|
|
354
|
+
environmentRunId: context.environmentRunId,
|
|
355
|
+
tools: context.tools,
|
|
356
|
+
...(context.world ? { world: structuredClone(context.world) } : {}),
|
|
357
|
+
...(context.mcp
|
|
358
|
+
? {
|
|
359
|
+
mcp: { url: context.mcp.url, token: context.mcp.token, expiresAt: context.mcp.expiresAt },
|
|
360
|
+
}
|
|
361
|
+
: {}),
|
|
362
|
+
...(context.connectionBundle
|
|
363
|
+
? { connectionBundle: structuredClone(context.connectionBundle) }
|
|
364
|
+
: {}),
|
|
365
|
+
files: structuredClone(context.files),
|
|
366
|
+
outputDirectory: context.outputDirectory,
|
|
367
|
+
signal: context.signal,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
347
370
|
/** Run an existing agent callback against one fresh hosted world per case. The helper
|
|
348
371
|
* owns immutable resolution, execution linkage, finalization, scoring and resumable uploads.
|
|
349
372
|
*/
|
|
@@ -462,32 +485,7 @@ export async function runSimulation(options) {
|
|
|
462
485
|
executionId: context.executionId,
|
|
463
486
|
caseId: context.item.id,
|
|
464
487
|
}),
|
|
465
|
-
target: (targetInputs, targetContext) => options.target(structuredClone(targetInputs),
|
|
466
|
-
config: structuredClone(targetContext.config),
|
|
467
|
-
item: {
|
|
468
|
-
id: targetContext.item.id,
|
|
469
|
-
externalKey: targetContext.item.externalKey,
|
|
470
|
-
},
|
|
471
|
-
executionId: targetContext.executionId,
|
|
472
|
-
environmentRunId: targetContext.environmentRunId,
|
|
473
|
-
tools: targetContext.tools,
|
|
474
|
-
...(targetContext.world ? { world: structuredClone(targetContext.world) } : {}),
|
|
475
|
-
...(targetContext.mcp
|
|
476
|
-
? {
|
|
477
|
-
mcp: {
|
|
478
|
-
url: targetContext.mcp.url,
|
|
479
|
-
token: targetContext.mcp.token,
|
|
480
|
-
expiresAt: targetContext.mcp.expiresAt,
|
|
481
|
-
},
|
|
482
|
-
}
|
|
483
|
-
: {}),
|
|
484
|
-
...(targetContext.connectionBundle
|
|
485
|
-
? { connectionBundle: structuredClone(targetContext.connectionBundle) }
|
|
486
|
-
: {}),
|
|
487
|
-
files: structuredClone(targetContext.files),
|
|
488
|
-
outputDirectory: targetContext.outputDirectory,
|
|
489
|
-
signal: targetContext.signal,
|
|
490
|
-
}),
|
|
488
|
+
target: (targetInputs, targetContext) => options.target(structuredClone(targetInputs), simulationTargetContext(targetContext)),
|
|
491
489
|
}),
|
|
492
490
|
});
|
|
493
491
|
const complete = { ...report, experimentId, runUrl };
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Package version shared by the instrumentation scope and the export User-Agent. */
|
|
2
|
-
export declare const sdkVersion = "0.20.
|
|
2
|
+
export declare const sdkVersion = "0.20.3";
|
package/dist/version.js
CHANGED