@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 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.0`, `@opentelemetry/api@1.9.1` and
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.0.tgz \
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.0.tgz \
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 Hue's checks were
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 incomplete,
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
- STATE_LABEL[item.state],
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 : [STATE_LABEL[item.state].toLowerCase()])
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
- totals.skipped ? `${totals.skipped} skipped` : "",
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: "fail_case",
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: "fail_case",
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: "fail_case",
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>;
@@ -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. */
@@ -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 && options.traceEvidence.mode !== "omit") {
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" && prepared.complete.traceEvidence !== "omit")
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>;
@@ -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 or explicitly omitted. */
574
- traceEvidence?: "required" | "omit";
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 or not requested. */
637
- traceEvidence: "captured" | "omitted" | "not_requested";
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.0";
2
+ export declare const sdkVersion = "0.12.1";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/write-version.mjs from package.json; do not edit by hand.
2
2
  /** Package version shared by the instrumentation scope and the export User-Agent. */
3
- export const sdkVersion = "0.12.0";
3
+ export const sdkVersion = "0.12.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {