@hue-run/sdk 0.12.0 → 0.12.1
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 +11 -6
- package/EVALUATIONS.md +3 -1
- package/dist/cli/eval.js +31 -8
- package/dist/evals/failure.d.ts +18 -0
- package/dist/evals/failure.js +150 -0
- package/dist/evals/local-worker.d.ts +1 -1
- package/dist/evals/runner.d.ts +21 -2
- package/dist/evals/runner.js +55 -4
- package/dist/evals/simulation.d.ts +1 -1
- package/dist/evals/types.d.ts +12 -4
- package/dist/evals.d.ts +1 -1
- 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.12.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.12.1`, `@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.12.
|
|
472
|
+
--archive .artifacts/typescript/hue-run-sdk-0.12.1.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.12.
|
|
483
|
+
--archive .artifacts/typescript/hue-run-sdk-0.12.1.tgz \
|
|
484
484
|
--origin https://STAGING_ORIGIN \
|
|
485
485
|
--project "$project" --command claim \
|
|
486
486
|
--evidence .context/setup-staging-after-claim.json
|
|
@@ -618,6 +618,11 @@ for example, needs a reviewed `attemptBaselineV2` (see
|
|
|
618
618
|
registers none, so put `"attemptBaselineV2"` in the run's configuration JSON when you launch the
|
|
619
619
|
run from Hue.
|
|
620
620
|
|
|
621
|
+
`--trace-not-accepted pending` completes a case whose telemetry Hue did not accept in time in its
|
|
622
|
+
true state with the trace still to arrive, carrying the count of spans the case ended; Hue scores
|
|
623
|
+
the sealed world at once and attaches the trace once its export is complete. The default,
|
|
624
|
+
`fail_case`, completes such a case as failed with its evidence omitted.
|
|
625
|
+
|
|
621
626
|
A Scenario or eval set whose dataset version is not saved cannot back an experiment: the command
|
|
622
627
|
exits 1 and asks for **Save eval-set version** in Hue or `--save-version`, which freezes that
|
|
623
628
|
version at its current revision. Connection settings are `HUE_API_KEY` and `HUE_BASE_URL`
|
|
@@ -663,9 +668,9 @@ an interrupted run without invoking the agent again; a different selection is re
|
|
|
663
668
|
unfinished one is resumed or its directory is removed.
|
|
664
669
|
|
|
665
670
|
Exit codes: `0` every case passed the evaluators that apply to it (advisory ones never count),
|
|
666
|
-
`1` a case failed, errored (including one no pinned evaluator applies to or decides), was skipped
|
|
667
|
-
or
|
|
668
|
-
still pending at `--wait`, `2` usage or configuration error (including a missing key), `130`
|
|
671
|
+
`1` a case failed, errored (including one no pinned evaluator applies to or decides), was skipped,
|
|
672
|
+
was inconclusive (a required judge could not run or answer; the table says `INCONCLUSIVE` and the
|
|
673
|
+
summary counts it apart from skipped cases) or Hue's checks were still pending at `--wait`, `2` usage or configuration error (including a missing key), `130`
|
|
669
674
|
interrupted. On Node.js 22, load TypeScript adapters with `NODE_OPTIONS=--experimental-strip-types`;
|
|
670
675
|
non-erasable syntax (enums, parameter properties, namespaces) needs a loader such as `--import tsx`
|
|
671
676
|
on any Node.js version.
|
package/EVALUATIONS.md
CHANGED
|
@@ -116,7 +116,7 @@ Both choices are required and independent:
|
|
|
116
116
|
|
|
117
117
|
Frozen dataset inputs/references already exist on Hue. Their presence is independent of these switches. No output is inferred from telemetry. JavaScript `undefined` means unavailable output; JSON `null`, false, zero and empty string remain present values. Historical subjects with unavailable output produce skipped results without executing a scorer callback. A failed quality metric remains `state:"scored"` with `passed:false`. Target errors and scorer errors remain separate. Target error types are generalized to `TargetError`; when content is enabled, bounded messages may be stored. Never put credentials in error messages, output, custom metric values, names or metadata.
|
|
118
118
|
|
|
119
|
-
`traceEvidence:{mode:"required"}` waits for trace and log export acknowledgement after the root span ends. The transport is shared by every case in flight, so each export issue names the traces of the records it concerned (`ExportIssue.traceIds`): a refused or rejected batch, a dropped record, a capture failure inside a case. A failure that names the case's trace is that case's own and final (its checkpoint reads `failed`, and resume refuses it); one that names only other traces leaves the case accepted; one that names no trace (a processor flush that failed) or a rejection in a batch of several traces is decided by the case's trace receipt, at once or on resume for a checkpoint still `pending`: the receipt must hold the case's root span and at least as many spans as the case ended (the checkpoint records the count at the flush; without it the receipt does not accept). The explicit alternative `{mode:"omit",reason:"..."}` stores the omission reason and declared trace ID without a fake snapshot. It can complete despite an export failure; export diagnostics remain available on the Hue client. There is no automatic fallback to omission. When required telemetry is not accepted, the runner by default keeps the saved outcome, leaves the execution started and rejects (`traceNotAccepted: "stop"`). With `traceNotAccepted: "fail_case"` it instead completes that case as failed (a `TelemetryNotAccepted` error, the evidence omitted with a reason starting `telemetry_not_accepted`, and no output or generated files attached, so no scorer can pass it), calls `onTelemetryNotAccepted(entry)` once, as soon as it completes the case (a resumed call only lists it), lists it in `report.telemetryNotAccepted` with sanitized issue counts, and goes on; `hue eval` uses this. A scorer that grades only the world can still score such a case, so treat a listed case as failed whatever its results say, as `hue eval` does.
|
|
119
|
+
`traceEvidence:{mode:"required"}` waits for trace and log export acknowledgement after the root span ends. The transport is shared by every case in flight, so each export issue names the traces of the records it concerned (`ExportIssue.traceIds`): a refused or rejected batch, a dropped record, a capture failure inside a case. A failure that names the case's trace is that case's own and final (its checkpoint reads `failed`, and resume refuses it); one that names only other traces leaves the case accepted; one that names no trace (a processor flush that failed) or a rejection in a batch of several traces is decided by the case's trace receipt, at once or on resume for a checkpoint still `pending`: the receipt must hold the case's root span and at least as many spans as the case ended (the checkpoint records the count at the flush; without it the receipt does not accept). The explicit alternative `{mode:"omit",reason:"..."}` stores the omission reason and declared trace ID without a fake snapshot. It can complete despite an export failure; export diagnostics remain available on the Hue client. There is no automatic fallback to omission. When required telemetry is not accepted, the runner by default keeps the saved outcome, leaves the execution started and rejects (`traceNotAccepted: "stop"`). With `traceNotAccepted: "fail_case"` it instead completes that case as failed (a `TelemetryNotAccepted` error, the evidence omitted with a reason starting `telemetry_not_accepted`, and no output or generated files attached, so no scorer can pass it), calls `onTelemetryNotAccepted(entry)` once, as soon as it completes the case (a resumed call only lists it), lists it in `report.telemetryNotAccepted` with sanitized issue counts, and goes on; `hue eval` uses this. A scorer that grades only the world can still score such a case, so treat a listed case as failed whatever its results say, as `hue eval` does. `traceNotAccepted:"pending"` completes a case whose telemetry Hue did not accept in time in its true state with `traceEvidence:"pending"` and the count of spans the case ended (`traceSpanCount`): Hue scores the sealed world at once, attaches the trace once it holds every span (a root span alone is not the trace), or records the evidence omitted a day later; such cases are listed in `RunnerReport.tracePending`. A saved outcome whose export the receipt does not accept on resume completes the same way. `hue eval --trace-not-accepted pending` chooses it for the CLI.
|
|
120
120
|
|
|
121
121
|
## Local scorers
|
|
122
122
|
|
|
@@ -371,6 +371,8 @@ files directory must be owned by the current user and closed to everyone else (m
|
|
|
371
371
|
target again; if result content is not persisted, the JSON output cannot be reconstructed and the
|
|
372
372
|
case is reported as uncertain.
|
|
373
373
|
|
|
374
|
+
A target that throws completes its execution as `error` with a type that says what stopped the case. `TargetError` is the agent's own failure. A service the agent called that refused with a retryable status (408, 429 or 5xx, such as a model provider's rate limit or outage, when the error carries the exchange: headers, a response or a URL) is `ServiceRefused`; a connection that failed (`ECONNRESET`, `ECONNREFUSED`, `fetch failed`, an `APIConnectionError`) is `ConnectionFailed`; a call that timed out (`TimeoutError`, `APITimeoutError`) is `TimedOut`. The runner reads the error's `cause`, `lastError` and `errors` chains for these signals. Hue counts the three as infrastructure and leaves them out of the agent's pass rate; a refusal the agent caused (a 400 from a model provider or another service) stays `TargetError`. Any other status Hue's own clients answered with (a 4xx: a key, a project, an environment version or a world Hue refused) is `ConfigurationRejected`, which Hue counts as configuration and also leaves out of the agent's pass rate: an error from Hue's own clients is never the agent's. The clients fetch with `redirect: "error"`, so a redirect from the configured endpoint surfaces without a status, as `ConnectionFailed`. A runner of your own that completes an execution with a `type` of its own can say which class Hue files it under with the error's `class`.
|
|
375
|
+
|
|
374
376
|
`rescore` downloads a subject's frozen `files` — the pinned inputs and the generated outputs, with
|
|
375
377
|
`role: "output"` for the documents a run produced — so a code evaluator can grade saved documents
|
|
376
378
|
without invoking an agent. Local scorers receive them as `context.files` exactly as they do during
|
package/dist/cli/eval.js
CHANGED
|
@@ -83,16 +83,28 @@ Output and limits:
|
|
|
83
83
|
--save-version Freeze an unsaved eval-set version before running
|
|
84
84
|
--checkpoint-dir <path> Private checkpoint directory (default: .hue/eval/<agent-key>)
|
|
85
85
|
--concurrency <n> Cases in flight, 1-64 (default: 1)
|
|
86
|
+
--trace-not-accepted <policy> A case whose telemetry Hue did not accept in time: fail_case
|
|
87
|
+
(default) completes it as failed with the evidence omitted;
|
|
88
|
+
pending completes it in its true state with the trace still to
|
|
89
|
+
arrive, which Hue attaches once its export is complete
|
|
86
90
|
--timeout <seconds> Per-case --command timeout (default: 600)
|
|
87
91
|
--wait <seconds> Verdict wait after the run finishes (default: 300)
|
|
88
92
|
-h, --help Show this help
|
|
89
93
|
|
|
90
94
|
HUE_API_KEY must be a "Read and write" project key; it is never printed.
|
|
91
95
|
Code evaluators pinned to a direct run are graded by Hue's executor after the upload; the wait
|
|
92
|
-
covers them. Exit codes: 0 every case passed, 1 a case failed, errored or is
|
|
93
|
-
2 usage error, 130 interrupted.
|
|
96
|
+
covers them. Exit codes: 0 every case passed, 1 a case failed, errored, is inconclusive or is
|
|
97
|
+
incomplete, 2 usage error, 130 interrupted.
|
|
94
98
|
`;
|
|
95
99
|
/** Thrown for invalid arguments or configuration; exits with status 2. */
|
|
100
|
+
/** The `--trace-not-accepted` policy: the failed-case default, or pending evidence. */
|
|
101
|
+
function traceNotAcceptedPolicy(value) {
|
|
102
|
+
if (value === undefined || value === "fail_case")
|
|
103
|
+
return "fail_case";
|
|
104
|
+
if (value === "pending")
|
|
105
|
+
return "pending";
|
|
106
|
+
throw new UsageError("--trace-not-accepted must be fail_case or pending");
|
|
107
|
+
}
|
|
96
108
|
class UsageError extends Error {
|
|
97
109
|
constructor(message) {
|
|
98
110
|
super(message);
|
|
@@ -129,6 +141,7 @@ function parse(argv) {
|
|
|
129
141
|
json: { type: "boolean", default: false },
|
|
130
142
|
content: { type: "boolean", default: false },
|
|
131
143
|
"no-output": { type: "boolean", default: false },
|
|
144
|
+
"trace-not-accepted": { type: "string" },
|
|
132
145
|
"save-version": { type: "boolean", default: false },
|
|
133
146
|
"checkpoint-dir": { type: "string" },
|
|
134
147
|
concurrency: { type: "string" },
|
|
@@ -702,6 +715,11 @@ const STATE_LABEL = {
|
|
|
702
715
|
skipped: "SKIPPED",
|
|
703
716
|
pending: "PENDING",
|
|
704
717
|
};
|
|
718
|
+
/** A skipped case whose deciding evaluator could not decide it, as an answer-graded case whose
|
|
719
|
+
* required judge could not run or answer: Hue's explanation begins "Inconclusive:". It is named
|
|
720
|
+
* apart from a case its evaluators did not grade, since it needs a judge, not another pin. */
|
|
721
|
+
const inconclusive = (item) => item.state === "skipped" && item.explanations.some((text) => /^inconclusive:/i.test(text));
|
|
722
|
+
const stateLabel = (item) => inconclusive(item) ? "INCONCLUSIVE" : STATE_LABEL[item.state];
|
|
705
723
|
export function renderTable(verdicts, output) {
|
|
706
724
|
const cases = verdicts.summary.cases;
|
|
707
725
|
// A column per metric name; a name several evaluators report, such as two judges' `verdict`,
|
|
@@ -736,7 +754,7 @@ export function renderTable(verdicts, output) {
|
|
|
736
754
|
// reporting the metric shows "-".
|
|
737
755
|
return item.notApplicable?.includes(column.version) ? "n/a" : "-";
|
|
738
756
|
}),
|
|
739
|
-
|
|
757
|
+
stateLabel(item),
|
|
740
758
|
]);
|
|
741
759
|
const widths = header.map((cell, index) => Math.max(cell.length, ...rows.map((row) => row[index].length)));
|
|
742
760
|
const line = (cells) => cells
|
|
@@ -751,15 +769,18 @@ export function renderTable(verdicts, output) {
|
|
|
751
769
|
if (item.state === "passed")
|
|
752
770
|
continue;
|
|
753
771
|
const details = [...item.errors.map((type) => `scorer error ${type}`), ...item.explanations];
|
|
754
|
-
for (const detail of details.length ? details : [
|
|
772
|
+
for (const detail of details.length ? details : [stateLabel(item).toLowerCase()])
|
|
755
773
|
output.log(` ${item.externalKey}: ${detail}`);
|
|
756
774
|
}
|
|
757
775
|
const totals = verdicts.summary.totals;
|
|
776
|
+
const inconclusiveCount = cases.filter(inconclusive).length;
|
|
777
|
+
const skipped = totals.skipped - inconclusiveCount;
|
|
758
778
|
const advisoryFailures = cases.reduce((count, item) => count +
|
|
759
779
|
item.metrics.filter((metric) => item.advisory?.includes(metric.scorerVersionId) && !metricPassed(metric)).length, 0);
|
|
760
780
|
const extra = [
|
|
761
781
|
totals.error ? `${totals.error} error` : "",
|
|
762
|
-
|
|
782
|
+
inconclusiveCount ? `${inconclusiveCount} inconclusive` : "",
|
|
783
|
+
skipped > 0 ? `${skipped} skipped` : "",
|
|
763
784
|
totals.pending ? `${totals.pending} pending` : "",
|
|
764
785
|
totals.notApplicable
|
|
765
786
|
? `${totals.notApplicable} evaluator result${totals.notApplicable === 1 ? "" : "s"} not applicable`
|
|
@@ -1116,7 +1137,7 @@ async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
|
1116
1137
|
persistResultContent: !values["no-output"],
|
|
1117
1138
|
traceEvidence: { mode: "required" },
|
|
1118
1139
|
// A case whose telemetry Hue did not accept fails instead of staying started.
|
|
1119
|
-
traceNotAccepted: "
|
|
1140
|
+
traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
|
|
1120
1141
|
onTelemetryNotAccepted: telemetry.report,
|
|
1121
1142
|
concurrency,
|
|
1122
1143
|
agentRevision: agent.revision,
|
|
@@ -1221,7 +1242,7 @@ async function runDirect(run, connection) {
|
|
|
1221
1242
|
// only the telemetry.
|
|
1222
1243
|
persistResultContent: !values["no-output"],
|
|
1223
1244
|
traceEvidence: { mode: "required" },
|
|
1224
|
-
traceNotAccepted: "
|
|
1245
|
+
traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
|
|
1225
1246
|
onTelemetryNotAccepted: telemetry.report,
|
|
1226
1247
|
concurrency: run.concurrency,
|
|
1227
1248
|
scorers: [],
|
|
@@ -1297,7 +1318,7 @@ async function runWorker(values, connection, adapter, agent, hue, output, signal
|
|
|
1297
1318
|
},
|
|
1298
1319
|
scorers: [],
|
|
1299
1320
|
concurrency,
|
|
1300
|
-
traceNotAccepted: "
|
|
1321
|
+
traceNotAccepted: traceNotAcceptedPolicy(values["trace-not-accepted"]),
|
|
1301
1322
|
onTelemetryNotAccepted: telemetry.report,
|
|
1302
1323
|
deprecationWarnings: false,
|
|
1303
1324
|
signal,
|
|
@@ -1402,6 +1423,8 @@ export async function runEvalCommand(argv) {
|
|
|
1402
1423
|
let json = false;
|
|
1403
1424
|
try {
|
|
1404
1425
|
const { values, positionals } = parse(argv);
|
|
1426
|
+
// A usage error before anything is prepared or created: no run is left to recover from it.
|
|
1427
|
+
traceNotAcceptedPolicy(values["trace-not-accepted"]);
|
|
1405
1428
|
json = values.json;
|
|
1406
1429
|
const output = {
|
|
1407
1430
|
log: (line) => (json ? process.stderr : process.stdout).write(`${line}\n`),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a target's throw shows about a service the agent called, when a service rather than the
|
|
3
|
+
* agent's own code stopped it: `ServiceRefused` for a retryable HTTP status (408, 429 or 5xx) a
|
|
4
|
+
* model provider, Hue or another service answered with, such as a rate limit or an outage;
|
|
5
|
+
* `ConnectionFailed` for a connection that failed; `TimedOut` for a call that timed out; and
|
|
6
|
+
* `ConfigurationRejected` for any other status Hue's own clients answered with (a 4xx: a key, a
|
|
7
|
+
* project, an environment version or a world Hue refused), which is the caller's configuration;
|
|
8
|
+
* the clients fetch with `redirect: "error"`, so a redirect surfaces without a status, as a
|
|
9
|
+
* failed connection.
|
|
10
|
+
* The runner records the type on the execution in place of `TargetError`; Hue classes the first
|
|
11
|
+
* three as infrastructure and the last as configuration, so an error from Hue's own clients is
|
|
12
|
+
* never filed as the agent's. Anything else is the agent's own error.
|
|
13
|
+
*/
|
|
14
|
+
export type ServiceFailureType = "ServiceRefused" | "ConnectionFailed" | "TimedOut" | "ConfigurationRejected";
|
|
15
|
+
/** The service failure a target's throw shows, by the errors linked from it, in precedence
|
|
16
|
+
* order: a timeout, a failed connection, a retryable refusal, a status Hue's own client was
|
|
17
|
+
* refused with; null for the agent's own error. */
|
|
18
|
+
export declare function serviceFailureType(error: unknown): ServiceFailureType | null;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { HueConnectionError } from "../client.js";
|
|
2
|
+
import { HueEnvironmentError } from "../environment/client.js";
|
|
3
|
+
import { HueApiError } from "./client.js";
|
|
4
|
+
const TIMEOUT_NAMES = new Set(["TimeoutError", "APIConnectionTimeoutError", "APITimeoutError"]);
|
|
5
|
+
/** Node's and Undici's codes for a call that timed out, connecting or waiting on the answer. */
|
|
6
|
+
const TIMEOUT_CODES = new Set([
|
|
7
|
+
"ETIMEDOUT",
|
|
8
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
9
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
10
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
11
|
+
]);
|
|
12
|
+
const NETWORK_CODES = new Set([
|
|
13
|
+
"ECONNRESET",
|
|
14
|
+
"ECONNREFUSED",
|
|
15
|
+
"ENOTFOUND",
|
|
16
|
+
"EAI_AGAIN",
|
|
17
|
+
"EPIPE",
|
|
18
|
+
"ENETUNREACH",
|
|
19
|
+
"EHOSTUNREACH",
|
|
20
|
+
"UND_ERR_SOCKET",
|
|
21
|
+
]);
|
|
22
|
+
/** Links followed from the thrown error, and errors read at most. */
|
|
23
|
+
const MAX_DEPTH = 6;
|
|
24
|
+
const MAX_ERRORS = 64;
|
|
25
|
+
/** The errors an error links to: its `cause`, and the `errors` and `lastError` of an
|
|
26
|
+
* `AggregateError` or a retry error. A property that throws when read links nothing. */
|
|
27
|
+
function links(error) {
|
|
28
|
+
try {
|
|
29
|
+
const { cause, lastError, errors } = error;
|
|
30
|
+
return [cause, lastError, ...(Array.isArray(errors) ? errors.slice(0, MAX_ERRORS) : [])];
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The thrown error and every error within `MAX_DEPTH` links of it, each once, so a cycle ends
|
|
37
|
+
* the walk. */
|
|
38
|
+
function linked(error) {
|
|
39
|
+
const found = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
let level = [error];
|
|
42
|
+
for (let depth = 0; depth <= MAX_DEPTH && level.length; depth++) {
|
|
43
|
+
const next = [];
|
|
44
|
+
for (const item of level) {
|
|
45
|
+
if (typeof item !== "object" || item === null || seen.has(item))
|
|
46
|
+
continue;
|
|
47
|
+
if (found.length === MAX_ERRORS)
|
|
48
|
+
return found;
|
|
49
|
+
seen.add(item);
|
|
50
|
+
found.push(item);
|
|
51
|
+
next.push(...links(item));
|
|
52
|
+
}
|
|
53
|
+
level = next;
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
/** The error's `name` and its class names, since some SDKs never set `name`. */
|
|
58
|
+
function names(error) {
|
|
59
|
+
const found = [];
|
|
60
|
+
const name = error.name;
|
|
61
|
+
if (typeof name === "string")
|
|
62
|
+
found.push(name);
|
|
63
|
+
let prototype = Object.getPrototypeOf(error);
|
|
64
|
+
for (let depth = 0; depth < MAX_DEPTH && prototype; depth++) {
|
|
65
|
+
const constructor = prototype.constructor;
|
|
66
|
+
if (typeof constructor?.name === "string")
|
|
67
|
+
found.push(constructor.name);
|
|
68
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
const retryable = (status) => typeof status === "number" &&
|
|
73
|
+
(status === 408 || status === 429 || (status >= 500 && status < 600));
|
|
74
|
+
function timedOut(error) {
|
|
75
|
+
const { code } = error;
|
|
76
|
+
return ((typeof code === "string" && TIMEOUT_CODES.has(code)) ||
|
|
77
|
+
names(error).some((name) => TIMEOUT_NAMES.has(name)));
|
|
78
|
+
}
|
|
79
|
+
/** An error one of Hue's own clients threw, which carries the status Hue answered or none. */
|
|
80
|
+
function hueClientError(error) {
|
|
81
|
+
return (error instanceof HueConnectionError ||
|
|
82
|
+
error instanceof HueEnvironmentError ||
|
|
83
|
+
error instanceof HueApiError);
|
|
84
|
+
}
|
|
85
|
+
function disconnected(error) {
|
|
86
|
+
// Hue's clients report a request that got no usable response without a status.
|
|
87
|
+
if (hueClientError(error) && error.status === undefined)
|
|
88
|
+
return true;
|
|
89
|
+
const { code, message } = error;
|
|
90
|
+
const classes = names(error);
|
|
91
|
+
return ((typeof code === "string" && NETWORK_CODES.has(code)) ||
|
|
92
|
+
classes.includes("APIConnectionError") ||
|
|
93
|
+
(classes.includes("TypeError") && message === "fetch failed"));
|
|
94
|
+
}
|
|
95
|
+
/** A retryable status a service answered with: on Hue's own clients, or on an error that also
|
|
96
|
+
* carries the exchange it came from (the OpenAI and Anthropic SDKs' `headers`, the AI SDK's
|
|
97
|
+
* `url` or `responseHeaders`, a fetch `response`), so an agent's own error that only names a
|
|
98
|
+
* status stays the agent's. */
|
|
99
|
+
function refused(error) {
|
|
100
|
+
if (hueClientError(error))
|
|
101
|
+
return retryable(error.status);
|
|
102
|
+
const { status, statusCode, headers, response, url, responseHeaders } = error;
|
|
103
|
+
const exchanged = [headers, response, url, responseHeaders].some((item) => item !== undefined && item !== null);
|
|
104
|
+
// A fetch `Response` carries the status itself.
|
|
105
|
+
const answered = response !== null && typeof response === "object"
|
|
106
|
+
? response
|
|
107
|
+
: {};
|
|
108
|
+
return (exchanged &&
|
|
109
|
+
(retryable(status) ||
|
|
110
|
+
retryable(statusCode) ||
|
|
111
|
+
retryable(answered.status) ||
|
|
112
|
+
retryable(answered.statusCode)));
|
|
113
|
+
}
|
|
114
|
+
/** A status Hue's own client answered with that is neither a success nor retryable, a 3xx or a
|
|
115
|
+
* 4xx: Hue refused the caller's key, project, environment version or world, so the case could
|
|
116
|
+
* not run as configured. The clients fetch with `redirect: "error"`, so a redirect reaches here
|
|
117
|
+
* only as a status some caller constructed; a real one surfaces without a status, as a failed
|
|
118
|
+
* connection. Only Hue's clients show it; another service's 4xx stays the agent's. */
|
|
119
|
+
function rejected(error) {
|
|
120
|
+
const { status } = error;
|
|
121
|
+
return (hueClientError(error) &&
|
|
122
|
+
typeof status === "number" &&
|
|
123
|
+
status >= 300 &&
|
|
124
|
+
status < 500 &&
|
|
125
|
+
!retryable(status));
|
|
126
|
+
}
|
|
127
|
+
/** Whether any error shows the signal; one whose properties throw when read shows none. */
|
|
128
|
+
const any = (errors, signal) => errors.some((error) => {
|
|
129
|
+
try {
|
|
130
|
+
return signal(error);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
/** The service failure a target's throw shows, by the errors linked from it, in precedence
|
|
137
|
+
* order: a timeout, a failed connection, a retryable refusal, a status Hue's own client was
|
|
138
|
+
* refused with; null for the agent's own error. */
|
|
139
|
+
export function serviceFailureType(error) {
|
|
140
|
+
const errors = linked(error);
|
|
141
|
+
if (any(errors, timedOut))
|
|
142
|
+
return "TimedOut";
|
|
143
|
+
if (any(errors, disconnected))
|
|
144
|
+
return "ConnectionFailed";
|
|
145
|
+
if (any(errors, refused))
|
|
146
|
+
return "ServiceRefused";
|
|
147
|
+
if (any(errors, rejected))
|
|
148
|
+
return "ConfigurationRejected";
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
@@ -104,7 +104,7 @@ export interface RunLocalAgentOptions {
|
|
|
104
104
|
concurrency?: number;
|
|
105
105
|
/** What a case does when its telemetry is not accepted; see
|
|
106
106
|
* `traceNotAccepted` of {@link runExperiment}. Defaults to `"stop"`. */
|
|
107
|
-
traceNotAccepted?: "stop" | "fail_case";
|
|
107
|
+
traceNotAccepted?: "stop" | "fail_case" | "pending";
|
|
108
108
|
/** Called as each case is failed for its telemetry; see `onTelemetryNotAccepted` of
|
|
109
109
|
* {@link runExperiment}. */
|
|
110
110
|
onTelemetryNotAccepted?(entry: TelemetryNotAccepted): void | Promise<void>;
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -123,9 +123,13 @@ export interface RunExperimentOptions extends RunnerOptions {
|
|
|
123
123
|
* rejects. `"fail_case"` completes the execution as failed instead, with the evidence omitted
|
|
124
124
|
* under the reason `telemetry_not_accepted` and without its output or generated files, so no
|
|
125
125
|
* scorer can pass it; reports it in {@link RunnerReport.telemetryNotAccepted} and goes on with
|
|
126
|
-
* the other cases.
|
|
126
|
+
* the other cases. `"pending"` completes the execution in its true state with
|
|
127
|
+
* `traceEvidence: "pending"` and the count of spans the case ended: Hue scores the sealed world
|
|
128
|
+
* at once and attaches the trace once it holds every span, or records the evidence omitted a
|
|
129
|
+
* day later; reports it in {@link RunnerReport.tracePending} and goes on. A saved outcome whose
|
|
130
|
+
* export the receipt does not accept on resume completes the same way.
|
|
127
131
|
*/
|
|
128
|
-
traceNotAccepted?: "stop" | "fail_case";
|
|
132
|
+
traceNotAccepted?: "stop" | "fail_case" | "pending";
|
|
129
133
|
/** Called when this call completes a case as failed under `traceNotAccepted: "fail_case"`,
|
|
130
134
|
* before the run goes on, so the failure is known even if a later case stops the run. A call
|
|
131
135
|
* that resumes an already completed case lists it in the report without calling again. */
|
|
@@ -152,6 +156,21 @@ export interface RunnerReport {
|
|
|
152
156
|
/** Cases completed as failed because their telemetry was not accepted, under
|
|
153
157
|
* `traceNotAccepted: "fail_case"`; absent when there were none. */
|
|
154
158
|
telemetryNotAccepted?: TelemetryNotAccepted[];
|
|
159
|
+
/** Cases completed with their trace evidence pending, under `traceNotAccepted: "pending"`;
|
|
160
|
+
* absent when there were none. */
|
|
161
|
+
tracePending?: TracePending[];
|
|
162
|
+
}
|
|
163
|
+
/** A case completed with its trace still to arrive. */
|
|
164
|
+
export interface TracePending {
|
|
165
|
+
/** Frozen case ID. */
|
|
166
|
+
caseId: string;
|
|
167
|
+
/** The case's external key. */
|
|
168
|
+
caseKey: string;
|
|
169
|
+
/** Execution completed pending. */
|
|
170
|
+
executionId: string;
|
|
171
|
+
/** How many spans the case ended, which Hue holds the arriving trace to; absent when the
|
|
172
|
+
* checkpoint recorded no count. */
|
|
173
|
+
spanCount?: number;
|
|
155
174
|
}
|
|
156
175
|
/** Export issue counts, never content: which signal, what happened, the HTTP status when Hue
|
|
157
176
|
* answered, and how many records. */
|
package/dist/evals/runner.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ROOT_CONTEXT } from "@opentelemetry/api";
|
|
|
6
6
|
import { HueExportError } from "../transport.js";
|
|
7
7
|
import { HueApiError } from "./client.js";
|
|
8
8
|
import { loadEnvironmentEvidence } from "./environment-evidence.js";
|
|
9
|
+
import { serviceFailureType } from "./failure.js";
|
|
9
10
|
import { CheckpointStore } from "./checkpoint.js";
|
|
10
11
|
import { onForcedExit } from "./exit-cleanup.js";
|
|
11
12
|
import { assertSafeFileNames, downloadCaseFiles, downloadCaseInputs, localOutputFiles, verifyCaseFiles, OutputFileError, stageOutputFiles, targetFileRoles, uploadOutputFiles, } from "./files.js";
|
|
@@ -252,6 +253,20 @@ async function pool(items, concurrency, execute) {
|
|
|
252
253
|
/** Whether Hue holds the case's telemetry, read from the trace receipt: its root span and at
|
|
253
254
|
* least as many spans as the case ended; false when they are not there by the wait's end or the
|
|
254
255
|
* receipt cannot be read, and for a checkpoint an older runner saved without the span. */
|
|
256
|
+
/** Turn a prepared completion into a pending one: the trace is declared but still to arrive,
|
|
257
|
+
* and the span count the checkpoint recorded at the flush goes with it, so Hue freezes the trace
|
|
258
|
+
* only once it holds every span the case ended, never a root span alone. */
|
|
259
|
+
function pendingCompletion(prepared) {
|
|
260
|
+
prepared.complete.traceEvidence = "pending";
|
|
261
|
+
delete prepared.complete.omissionReason;
|
|
262
|
+
if (prepared.trace?.spans !== undefined)
|
|
263
|
+
prepared.complete.traceSpanCount = prepared.trace.spans;
|
|
264
|
+
}
|
|
265
|
+
/** The reverse, once the receipt accepted the trace after all: required evidence, no count. */
|
|
266
|
+
function acceptedCompletion(prepared) {
|
|
267
|
+
prepared.complete.traceEvidence = "required";
|
|
268
|
+
delete prepared.complete.traceSpanCount;
|
|
269
|
+
}
|
|
255
270
|
async function traceLanded(hue, trace) {
|
|
256
271
|
// Without the count of spans the case ended, the receipt cannot tell a lost child span from a
|
|
257
272
|
// landed trace, and does not accept.
|
|
@@ -417,13 +432,15 @@ export async function runExperiment(options) {
|
|
|
417
432
|
const filesRoot = resolve(options.filesDirectory ?? join(options.checkpointDirectory, "files"));
|
|
418
433
|
const sanitize = (message) => message.slice(0, 4000).toWellFormed().replaceAll("\u0000", "");
|
|
419
434
|
// A target stopped at its world's deadline is a `TargetTimeout`, told apart from an error the
|
|
420
|
-
// target raised itself: the agent did not finish, rather than finishing wrongly.
|
|
435
|
+
// target raised itself: the agent did not finish, rather than finishing wrongly. A service the
|
|
436
|
+
// agent called that refused, dropped the connection or timed out is named apart from the
|
|
437
|
+
// agent's own error (`failure.ts`), so Hue counts it as infrastructure, not as the agent.
|
|
421
438
|
const errorPayload = (error) => ({
|
|
422
439
|
type: error instanceof OutputTooLargeError
|
|
423
440
|
? "OutputTooLarge"
|
|
424
441
|
: error instanceof TargetTimeoutError
|
|
425
442
|
? "TargetTimeout"
|
|
426
|
-
: "TargetError",
|
|
443
|
+
: (serviceFailureType(error) ?? "TargetError"),
|
|
427
444
|
...(options.persistResultContent && error instanceof Error
|
|
428
445
|
? { message: sanitize(error.message) }
|
|
429
446
|
: {}),
|
|
@@ -707,7 +724,15 @@ export async function runExperiment(options) {
|
|
|
707
724
|
if (options.traceEvidence.mode !== "omit")
|
|
708
725
|
throw error;
|
|
709
726
|
}
|
|
710
|
-
if (exportError !== undefined &&
|
|
727
|
+
if (exportError !== undefined &&
|
|
728
|
+
options.traceEvidence.mode !== "omit" &&
|
|
729
|
+
options.traceNotAccepted === "pending") {
|
|
730
|
+
// The outcome stands as the target gave it; Hue attaches the trace once it holds
|
|
731
|
+
// every span the case ended, which the checkpoint's count tells it.
|
|
732
|
+
pendingCompletion(checkpoint);
|
|
733
|
+
await store.write(file, checkpoint);
|
|
734
|
+
}
|
|
735
|
+
else if (exportError !== undefined && options.traceEvidence.mode !== "omit") {
|
|
711
736
|
if (options.traceNotAccepted !== "fail_case")
|
|
712
737
|
throw exportError;
|
|
713
738
|
// Required evidence that Hue did not accept fails the case rather than leaving its
|
|
@@ -746,9 +771,26 @@ export async function runExperiment(options) {
|
|
|
746
771
|
prepared.complete.traceEvidence !== "omit" &&
|
|
747
772
|
(await traceLanded(options.hue, prepared.trace))) {
|
|
748
773
|
prepared.exportState = "accepted";
|
|
774
|
+
// A completion made pending by an earlier receipt read is required again: the trace
|
|
775
|
+
// landed, so Hue freezes it at completion as for any accepted case.
|
|
776
|
+
if (prepared.complete.traceEvidence === "pending")
|
|
777
|
+
acceptedCompletion(prepared);
|
|
778
|
+
await store.write(file, prepared);
|
|
779
|
+
}
|
|
780
|
+
// Under the pending policy a saved outcome the receipt does not accept, whether no flush
|
|
781
|
+
// decided it or an export failure named its trace, completes with the trace pending rather
|
|
782
|
+
// than being refused: Hue attaches the trace when it arrives in full.
|
|
783
|
+
if (options.traceNotAccepted === "pending" &&
|
|
784
|
+
prepared.exportState !== "accepted" &&
|
|
785
|
+
prepared.exportState !== "not_accepted" &&
|
|
786
|
+
prepared.complete.traceEvidence !== "omit" &&
|
|
787
|
+
prepared.complete.traceEvidence !== "pending") {
|
|
788
|
+
pendingCompletion(prepared);
|
|
749
789
|
await store.write(file, prepared);
|
|
750
790
|
}
|
|
751
|
-
if (prepared.exportState !== "accepted" &&
|
|
791
|
+
if (prepared.exportState !== "accepted" &&
|
|
792
|
+
prepared.complete.traceEvidence !== "omit" &&
|
|
793
|
+
prepared.complete.traceEvidence !== "pending")
|
|
752
794
|
throw new TraceExportUnacknowledgedError(prepared.executionId);
|
|
753
795
|
const save = () => store.write(file, prepared);
|
|
754
796
|
const notAccepted = prepared.exportState === "not_accepted"
|
|
@@ -770,6 +812,15 @@ export async function runExperiment(options) {
|
|
|
770
812
|
}
|
|
771
813
|
if (notAccepted)
|
|
772
814
|
(report.telemetryNotAccepted ??= []).push(notAccepted);
|
|
815
|
+
if (prepared.complete.traceEvidence === "pending")
|
|
816
|
+
(report.tracePending ??= []).push({
|
|
817
|
+
caseId: item.id,
|
|
818
|
+
caseKey: item.externalKey,
|
|
819
|
+
executionId: prepared.executionId,
|
|
820
|
+
...(prepared.complete.traceSpanCount !== undefined
|
|
821
|
+
? { spanCount: prepared.complete.traceSpanCount }
|
|
822
|
+
: {}),
|
|
823
|
+
});
|
|
773
824
|
const results = await uploadScores(options, experiment.evaluation.id, prepared.scores, save, versions);
|
|
774
825
|
report.subjectIds.push(prepared.completion.subjectId);
|
|
775
826
|
report.resultIds.push(...results);
|
|
@@ -160,7 +160,7 @@ export interface RunSimulationOptions {
|
|
|
160
160
|
};
|
|
161
161
|
/** What a case does when required telemetry is not accepted; see
|
|
162
162
|
* `traceNotAccepted` of {@link runExperiment}. Defaults to `"stop"`. */
|
|
163
|
-
traceNotAccepted?: "stop" | "fail_case";
|
|
163
|
+
traceNotAccepted?: "stop" | "fail_case" | "pending";
|
|
164
164
|
/** Called as each case is failed for its telemetry; see `onTelemetryNotAccepted` of
|
|
165
165
|
* {@link runExperiment}. */
|
|
166
166
|
onTelemetryNotAccepted?(entry: TelemetryNotAccepted): void | Promise<void>;
|
package/dist/evals/types.d.ts
CHANGED
|
@@ -542,6 +542,10 @@ export interface TypedError {
|
|
|
542
542
|
type: string;
|
|
543
543
|
/** Optional message; stored only when result content is persisted. */
|
|
544
544
|
message?: string;
|
|
545
|
+
/** For a runner of your own that writes a `type` of its own: the class Hue files it under
|
|
546
|
+
* (`agent`, `infrastructure`, `configuration`, `telemetry` or `unsupported`), which Hue reads
|
|
547
|
+
* only for a name it does not know. The SDK's own types need none. */
|
|
548
|
+
class?: "agent" | "infrastructure" | "configuration" | "telemetry" | "unsupported";
|
|
545
549
|
}
|
|
546
550
|
/** Input for {@link EvaluationClient.startExecution}. */
|
|
547
551
|
export interface StartExecution {
|
|
@@ -570,10 +574,13 @@ export interface CompleteExecution {
|
|
|
570
574
|
error?: TypedError;
|
|
571
575
|
/** Trace revision the stored snapshot must have reached. */
|
|
572
576
|
expectedTraceRevision?: number;
|
|
573
|
-
/** Whether stored trace evidence is required
|
|
574
|
-
|
|
577
|
+
/** Whether stored trace evidence is required, explicitly omitted, or pending: declared and
|
|
578
|
+
* still to arrive, which Hue attaches once the export is complete. */
|
|
579
|
+
traceEvidence?: "required" | "omit" | "pending";
|
|
575
580
|
/** Why trace evidence was omitted. */
|
|
576
581
|
omissionReason?: string;
|
|
582
|
+
/** With `pending`: how many spans the case ended, which Hue holds the arriving trace to. */
|
|
583
|
+
traceSpanCount?: number;
|
|
577
584
|
}
|
|
578
585
|
/** Result of {@link EvaluationClient.completeExecution}. */
|
|
579
586
|
export interface Completion {
|
|
@@ -633,8 +640,9 @@ export interface Subject {
|
|
|
633
640
|
experimentId: string;
|
|
634
641
|
/** Attempt number of the execution. */
|
|
635
642
|
attempt: number;
|
|
636
|
-
/** Whether trace evidence was captured, omitted with a reason
|
|
637
|
-
|
|
643
|
+
/** Whether trace evidence was captured, omitted with a reason, not requested, or is pending
|
|
644
|
+
* (declared and still to arrive; Hue attaches it once the export is complete). */
|
|
645
|
+
traceEvidence: "captured" | "omitted" | "not_requested" | "pending";
|
|
638
646
|
/** Declared OpenTelemetry trace ID, or `null`. */
|
|
639
647
|
traceExternalId: string | null;
|
|
640
648
|
/** Reason trace evidence was omitted, or `null`. */
|
package/dist/evals.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { ArtifactSizeError, createEvaluationClient, EvaluationClient, HueApiError, isTransientApiError, } from "./evals/client.js";
|
|
2
2
|
export type { EvaluationClientOptions } from "./evals/client.js";
|
|
3
3
|
export { runExperiment, rescore, UncertainExecutionError, OutcomeSerializationError, TargetCancelledError, TargetOutcomeUncertainError, TargetTimeoutError, TraceExportUnacknowledgedError, } from "./evals/runner.js";
|
|
4
|
-
export type { RunExperimentOptions, RunExperimentTargetContext, RescoreOptions, RunnerReport, TelemetryIssueCount, TelemetryNotAccepted, } from "./evals/runner.js";
|
|
4
|
+
export type { RunExperimentOptions, RunExperimentTargetContext, RescoreOptions, RunnerReport, TelemetryIssueCount, TelemetryNotAccepted, TracePending, } from "./evals/runner.js";
|
|
5
5
|
export { TargetResult, withFiles } from "./evals/types.js";
|
|
6
6
|
export { CaseFileError, outputContentTypes, outputFileLimits, OutputFileError, safeFilename, targetFileRoles, } from "./evals/files.js";
|
|
7
7
|
export type { CaseFileErrorCode } from "./evals/files.js";
|
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.12.
|
|
2
|
+
export declare const sdkVersion = "0.12.1";
|
package/dist/version.js
CHANGED