@hue-run/sdk 0.15.2 → 0.16.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 +20 -18
- package/EVALUATIONS.md +1 -1
- package/README.md +4 -7
- package/dist/cli/env-file.d.ts +2 -0
- package/dist/cli/env-file.js +2 -0
- package/dist/cli/eval.js +109 -22
- package/dist/cli/login.js +2 -3
- package/dist/evals/attempt.d.ts +1 -1
- package/dist/evals/attempt.js +1 -1
- package/dist/evals/checkpoint.d.ts +6 -2
- package/dist/evals/checkpoint.js +75 -12
- package/dist/evals/runner.d.ts +9 -0
- package/dist/evals/runner.js +63 -10
- package/dist/evals/scorer-publication.js +1 -1
- package/dist/evals/simulation.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -9
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.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.16.0`, `@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
|
|
|
@@ -469,7 +469,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
469
469
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
470
470
|
project=/absolute/path/to/supported-fixture
|
|
471
471
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
472
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
472
|
+
--archive .artifacts/typescript/hue-run-sdk-0.16.0.tgz \
|
|
473
473
|
--origin https://STAGING_ORIGIN \
|
|
474
474
|
--project "$project" --command setup \
|
|
475
475
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -480,7 +480,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
480
480
|
|
|
481
481
|
```sh
|
|
482
482
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
483
|
-
--archive .artifacts/typescript/hue-run-sdk-0.
|
|
483
|
+
--archive .artifacts/typescript/hue-run-sdk-0.16.0.tgz \
|
|
484
484
|
--origin https://STAGING_ORIGIN \
|
|
485
485
|
--project "$project" --command claim \
|
|
486
486
|
--evidence .context/setup-staging-after-claim.json
|
|
@@ -510,7 +510,7 @@ and prints Hue's verdicts. It is the command-line form of `runSimulation()` (one
|
|
|
510
510
|
process, Hue creates one isolated simulated world per case, and Hue-owned outcome checks grade
|
|
511
511
|
the sealed world. Hue never executes the agent. `HUE_API_KEY` must be a **Read and
|
|
512
512
|
write** project key (a **Read** or **Tracing only** key cannot read cases or create runs);
|
|
513
|
-
the CLI never prints it.
|
|
513
|
+
the CLI never prints it.
|
|
514
514
|
|
|
515
515
|
Run the agent's own start command with `--command`. `hue eval` spawns it once per case with the
|
|
516
516
|
world's environment (below) and `{"inputs", "config"}` on stdin, and stores its stdout as the
|
|
@@ -519,19 +519,20 @@ committed helper described in [Make your agent eval-ready](https://docs.hue.run/
|
|
|
519
519
|
reads `HUE_SIM_<SURFACE ID>_URL` and `HUE_WORLD_TOKEN` when `HUE_WORLD_TOKEN` is set, fails
|
|
520
520
|
closed when a needed variable is missing, and returns the production URL and credential
|
|
521
521
|
otherwise. Never hand the agent Hue-native tools (`context.tools`, `context.mcp`) or a tool its
|
|
522
|
-
production agent lacks. Pin the SDK
|
|
523
|
-
gains no dependency:
|
|
522
|
+
production agent lacks. Pin the SDK, so the agent's repository gains no dependency:
|
|
524
523
|
|
|
525
524
|
```sh
|
|
526
|
-
npx
|
|
527
|
-
npx
|
|
528
|
-
npx
|
|
529
|
-
npx
|
|
525
|
+
npx -y @hue-run/sdk@0.16.0 eval --case "Refund an eligible charge" --command "node agent.js"
|
|
526
|
+
npx -y @hue-run/sdk@0.16.0 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
|
|
527
|
+
npx -y @hue-run/sdk@0.16.0 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
|
|
528
|
+
npx -y @hue-run/sdk@0.16.0 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
|
|
530
529
|
```
|
|
531
530
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
531
|
+
One invocation creates one run holding every selected case; it prints the run URL
|
|
532
|
+
(`https://app.hue.run/runs/<id>`) once the run exists. Add `--check` to confirm the key, the
|
|
533
|
+
selection, the case kind and the agent first: it prints what the run would do and exits without
|
|
534
|
+
creating a run, a world or a checkpoint. An
|
|
535
|
+
unpinned `npx @hue-run/sdk` runs a local, possibly older, copy in a project that already
|
|
535
536
|
depends on it.
|
|
536
537
|
|
|
537
538
|
An adapter module is the in-process alternative for a TypeScript agent. It exports `default` or
|
|
@@ -562,7 +563,7 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
|
|
|
562
563
|
```
|
|
563
564
|
|
|
564
565
|
```sh
|
|
565
|
-
npx
|
|
566
|
+
npx -y @hue-run/sdk@0.16.0 eval --case "Refund an eligible charge" ./hue-agent.ts --content
|
|
566
567
|
```
|
|
567
568
|
|
|
568
569
|
`--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
|
|
@@ -593,7 +594,7 @@ required judge to decide answer-only cases. Any
|
|
|
593
594
|
other evaluator's result, an error, or a metric that carries `passed` is never advisory, whatever
|
|
594
595
|
its evidence says. A metric name that
|
|
595
596
|
several evaluators report, such as two judges' `verdict`, gets a column per evaluator, labelled
|
|
596
|
-
with the version's first eight characters. `--baseline <
|
|
597
|
+
with the version's first eight characters. `--baseline <run id|url>` adds improvement,
|
|
597
598
|
regression and unchanged counts with per-case deltas. `--json` prints one JSON document
|
|
598
599
|
(`experimentId`, `runId`, `runUrl`, `complete`, `cases`, `totals`, optional `baseline`) on stdout
|
|
599
600
|
and sends progress to stderr; each case lists its not-applicable evaluator versions in
|
|
@@ -670,7 +671,8 @@ the sealed world at once and attaches the trace once its export is complete. The
|
|
|
670
671
|
A Scenario or eval set whose dataset version is not saved cannot back an experiment: the command
|
|
671
672
|
exits 1 and asks for **Save eval-set version** in Hue or `--save-version`, which freezes that
|
|
672
673
|
version at its current revision. Connection settings are `HUE_API_KEY` and `HUE_BASE_URL`
|
|
673
|
-
(default `https://app.hue.run`), loaded from `--env-file <path>` (or its alias `--env-path`) first when given
|
|
674
|
+
(default `https://app.hue.run`), loaded from `--env-file <path>` (or its alias `--env-path`) first when given, otherwise from
|
|
675
|
+
`./.env.hue` when the shell has no `HUE_API_KEY` (a key in the shell keeps its own origin); `--origin`
|
|
674
676
|
overrides the origin. Since TypeScript 0.14.0, telemetry content capture defaults on; `--no-content` opts out.
|
|
675
677
|
Earlier releases require `--content`; the examples include it for compatibility. Model and tool spans inside the agent come
|
|
676
678
|
only from the agent's own instrumentation. Case outputs, error messages and explanations are
|
|
@@ -734,8 +736,8 @@ published version, beside or instead of explicit `--scorer-version` IDs.
|
|
|
734
736
|
```sh
|
|
735
737
|
hue eval --set gia-d1-citation --scorer gia-d1-citation \
|
|
736
738
|
--command "pnpm --filter @august/frontend run hue:gia-agent" \
|
|
737
|
-
--revision prompt-v10 --wait 1800 --content --json
|
|
738
|
-
hue eval --set gia-d1-citation --set-version 1 --scorer gia-d1-citation ./hue-agent.ts --content --baseline <
|
|
739
|
+
--revision prompt-v10 --wait 1800 --content --json
|
|
740
|
+
hue eval --set gia-d1-citation --set-version 1 --scorer gia-d1-citation ./hue-agent.ts --content --baseline <run id>
|
|
739
741
|
```
|
|
740
742
|
|
|
741
743
|
For each case the command is spawned once **inside a private case directory** with
|
package/EVALUATIONS.md
CHANGED
|
@@ -7,7 +7,7 @@ Install the optional runtime-contract peer with the SDK before importing
|
|
|
7
7
|
npm install @hue-run/sdk zod
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
The SDK executes targets and scorers on your machine. Hue stores pinned definitions, experiment progress and results. It does not execute uploaded source code. Follow the
|
|
10
|
+
The SDK executes targets and scorers on your machine. Hue stores pinned definitions, experiment progress and results. It does not execute uploaded source code. Follow [Install the SDK](https://docs.hue.run/quickstart#2-install-the-sdk) to add `@hue-run/sdk` to your application.
|
|
11
11
|
|
|
12
12
|
Create a **Read and write** project service key under **Settings → Integrations & API keys** and expose it to this server-side process as `HUE_API_KEY`. A **Tracing only** key cannot author datasets or evaluation runs.
|
|
13
13
|
|
package/README.md
CHANGED
|
@@ -677,15 +677,12 @@ conditions only. Use Hue's UI to inspect captured values and redaction.
|
|
|
677
677
|
|
|
678
678
|
## Dependencies
|
|
679
679
|
|
|
680
|
-
The tracing core depends only on official `@opentelemetry/*` packages
|
|
681
|
-
`@hue-run/sdk/evals` entry point
|
|
682
|
-
|
|
683
|
-
loaded inside a worker only when `builtins.jsonSchema` scores a case; without it that scorer
|
|
680
|
+
The tracing core depends only on official `@opentelemetry/*` packages and `zod`, which the
|
|
681
|
+
`@hue-run/sdk/evals` entry point and `hue eval` use for their bounded runtime contracts and which
|
|
682
|
+
installs with the SDK. JSON Schema scoring also uses `ajv`, an optional peer loaded inside a worker only when `builtins.jsonSchema` scores a case; without it that scorer
|
|
684
683
|
reports `SchemaValidatorUnavailable`. Install it when you use that scorer:
|
|
685
684
|
|
|
686
685
|
```bash
|
|
687
|
-
npm install zod
|
|
688
|
-
# Add ajv too when using builtins.jsonSchema.
|
|
689
686
|
npm install ajv
|
|
690
687
|
```
|
|
691
688
|
|
|
@@ -795,6 +792,6 @@ existing-provider flush callbacks and recovery. Local/CI runners remain availabl
|
|
|
795
792
|
|
|
796
793
|
Use `createHueSafe(options)` for best-effort startup. Invalid initialization returns a disabled client that keeps your `onExportIssue` hook and records the reason as an instrumentation failure. Pass `enabled: false` to disable Hue without a key or a `captureContent` choice; disabled helpers still execute the application callback, and `inject()` keeps propagating the application's own trace context. `flushSafe({ timeoutMillis: 1000 })` and `shutdownSafe({ timeoutMillis: 1000 })` return `{ ok, timedOut, report }` without rejecting. Strict initialization, connection checks and `flush()` remain available for diagnostics; do not gate application readiness or responses on them.
|
|
797
794
|
|
|
798
|
-
Capture/serialization/redaction/provider failures omit unsafe telemetry, record failures, and preserve the original business result/error. Async diagnostic rejections are contained; diagnostics are rate-limited. The default `maxQueueBytes` is 8 MiB across traces/logs including in-flight work, alongside the existing record cap. `pendingBytes` is a current queue gauge; `droppedSpans`, `droppedLogs` and `instrumentationFailures` are cumulative failure counters; `uploadedValues` and `uploadFallbacks` count the values over the inline limit uploaded apart from their spans and exported cut instead. This is a telemetry budget, not a total process memory ceiling. A timeout bounds the caller and does not cancel a borrowed provider. Never retry the business operation to recover telemetry. See [production
|
|
795
|
+
Capture/serialization/redaction/provider failures omit unsafe telemetry, record failures, and preserve the original business result/error. Async diagnostic rejections are contained; diagnostics are rate-limited. The default `maxQueueBytes` is 8 MiB across traces/logs including in-flight work, alongside the existing record cap. `pendingBytes` is a current queue gauge; `droppedSpans`, `droppedLogs` and `instrumentationFailures` are cumulative failure counters; `uploadedValues` and `uploadFallbacks` count the values over the inline limit uploaded apart from their spans and exported cut instead. This is a telemetry budget, not a total process memory ceiling. A timeout bounds the caller and does not cancel a borrowed provider. Never retry the business operation to recover telemetry. See [production setup](https://docs.hue.run/sdks/typescript#production-setup).
|
|
799
796
|
|
|
800
797
|
Queued records snapshot supported telemetry values when a span ends or a log is emitted; later caller mutations cannot change queued data. Resource attributes still awaiting detection are omitted with a sanitized warning. Later records include them after detection finishes; await resource detection before instrumentation when those attributes are required.
|
package/dist/cli/env-file.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The key file `hue login` writes; `hue eval` loads it when no env file is named and the shell has no HUE_API_KEY. */
|
|
2
|
+
export declare const DEFAULT_ENV_FILE = ".env.hue";
|
|
1
3
|
/**
|
|
2
4
|
* Node 22 and 24 scan the whole command line for `--env-file`, including arguments meant for the
|
|
3
5
|
* script, and exit with "node: <path>: not found" before the CLI runs when that file does not
|
package/dist/cli/env-file.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
/** The key file `hue login` writes; `hue eval` loads it when no env file is named and the shell has no HUE_API_KEY. */
|
|
3
|
+
export const DEFAULT_ENV_FILE = ".env.hue";
|
|
2
4
|
/**
|
|
3
5
|
* Node 22 and 24 scan the whole command line for `--env-file`, including arguments meant for the
|
|
4
6
|
* script, and exit with "node: <path>: not found" before the CLI runs when that file does not
|
package/dist/cli/eval.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { access, mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, extname, join, resolve } from "node:path";
|
|
@@ -22,7 +22,7 @@ import { matchByName, parseScenarioSelector, resolveEvalSetPins, resolveScenario
|
|
|
22
22
|
import { collectDirectOutputs, stageDirectCase } from "./eval-direct.js";
|
|
23
23
|
import { runSimulation, } from "../evals/simulation.js";
|
|
24
24
|
import { collectExperimentVerdicts, compareVerdicts, metricPassed, } from "../evals/verdicts.js";
|
|
25
|
-
import { envFileArgument, envFileOptions } from "./env-file.js";
|
|
25
|
+
import { DEFAULT_ENV_FILE, envFileArgument, envFileOptions } from "./env-file.js";
|
|
26
26
|
const USAGE = `Usage: hue eval [adapter-file] [options]
|
|
27
27
|
|
|
28
28
|
Run a local agent against a published case or eval set, then print Hue's verdicts.
|
|
@@ -70,13 +70,15 @@ Modes:
|
|
|
70
70
|
environment-files:v1 and input:pdf for world cases with files
|
|
71
71
|
|
|
72
72
|
Connection:
|
|
73
|
-
--env-file <path> Load a dotenv file (HUE_API_KEY, HUE_BASE_URL) first
|
|
73
|
+
--env-file <path> Load a dotenv file (HUE_API_KEY, HUE_BASE_URL) first;
|
|
74
|
+
./.env.hue loads by default when HUE_API_KEY is not
|
|
75
|
+
set. Variables already set in the environment win
|
|
74
76
|
--env-path <path> Same as --env-file
|
|
75
77
|
--origin <url> Hue origin (default: HUE_BASE_URL or https://app.hue.run)
|
|
76
78
|
|
|
77
79
|
Output and limits:
|
|
78
80
|
--name <run name> Run name (default: <agent key> @ <revision>, hashes shortened)
|
|
79
|
-
--baseline <
|
|
81
|
+
--baseline <run id|url> Compare verdicts with a previous run
|
|
80
82
|
--json Print one JSON document on stdout; progress goes to stderr
|
|
81
83
|
--content Capture telemetry content (the default)
|
|
82
84
|
--no-content Omit telemetry inputs, outputs and messages
|
|
@@ -84,6 +86,8 @@ Output and limits:
|
|
|
84
86
|
explanations (stored by default; --worker always stores); with
|
|
85
87
|
--content the case span still carries the output
|
|
86
88
|
--save-version Freeze an unsaved eval-set version before running
|
|
89
|
+
--check One-shot: check the key, selection and agent, print what the
|
|
90
|
+
run would do and exit without creating a run or a world
|
|
87
91
|
--checkpoint-dir <path> Private checkpoint directory (default: .hue/eval/<agent-key>)
|
|
88
92
|
--concurrency <n> Cases in flight, 1-100 (default: 1)
|
|
89
93
|
--trace-not-accepted <policy> A case whose telemetry Hue did not accept in time: fail_case
|
|
@@ -147,6 +151,7 @@ function parse(argv) {
|
|
|
147
151
|
"no-output": { type: "boolean", default: false },
|
|
148
152
|
"trace-not-accepted": { type: "string" },
|
|
149
153
|
"save-version": { type: "boolean", default: false },
|
|
154
|
+
check: { type: "boolean", default: false },
|
|
150
155
|
"checkpoint-dir": { type: "string" },
|
|
151
156
|
concurrency: { type: "string" },
|
|
152
157
|
timeout: { type: "string" },
|
|
@@ -971,20 +976,27 @@ function parseMode(value) {
|
|
|
971
976
|
return value;
|
|
972
977
|
throw new UsageError("--mode must be auto, direct or simulation");
|
|
973
978
|
}
|
|
974
|
-
/**
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
979
|
+
/** The selection's cases, and whether the run is direct: when `--mode` leaves it open, direct
|
|
980
|
+
* means nothing pins a simulated world, not a Scenario and no case of the version. A simulation
|
|
981
|
+
* run cannot execute a case that pins no world, so a selection mixing both kinds is refused. */
|
|
982
|
+
async function inspectSelection(client, pins, mode) {
|
|
983
|
+
let cases = 0;
|
|
984
|
+
let worldCases = 0;
|
|
980
985
|
let after;
|
|
981
986
|
do {
|
|
982
987
|
const page = await client.listCases(pins.datasetVersionId, { after });
|
|
983
|
-
|
|
984
|
-
|
|
988
|
+
cases += page.items.length;
|
|
989
|
+
worldCases += page.items.filter((item) => item.environmentVersionId).length;
|
|
985
990
|
after = page.nextCursor ?? undefined;
|
|
986
991
|
} while (after);
|
|
987
|
-
|
|
992
|
+
const direct = mode === "auto"
|
|
993
|
+
? !pins.scenarioId && !pins.environmentVersionId && worldCases === 0
|
|
994
|
+
: mode === "direct";
|
|
995
|
+
const answerOnly = cases - worldCases;
|
|
996
|
+
const refusal = !direct && answerOnly > 0
|
|
997
|
+
? `${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. Put the answer-only cases in their own eval set${mode === "simulation" ? ", or drop --mode simulation" : ""}.`
|
|
998
|
+
: undefined;
|
|
999
|
+
return { direct, cases, worldCases, ...(refusal ? { refusal } : {}) };
|
|
988
1000
|
}
|
|
989
1001
|
function toJson(verdicts, runUrl, baseline, extra = {}, telemetry = []) {
|
|
990
1002
|
const notAccepted = new Map(telemetry.map((entry) => [entry.caseId, entry.issues]));
|
|
@@ -1055,7 +1067,7 @@ async function reportVerdicts(client, values, run, baselineId, output, signal) {
|
|
|
1055
1067
|
// The wait returns its partial state on abort rather than throwing, so Ctrl+C here must not
|
|
1056
1068
|
// fall through to a baseline read and a verdict table that nobody asked to finish.
|
|
1057
1069
|
if (signal.aborted) {
|
|
1058
|
-
process.stderr.write(
|
|
1070
|
+
process.stderr.write(`Interrupted. The cases already finished and Hue is still checking them; open ${run.runUrl} instead of rerunning.\n`);
|
|
1059
1071
|
return 130;
|
|
1060
1072
|
}
|
|
1061
1073
|
let baseline;
|
|
@@ -1086,7 +1098,7 @@ async function reportVerdicts(client, values, run, baselineId, output, signal) {
|
|
|
1086
1098
|
function parseBaseline(value) {
|
|
1087
1099
|
if (!value)
|
|
1088
1100
|
return undefined;
|
|
1089
|
-
const parsed = parseScenarioSelector(value, ["experiments"]);
|
|
1101
|
+
const parsed = parseScenarioSelector(value, ["runs", "experiments"]);
|
|
1090
1102
|
if (parsed.kind !== "id")
|
|
1091
1103
|
throw new UsageError("--baseline must be an experiment ID or its Hue URL");
|
|
1092
1104
|
return parsed.id;
|
|
@@ -1096,7 +1108,15 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1096
1108
|
const concurrency = integer("concurrency", values.concurrency, 1, 1, 100);
|
|
1097
1109
|
const baselineId = parseBaseline(values.baseline);
|
|
1098
1110
|
const mode = parseMode(values.mode);
|
|
1111
|
+
integer("wait", values.wait, 300, 0, 86_400);
|
|
1099
1112
|
const pins = await resolveSelection(client, values);
|
|
1113
|
+
const selection = await inspectSelection(client, pins, mode);
|
|
1114
|
+
if (values.check)
|
|
1115
|
+
return checkRun(client, values, pins, selection, agent, output);
|
|
1116
|
+
if (selection.refusal) {
|
|
1117
|
+
output.error(selection.refusal);
|
|
1118
|
+
return 1;
|
|
1119
|
+
}
|
|
1100
1120
|
if (!pins.saved) {
|
|
1101
1121
|
if (!values["save-version"]) {
|
|
1102
1122
|
output.error(`The ${pins.scenarioId ? "Scenario" : "eval set"} "${pins.name}" points at an unsaved version. Choose "Save eval-set version" in Hue, or pass --save-version to freeze it now, then rerun.`);
|
|
@@ -1107,7 +1127,7 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1107
1127
|
}
|
|
1108
1128
|
const runName = values.name ?? defaultRunName(agent);
|
|
1109
1129
|
const project = await client.checkConnection();
|
|
1110
|
-
if (
|
|
1130
|
+
if (selection.direct)
|
|
1111
1131
|
return runDirect({
|
|
1112
1132
|
client,
|
|
1113
1133
|
values,
|
|
@@ -1190,6 +1210,62 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1190
1210
|
telemetryNotAccepted: report.telemetryNotAccepted,
|
|
1191
1211
|
}, baselineId, output, signal);
|
|
1192
1212
|
}
|
|
1213
|
+
/**
|
|
1214
|
+
* `--check`: everything a one-shot run resolves before it creates anything (the key's project,
|
|
1215
|
+
* the selection, the case kind and the agent), printed with the command that would start it.
|
|
1216
|
+
* Nothing is created, frozen or checkpointed.
|
|
1217
|
+
*/
|
|
1218
|
+
async function checkRun(client, values, pins, selection, agent, output) {
|
|
1219
|
+
const project = await client.checkConnection();
|
|
1220
|
+
for (const id of pins.scorerVersionIds) {
|
|
1221
|
+
const found = await client.getScorerVersion(id).then(() => true, (error) => {
|
|
1222
|
+
if (error instanceof HueApiError && (error.status === 404 || error.status === 403))
|
|
1223
|
+
return false;
|
|
1224
|
+
throw error;
|
|
1225
|
+
});
|
|
1226
|
+
if (!found) {
|
|
1227
|
+
output.error(`Evaluator version ${id} was not found in project "${project.name}".`);
|
|
1228
|
+
return 1;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const { direct, cases, worldCases, refusal } = selection;
|
|
1232
|
+
const unsaved = !pins.saved && !values["save-version"];
|
|
1233
|
+
const check = {
|
|
1234
|
+
ok: !unsaved && !refusal,
|
|
1235
|
+
project: { id: project.id, name: project.name },
|
|
1236
|
+
selection: {
|
|
1237
|
+
kind: pins.scenarioId ? "case" : "eval_set",
|
|
1238
|
+
name: pins.name,
|
|
1239
|
+
datasetVersionId: pins.datasetVersionId,
|
|
1240
|
+
saved: pins.saved,
|
|
1241
|
+
cases,
|
|
1242
|
+
evaluators: pins.scorerVersionIds.length,
|
|
1243
|
+
},
|
|
1244
|
+
mode: direct ? "direct" : "simulation",
|
|
1245
|
+
worldCases: direct ? 0 : worldCases,
|
|
1246
|
+
agent: { key: agent.key, revision: agent.revision, concurrency: values.concurrency ?? "1" },
|
|
1247
|
+
};
|
|
1248
|
+
if (values.json)
|
|
1249
|
+
process.stdout.write(`${JSON.stringify({ check })}\n`);
|
|
1250
|
+
else {
|
|
1251
|
+
output.log(`Project: ${project.name} (${project.id})`);
|
|
1252
|
+
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"}`);
|
|
1253
|
+
output.log(direct
|
|
1254
|
+
? "Mode: direct; the agent answers without a simulated world or app mirrors"
|
|
1255
|
+
: `Mode: simulation; ${worldCases} ${worldCases === 1 ? "case gets" : "cases get"} a fresh world, reached through the HUE_SIM_*_URL mirrors and HUE_WORLD_TOKEN`);
|
|
1256
|
+
output.log(`Agent: ${agent.key} @ ${agent.revision}`);
|
|
1257
|
+
}
|
|
1258
|
+
if (refusal) {
|
|
1259
|
+
output.error(refusal);
|
|
1260
|
+
return 1;
|
|
1261
|
+
}
|
|
1262
|
+
if (unsaved) {
|
|
1263
|
+
output.error(`The ${pins.scenarioId ? "Scenario" : "eval set"} "${pins.name}" points at an unsaved version; add --save-version to freeze it when the run starts.`);
|
|
1264
|
+
return 1;
|
|
1265
|
+
}
|
|
1266
|
+
output.log("Check passed. Nothing was created; run the same command without --check to start one run.");
|
|
1267
|
+
return 0;
|
|
1268
|
+
}
|
|
1193
1269
|
/**
|
|
1194
1270
|
* Direct cases: one ordinary experiment through `runExperiment`. The agent receives the case's
|
|
1195
1271
|
* pinned files and returns generated documents; code evaluators pinned to the run stay deferred
|
|
@@ -1218,7 +1294,7 @@ async function runDirect(run, connection) {
|
|
|
1218
1294
|
});
|
|
1219
1295
|
let attempt = await store.read("active-attempt");
|
|
1220
1296
|
if (attempt && attempt.stage !== "completed" && attempt.selectionDigest !== selectionDigest)
|
|
1221
|
-
throw new Error("Recover the unfinished direct run before running a changed selection");
|
|
1297
|
+
throw new Error("Recover the unfinished direct run before running a changed selection: run the unchanged command again to finish it, or pass another --checkpoint-dir");
|
|
1222
1298
|
if (!attempt || attempt.stage === "completed") {
|
|
1223
1299
|
attempt = { selectionDigest, idempotencyKey: randomUUID(), stage: "preparing" };
|
|
1224
1300
|
await store.write("active-attempt", attempt);
|
|
@@ -1236,7 +1312,7 @@ async function runDirect(run, connection) {
|
|
|
1236
1312
|
await store.write("active-attempt", attempt);
|
|
1237
1313
|
}
|
|
1238
1314
|
experimentId = attempt.experimentId;
|
|
1239
|
-
runUrl = new URL(`/
|
|
1315
|
+
runUrl = new URL(`/runs/${experimentId}`, connection.baseUrl).toString();
|
|
1240
1316
|
output.log(`Run: ${runUrl}`);
|
|
1241
1317
|
output.log(`Experiment: ${experimentId}`);
|
|
1242
1318
|
report = await runExperiment({
|
|
@@ -1251,6 +1327,7 @@ async function runDirect(run, connection) {
|
|
|
1251
1327
|
traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
|
|
1252
1328
|
onTelemetryNotAccepted: telemetry.report,
|
|
1253
1329
|
concurrency: run.concurrency,
|
|
1330
|
+
signal,
|
|
1254
1331
|
scorers: [],
|
|
1255
1332
|
deferUnboundLocalScorers: true,
|
|
1256
1333
|
environmentEvidence: "when_pinned",
|
|
@@ -1308,7 +1385,7 @@ async function runWorker(values, connection, agents, agent, hue, output, signal)
|
|
|
1308
1385
|
};
|
|
1309
1386
|
client.onClaimed = (claim) => {
|
|
1310
1387
|
current = claim;
|
|
1311
|
-
output.log(`Claimed run ${claim.runId}: ${new URL(`/
|
|
1388
|
+
output.log(`Claimed run ${claim.runId}: ${new URL(`/runs/${claim.experimentId}`, connection.baseUrl).toString()}`);
|
|
1312
1389
|
};
|
|
1313
1390
|
const telemetry = telemetryReporter(output);
|
|
1314
1391
|
// A command can use the same stdin contract for both case kinds. Existing adapter files
|
|
@@ -1396,7 +1473,7 @@ async function runWorker(values, connection, agents, agent, hue, output, signal)
|
|
|
1396
1473
|
if (signal.aborted)
|
|
1397
1474
|
return;
|
|
1398
1475
|
if (values.json)
|
|
1399
|
-
process.stdout.write(`${JSON.stringify(toJson(verdicts, new URL(`/
|
|
1476
|
+
process.stdout.write(`${JSON.stringify(toJson(verdicts, new URL(`/runs/${current.experimentId}`, connection.baseUrl).toString(), undefined, {}, report.telemetryNotAccepted))}\n`);
|
|
1400
1477
|
else
|
|
1401
1478
|
renderTable(verdicts, output);
|
|
1402
1479
|
}
|
|
@@ -1446,8 +1523,10 @@ export async function runEvalCommand(argv) {
|
|
|
1446
1523
|
process.on("SIGTERM", interrupt);
|
|
1447
1524
|
let hue;
|
|
1448
1525
|
let json = false;
|
|
1526
|
+
let worker = false;
|
|
1449
1527
|
try {
|
|
1450
1528
|
const { values, positionals } = parse(argv);
|
|
1529
|
+
worker = values.worker;
|
|
1451
1530
|
if (values.content && values["no-content"])
|
|
1452
1531
|
throw new UsageError("Choose either --content or --no-content");
|
|
1453
1532
|
// A usage error before anything is prepared or created: no run is left to recover from it.
|
|
@@ -1478,6 +1557,10 @@ export async function runEvalCommand(argv) {
|
|
|
1478
1557
|
catch (error) {
|
|
1479
1558
|
throw new UsageError(error.message);
|
|
1480
1559
|
}
|
|
1560
|
+
// A key already in the environment keeps its own origin: the checkout's .env.hue is
|
|
1561
|
+
// consulted only when it has to supply the key, so it cannot point an inherited key elsewhere.
|
|
1562
|
+
if (envFile === undefined && !process.env.HUE_API_KEY && existsSync(DEFAULT_ENV_FILE))
|
|
1563
|
+
envFile = DEFAULT_ENV_FILE;
|
|
1481
1564
|
if (envFile) {
|
|
1482
1565
|
try {
|
|
1483
1566
|
process.loadEnvFile(resolve(envFile));
|
|
@@ -1488,7 +1571,7 @@ export async function runEvalCommand(argv) {
|
|
|
1488
1571
|
}
|
|
1489
1572
|
const apiKey = process.env.HUE_API_KEY?.trim();
|
|
1490
1573
|
if (!apiKey)
|
|
1491
|
-
throw new UsageError('HUE_API_KEY is required: a "Read and write" project key, set in the environment or an ignored --env-file');
|
|
1574
|
+
throw new UsageError('HUE_API_KEY is required: a "Read and write" project key, set in the environment, ./.env.hue or an ignored --env-file');
|
|
1492
1575
|
secrets.push(apiKey);
|
|
1493
1576
|
const baseUrl = values.origin ?? process.env.HUE_BASE_URL?.trim() ?? "https://app.hue.run";
|
|
1494
1577
|
const connection = { apiKey, baseUrl };
|
|
@@ -1503,6 +1586,8 @@ export async function runEvalCommand(argv) {
|
|
|
1503
1586
|
throw new UsageError("--worker takes no selection; Hue chooses the run to execute");
|
|
1504
1587
|
if (values.worker && values["no-output"])
|
|
1505
1588
|
throw new UsageError("--no-output applies to one-shot runs; a run launched from Hue always stores its outputs");
|
|
1589
|
+
if (values.worker && values.check)
|
|
1590
|
+
throw new UsageError("--check applies to one-shot runs");
|
|
1506
1591
|
if (!values.worker && values.capability?.length)
|
|
1507
1592
|
throw new UsageError("--capability applies to --worker only");
|
|
1508
1593
|
const loaded = adapterFile ? await loadAdapter(adapterFile) : undefined;
|
|
@@ -1535,7 +1620,9 @@ export async function runEvalCommand(argv) {
|
|
|
1535
1620
|
}
|
|
1536
1621
|
catch (error) {
|
|
1537
1622
|
if (controller.signal.aborted || error instanceof TargetCancelledError) {
|
|
1538
|
-
process.stderr.write(
|
|
1623
|
+
process.stderr.write(worker
|
|
1624
|
+
? "Interrupted.\n"
|
|
1625
|
+
: "Interrupted. The run stays open; run the same command again to resume it.\n");
|
|
1539
1626
|
return 130;
|
|
1540
1627
|
}
|
|
1541
1628
|
if (error instanceof UsageError) {
|
package/dist/cli/login.js
CHANGED
|
@@ -7,9 +7,8 @@ import { createInterface } from "node:readline";
|
|
|
7
7
|
import { Writable } from "node:stream";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import { isLoopbackHost } from "../config.js";
|
|
10
|
-
import { envFileArgument, envFileOptions } from "./env-file.js";
|
|
10
|
+
import { DEFAULT_ENV_FILE, envFileArgument, envFileOptions } from "./env-file.js";
|
|
11
11
|
const DEFAULT_ORIGIN = "https://app.hue.run";
|
|
12
|
-
const DEFAULT_ENV_FILE = ".env.hue";
|
|
13
12
|
/** Settings section that lists and creates project service keys. */
|
|
14
13
|
const KEY_SETTINGS_PATH = "/settings/integrations";
|
|
15
14
|
const REQUEST_TIMEOUT_MILLIS = 10_000;
|
|
@@ -745,6 +744,6 @@ export async function runLoginCommand(argv, io = {}) {
|
|
|
745
744
|
// Without --auth it configures sign-in unless HUE_MCP_KEY is exported, so name the key stored.
|
|
746
745
|
out(` hue mcp install --client claude-code --auth key${mcpUrl === mcpUrlForOrigin(DEFAULT_ORIGIN) ? "" : ` --url ${mcpUrl}`}`);
|
|
747
746
|
if (stored.includes("evaluations"))
|
|
748
|
-
out(` hue eval --case "<name>" ./hue-agent.ts --env-file ${envDisplay}`);
|
|
747
|
+
out(` hue eval --case "<name>" ./hue-agent.ts${envDisplay === DEFAULT_ENV_FILE ? "" : ` --env-file ${envDisplay}`}`);
|
|
749
748
|
return 0;
|
|
750
749
|
}
|
package/dist/evals/attempt.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
2
|
import type { CoverageGap } from "../environment/types.js";
|
|
3
3
|
import type { SimulationMcpCapability } from "./types.js";
|
|
4
4
|
/** Canonical caller-observed agent configuration after missing evidence is made explicit. */
|
package/dist/evals/attempt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { z } from "zod";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
3
|
import { json, valueBounds } from "./json.js";
|
|
4
4
|
const canonicalUuid = z.uuid().transform((value) => value.toLowerCase());
|
|
5
5
|
const sha256Digest = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
@@ -15,11 +15,15 @@ export declare class CheckpointIdentityError extends Error {
|
|
|
15
15
|
captureContent?: unknown;
|
|
16
16
|
} | undefined);
|
|
17
17
|
}
|
|
18
|
-
/** One owner per directory. A
|
|
19
|
-
*
|
|
18
|
+
/** One owner per directory. A forced exit of `hue eval`, which stops its agents first, releases
|
|
19
|
+
* it; a crash leaves .lock, which the next owner reclaims once the process that held it on this
|
|
20
|
+
* machine has exited. Any other lock is left for explicit operator recovery. */
|
|
20
21
|
export declare class CheckpointStore {
|
|
21
22
|
readonly directory: string;
|
|
22
23
|
private untrack;
|
|
24
|
+
/** Whether this store took over the lock of a process that died here, so whatever that process
|
|
25
|
+
* was running never finished. */
|
|
26
|
+
reclaimed: boolean;
|
|
23
27
|
private constructor();
|
|
24
28
|
static acquire(directory: string, identity: unknown): Promise<CheckpointStore>;
|
|
25
29
|
read<T>(key: string): Promise<T | undefined>;
|
package/dist/evals/checkpoint.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { constants, rmSync } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
import { join, resolve, sep } from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { onForcedExit } from "./exit-cleanup.js";
|
|
6
7
|
import { digest } from "./json.js";
|
|
7
8
|
const CONTENT_POLICY = ["persistResultContent", "captureContent"];
|
|
@@ -46,11 +47,74 @@ function contentPolicyOnly(prior, identity) {
|
|
|
46
47
|
return undefined;
|
|
47
48
|
return Object.fromEntries(CONTENT_POLICY.map((key) => [key, before[key]]));
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
async function createLock(lock) {
|
|
51
|
+
try {
|
|
52
|
+
await mkdir(lock, { mode: 0o700 });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "EEXIST")
|
|
57
|
+
return false;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Whether a process with this ID is running; one owned by another user still counts. */
|
|
62
|
+
function running(pid) {
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return error.code === "EPERM";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Takes over a lock whose owner ran on this machine and has exited. The lock moves to a name
|
|
72
|
+
* derived from its unique owner record, which a rename can claim only once, so of the processes
|
|
73
|
+
* that saw the same dead owner one takes the lock and no live lock is ever moved; that name stays
|
|
74
|
+
* behind as the record. A lock without a readable owner, from another machine or from an SDK that
|
|
75
|
+
* did not record the machine is never reclaimed. */
|
|
76
|
+
async function reclaimLock(root, lock) {
|
|
77
|
+
const owner = await readFile(join(lock, "owner.json"), "utf8").catch(() => undefined);
|
|
78
|
+
if (owner === undefined)
|
|
79
|
+
return false;
|
|
80
|
+
let pid;
|
|
81
|
+
let host;
|
|
82
|
+
try {
|
|
83
|
+
({ pid, host } =
|
|
84
|
+
JSON.parse(owner).value ?? {});
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (typeof pid !== "number" ||
|
|
90
|
+
!Number.isInteger(pid) ||
|
|
91
|
+
pid <= 0 ||
|
|
92
|
+
pid === process.pid ||
|
|
93
|
+
host !== hostname() ||
|
|
94
|
+
running(pid))
|
|
95
|
+
return false;
|
|
96
|
+
const dead = join(root, `.lock-dead-${createHash("sha256").update(owner).digest("hex")}`);
|
|
97
|
+
try {
|
|
98
|
+
await rename(lock, dead);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if ((await readFile(join(dead, "owner.json"), "utf8").catch(() => undefined)) !== owner) {
|
|
104
|
+
await rename(dead, lock).catch(() => { });
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return createLock(lock);
|
|
108
|
+
}
|
|
109
|
+
/** One owner per directory. A forced exit of `hue eval`, which stops its agents first, releases
|
|
110
|
+
* it; a crash leaves .lock, which the next owner reclaims once the process that held it on this
|
|
111
|
+
* machine has exited. Any other lock is left for explicit operator recovery. */
|
|
51
112
|
export class CheckpointStore {
|
|
52
113
|
directory;
|
|
53
114
|
untrack = () => { };
|
|
115
|
+
/** Whether this store took over the lock of a process that died here, so whatever that process
|
|
116
|
+
* was running never finished. */
|
|
117
|
+
reclaimed = false;
|
|
54
118
|
constructor(directory) {
|
|
55
119
|
this.directory = directory;
|
|
56
120
|
}
|
|
@@ -61,15 +125,14 @@ export class CheckpointStore {
|
|
|
61
125
|
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o077) !== 0)
|
|
62
126
|
throw new Error("Use a private checkpoint directory (mode 0700, no symlink)");
|
|
63
127
|
const store = new CheckpointStore(root);
|
|
128
|
+
const lock = join(root, ".lock");
|
|
129
|
+
const created = await createLock(lock);
|
|
130
|
+
if (!created && !(await reclaimLock(root, lock)))
|
|
131
|
+
throw new Error("Checkpoint directory is locked by a process that may still be running; confirm it stopped before explicitly removing .lock");
|
|
132
|
+
store.reclaimed = !created;
|
|
133
|
+
store.untrack = onForcedExit(() => rmSync(lock, { recursive: true, force: true }));
|
|
64
134
|
try {
|
|
65
|
-
await
|
|
66
|
-
store.untrack = onForcedExit(() => rmSync(join(root, ".lock"), { recursive: true, force: true }));
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
throw new Error("Checkpoint directory is locked; confirm its owner stopped before explicitly removing .lock");
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
await store.write(".lock/owner", { pid: process.pid });
|
|
135
|
+
await store.write(".lock/owner", { pid: process.pid, host: hostname(), id: randomUUID() });
|
|
73
136
|
const expected = { format: 1, identity, digest: digest(identity) };
|
|
74
137
|
const prior = await store.read("manifest");
|
|
75
138
|
if (prior && (prior.format !== 1 || prior.digest !== expected.digest))
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -105,6 +105,15 @@ export interface RunExperimentTargetContext {
|
|
|
105
105
|
export interface RunExperimentOptions extends RunnerOptions {
|
|
106
106
|
/** Hue tracing client; each case runs inside a `hue.experiment.case` span. */
|
|
107
107
|
hue: HueClient;
|
|
108
|
+
/** Caller cancellation. Once it aborts, no further case starts, a case whose target stops with
|
|
109
|
+
* `TargetCancelledError` is saved as interrupted rather than completed, and the experiment is
|
|
110
|
+
* left unfinished: the call rejects with `TargetCancelledError`, and calling it again with the
|
|
111
|
+
* same checkpoint directory keeps the finished cases and runs the interrupted ones anew. */
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
/** Rerun, as a new attempt, a case that a process which died on this machine left running.
|
|
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
|
+
rerunCrashedCases?: boolean;
|
|
108
117
|
/** Experiment to run; its dataset version must be frozen. */
|
|
109
118
|
experimentId: string;
|
|
110
119
|
/** Whether each case waits for acknowledged trace export or explicitly omits evidence. Required. */
|
package/dist/evals/runner.js
CHANGED
|
@@ -213,6 +213,10 @@ async function allPages(page, maximum = 5000) {
|
|
|
213
213
|
* world that cannot be sealed), telemetry not accepted by the one transport the cases share, or
|
|
214
214
|
* a target outcome left uncertain because its world could not be sealed or read back. Starting
|
|
215
215
|
* more cases into it only piles up the same failure; the run is resumed later instead. */
|
|
216
|
+
/** A new attempt replacing an interrupted one, which Hue still holds as started. */
|
|
217
|
+
function retry(previousExecutionId) {
|
|
218
|
+
return previousExecutionId ? { previousExecutionId, allowUncertainRetry: true } : {};
|
|
219
|
+
}
|
|
216
220
|
function systemic(error) {
|
|
217
221
|
// A saved outcome still waiting for its trace (`TraceExportUnacknowledgedError`) is not
|
|
218
222
|
// systemic: the flush failed on an earlier attempt, telemetry may have recovered since, and
|
|
@@ -230,6 +234,7 @@ async function pool(items, concurrency, execute) {
|
|
|
230
234
|
let position = 0;
|
|
231
235
|
const failures = [];
|
|
232
236
|
let stop = false;
|
|
237
|
+
let cancelled;
|
|
233
238
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
234
239
|
while (position < items.length && !stop) {
|
|
235
240
|
const item = items[position++];
|
|
@@ -237,12 +242,20 @@ async function pool(items, concurrency, execute) {
|
|
|
237
242
|
await execute(item);
|
|
238
243
|
}
|
|
239
244
|
catch (error) {
|
|
245
|
+
// The caller's stop: start nothing more, and report the stop once the cases in flight settle.
|
|
246
|
+
if (error instanceof TargetCancelledError) {
|
|
247
|
+
cancelled = error;
|
|
248
|
+
stop = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
240
251
|
failures.push(error);
|
|
241
252
|
if (systemic(error))
|
|
242
253
|
stop = true;
|
|
243
254
|
}
|
|
244
255
|
}
|
|
245
256
|
}));
|
|
257
|
+
if (cancelled)
|
|
258
|
+
throw cancelled;
|
|
246
259
|
if (failures.length === 1)
|
|
247
260
|
throw failures[0];
|
|
248
261
|
if (failures.length) {
|
|
@@ -498,17 +511,19 @@ export async function runExperiment(options) {
|
|
|
498
511
|
scores,
|
|
499
512
|
exportState: "pending",
|
|
500
513
|
...(saved.trace ? { trace: saved.trace } : {}),
|
|
514
|
+
...(saved.attempt ? { attempt: saved.attempt } : {}),
|
|
501
515
|
};
|
|
502
516
|
await store.write(file, prepared);
|
|
503
517
|
return prepared;
|
|
504
518
|
}
|
|
505
519
|
// A case pinned to a world keeps its files apart and removes them when it ends: the agent's
|
|
506
520
|
// copies of its inputs, its work, the evaluator's downloads and the staged outputs.
|
|
507
|
-
|
|
521
|
+
// A retry gets its own, so an agent a dead run left behind cannot write into it.
|
|
522
|
+
const worldDirectoryOf = (itemId, attempt) => join(filesRoot, `world-case-${uuid(itemId)}${attempt ? `-${uuid(attempt)}` : ""}`);
|
|
508
523
|
const runCase = async (item, files) => {
|
|
509
524
|
const file = `case-${uuid(item.id)}`;
|
|
510
|
-
const
|
|
511
|
-
const caseDirectoryOf = (frozen) => frozen.environmentVersionId ?
|
|
525
|
+
const directDirectory = join(filesRoot, `case-${uuid(item.id)}`);
|
|
526
|
+
const caseDirectoryOf = (frozen) => frozen.environmentVersionId ? files.directory : directDirectory;
|
|
512
527
|
let checkpoint;
|
|
513
528
|
try {
|
|
514
529
|
checkpoint = await store.read(file);
|
|
@@ -521,6 +536,24 @@ export async function runExperiment(options) {
|
|
|
521
536
|
throw error;
|
|
522
537
|
}
|
|
523
538
|
files.keepOutputs = checkpoint?.stage === "uploading";
|
|
539
|
+
if (checkpoint && "attempt" in checkpoint)
|
|
540
|
+
files.directory = worldDirectoryOf(item.id, checkpoint.attempt);
|
|
541
|
+
let retryOf;
|
|
542
|
+
// A case left running by a process that died here is as interrupted as one the caller
|
|
543
|
+
// stopped; a fresh world isolates it from anything the dead run's agent still does.
|
|
544
|
+
if (checkpoint?.stage === "running" && store.reclaimed && options.rerunCrashedCases) {
|
|
545
|
+
checkpoint = { ...checkpoint, stage: "interrupted" };
|
|
546
|
+
await store.write(file, checkpoint);
|
|
547
|
+
}
|
|
548
|
+
if (checkpoint?.stage === "interrupted") {
|
|
549
|
+
retryOf = checkpoint.executionId;
|
|
550
|
+
checkpoint = undefined;
|
|
551
|
+
// A direct case's directory goes too, so the retry finds no input copy or output of the
|
|
552
|
+
// stopped attempt.
|
|
553
|
+
await removeCaseFiles(files.directory);
|
|
554
|
+
await removeCaseFiles(directDirectory);
|
|
555
|
+
files.directory = worldDirectoryOf(item.id, retryOf);
|
|
556
|
+
}
|
|
524
557
|
if (checkpoint && checkpoint.stage !== "prepared" && checkpoint.stage !== "uploading") {
|
|
525
558
|
if (checkpoint.stage === "serialization_failed")
|
|
526
559
|
throw new OutcomeSerializationError(checkpoint.executionId);
|
|
@@ -528,6 +561,7 @@ export async function runExperiment(options) {
|
|
|
528
561
|
? await options.client.startExecution(experiment.id, item.id, {
|
|
529
562
|
idempotencyKey: checkpoint.startKey,
|
|
530
563
|
traceExternalId: checkpoint.traceExternalId,
|
|
564
|
+
...retry(checkpoint.previousExecutionId),
|
|
531
565
|
})
|
|
532
566
|
: await options.client.getExecution(checkpoint.executionId);
|
|
533
567
|
throw new UncertainExecutionError(item.id, execution.id);
|
|
@@ -542,7 +576,9 @@ export async function runExperiment(options) {
|
|
|
542
576
|
files.keepOutputs = false;
|
|
543
577
|
}
|
|
544
578
|
if (!checkpoint) {
|
|
545
|
-
if (
|
|
579
|
+
if (options.signal?.aborted)
|
|
580
|
+
throw new TargetCancelledError();
|
|
581
|
+
if (item.execution && item.execution.id !== retryOf)
|
|
546
582
|
throw new UncertainExecutionError(item.id, item.execution.id);
|
|
547
583
|
const frozenCase = await options.client.getExperimentCase(experiment.id, item.id);
|
|
548
584
|
if (frozenCase.datasetVersionId !== version.id)
|
|
@@ -574,13 +610,16 @@ export async function runExperiment(options) {
|
|
|
574
610
|
stage: "starting",
|
|
575
611
|
startKey: randomUUID(),
|
|
576
612
|
traceExternalId: span.traceId,
|
|
613
|
+
...(retryOf ? { previousExecutionId: retryOf } : {}),
|
|
577
614
|
};
|
|
578
615
|
await store.write(file, start);
|
|
579
616
|
const execution = await options.client.startExecution(experiment.id, item.id, {
|
|
580
617
|
idempotencyKey: start.startKey,
|
|
581
618
|
traceExternalId: span.traceId,
|
|
619
|
+
...retry(retryOf),
|
|
582
620
|
});
|
|
583
|
-
|
|
621
|
+
const attempt = retryOf ? { attempt: retryOf } : {};
|
|
622
|
+
await store.write(file, { stage: "running", executionId: execution.id, ...attempt });
|
|
584
623
|
let state = "succeeded";
|
|
585
624
|
let output;
|
|
586
625
|
let generated;
|
|
@@ -604,6 +643,17 @@ export async function runExperiment(options) {
|
|
|
604
643
|
catch (error) {
|
|
605
644
|
if (error instanceof TargetOutcomeUncertainError)
|
|
606
645
|
throw error;
|
|
646
|
+
// A stop the caller asked for is no outcome: the execution stays open for the
|
|
647
|
+
// resumed run to replace instead of being scored as cancelled.
|
|
648
|
+
if (error instanceof TargetCancelledError && options.signal?.aborted) {
|
|
649
|
+
options.hue.recordError(span.span, error);
|
|
650
|
+
await store.write(file, {
|
|
651
|
+
stage: "interrupted",
|
|
652
|
+
executionId: execution.id,
|
|
653
|
+
...attempt,
|
|
654
|
+
});
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
607
657
|
state = error instanceof TargetCancelledError ? "cancelled" : "error";
|
|
608
658
|
targetError = error;
|
|
609
659
|
options.hue.recordError(span.span, error);
|
|
@@ -659,6 +709,7 @@ export async function runExperiment(options) {
|
|
|
659
709
|
...(options.persistResultContent && output !== undefined ? { output } : {}),
|
|
660
710
|
...(state === "error" ? { error: errorPayload(targetError) } : {}),
|
|
661
711
|
files: staged,
|
|
712
|
+
...attempt,
|
|
662
713
|
};
|
|
663
714
|
// Without persisted result content a restart cannot reconstruct the outcome; the
|
|
664
715
|
// saved stage then reports the execution as uncertain instead of guessing.
|
|
@@ -838,21 +889,23 @@ export async function runExperiment(options) {
|
|
|
838
889
|
// A world case's directory goes however the case ends, a forced exit included. An
|
|
839
890
|
// interrupted upload keeps only the staged outputs it resumes from; inputs are downloaded
|
|
840
891
|
// anew for scoring. A direct case has no such directory, so nothing is removed for it.
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
const untrack = onForcedExit(() => releaseWorldFilesSync(worldDirectory, files.keepOutputs));
|
|
892
|
+
const files = { keepOutputs: true, directory: worldDirectoryOf(item.id) };
|
|
893
|
+
const untrack = onForcedExit(() => releaseWorldFilesSync(files.directory, files.keepOutputs));
|
|
844
894
|
try {
|
|
845
895
|
await runCase(item, files);
|
|
846
|
-
await releaseWorldFiles(
|
|
896
|
+
await releaseWorldFiles(files.directory, false);
|
|
847
897
|
}
|
|
848
898
|
catch (error) {
|
|
849
|
-
await releaseWorldFiles(
|
|
899
|
+
await releaseWorldFiles(files.directory, files.keepOutputs);
|
|
850
900
|
throw error;
|
|
851
901
|
}
|
|
852
902
|
finally {
|
|
853
903
|
untrack();
|
|
854
904
|
}
|
|
855
905
|
});
|
|
906
|
+
// A stop that came while the last cases saved their outcomes still leaves the run open.
|
|
907
|
+
if (options.signal?.aborted)
|
|
908
|
+
throw new TargetCancelledError();
|
|
856
909
|
let finish = await store.read("finish");
|
|
857
910
|
if (!finish) {
|
|
858
911
|
finish = { key: randomUUID() };
|
package/dist/evals/simulation.js
CHANGED
|
@@ -391,7 +391,7 @@ export async function runSimulation(options) {
|
|
|
391
391
|
attempt.stage !== "completed" &&
|
|
392
392
|
attempt.scenarioDigest !== scenarioDigest &&
|
|
393
393
|
attempt.scenarioDigest !== previousDigest)
|
|
394
|
-
throw new Error("Recover the unfinished simulation before running a changed scenario or agent revision");
|
|
394
|
+
throw new Error("Recover the unfinished simulation before running a changed scenario or agent revision: run the unchanged one again to finish it, or use another checkpoint directory");
|
|
395
395
|
if (!attempt || attempt.stage === "completed") {
|
|
396
396
|
attempt = {
|
|
397
397
|
scenarioDigest,
|
|
@@ -417,12 +417,14 @@ export async function runSimulation(options) {
|
|
|
417
417
|
];
|
|
418
418
|
}
|
|
419
419
|
const experimentId = checkpointSegment(attempt.experimentId, "experiment id");
|
|
420
|
-
const runUrl = new URL(`/
|
|
420
|
+
const runUrl = new URL(`/runs/${experimentId}`, options.client.baseUrl).toString();
|
|
421
421
|
await options.onProgress?.({ type: "run_created", experimentId, runUrl });
|
|
422
422
|
const requested = requestedConfiguration
|
|
423
423
|
? pinRequestedAttemptV2(requestedConfiguration, (await options.client.getExperiment(experimentId)).config)
|
|
424
424
|
: undefined;
|
|
425
425
|
const report = await runExperiment({
|
|
426
|
+
signal: options.signal,
|
|
427
|
+
rerunCrashedCases: true,
|
|
426
428
|
client: options.client,
|
|
427
429
|
hue: options.hue,
|
|
428
430
|
experimentId,
|
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.
|
|
2
|
+
export declare const sdkVersion = "0.16.0";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hue-run/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"@opentelemetry/otlp-transformer": "0.222.0",
|
|
92
92
|
"@opentelemetry/resources": "2.11.0",
|
|
93
93
|
"@opentelemetry/sdk-logs": "0.222.0",
|
|
94
|
-
"@opentelemetry/sdk-trace": "2.11.0"
|
|
94
|
+
"@opentelemetry/sdk-trace": "2.11.0",
|
|
95
|
+
"zod": "^3.25.76 || ^4.6.5"
|
|
95
96
|
},
|
|
96
97
|
"devDependencies": {
|
|
97
98
|
"@ai-sdk/otel": "1.0.99",
|
|
@@ -102,15 +103,13 @@
|
|
|
102
103
|
"ai": "7.0.99",
|
|
103
104
|
"ajv": "8.20.0",
|
|
104
105
|
"protobufjs": "8.8.0",
|
|
105
|
-
"typescript": "7.0.2"
|
|
106
|
-
"zod": "4.6.5"
|
|
106
|
+
"typescript": "7.0.2"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
109
|
"@ai-sdk/otel": "^1.0.99",
|
|
110
110
|
"@opentelemetry/api": "^1.9.1",
|
|
111
111
|
"ai": "^6.0.0 || ^7.0.99",
|
|
112
|
-
"ajv": "^8.17.0"
|
|
113
|
-
"zod": "^4.6.5"
|
|
112
|
+
"ajv": "^8.17.0"
|
|
114
113
|
},
|
|
115
114
|
"peerDependenciesMeta": {
|
|
116
115
|
"@ai-sdk/otel": {
|
|
@@ -121,9 +120,6 @@
|
|
|
121
120
|
},
|
|
122
121
|
"ajv": {
|
|
123
122
|
"optional": true
|
|
124
|
-
},
|
|
125
|
-
"zod": {
|
|
126
|
-
"optional": true
|
|
127
123
|
}
|
|
128
124
|
},
|
|
129
125
|
"engines": {
|