@hue-run/sdk 0.14.0 → 0.15.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/dist/cli/eval.js CHANGED
@@ -85,7 +85,7 @@ Output and limits:
85
85
  --content the case span still carries the output
86
86
  --save-version Freeze an unsaved eval-set version before running
87
87
  --checkpoint-dir <path> Private checkpoint directory (default: .hue/eval/<agent-key>)
88
- --concurrency <n> Cases in flight, 1-64 (default: 1)
88
+ --concurrency <n> Cases in flight, 1-100 (default: 1)
89
89
  --trace-not-accepted <policy> A case whose telemetry Hue did not accept in time: fail_case
90
90
  (default) completes it as failed with the evidence omitted;
91
91
  pending completes it in its true state with the trace still to
@@ -1093,7 +1093,7 @@ function parseBaseline(value) {
1093
1093
  }
1094
1094
  async function runOnce(values, connection, agents, agent, hue, output, signal) {
1095
1095
  const client = new EvaluationClient(connection);
1096
- const concurrency = integer("concurrency", values.concurrency, 1, 1, 64);
1096
+ const concurrency = integer("concurrency", values.concurrency, 1, 1, 100);
1097
1097
  const baselineId = parseBaseline(values.baseline);
1098
1098
  const mode = parseMode(values.mode);
1099
1099
  const pins = await resolveSelection(client, values);
@@ -1296,7 +1296,7 @@ async function runWorker(values, connection, agents, agent, hue, output, signal)
1296
1296
  const client = new ObservedClient(connection);
1297
1297
  const environmentClient = createEnvironmentClient(connection);
1298
1298
  const wait = integer("wait", values.wait, 300, 0, 86_400);
1299
- const concurrency = integer("concurrency", values.concurrency, 1, 1, 64);
1299
+ const concurrency = integer("concurrency", values.concurrency, 1, 1, 100);
1300
1300
  const maxRuns = values["max-runs"] === undefined
1301
1301
  ? undefined
1302
1302
  : integer("max-runs", values["max-runs"], 1, 1, 1_000_000);
package/dist/client.d.ts CHANGED
@@ -156,11 +156,16 @@ export declare class HueClient {
156
156
  recordFile(file: FileRecord, explicitContext?: Context): void;
157
157
  /** The keys of each span's content values that were cut to Hue's value cap, for `hue.truncated`. */
158
158
  private truncatedKeys;
159
+ /** A content value's bounds: Hue's value cap as the receiver last advertised it. */
160
+ private contentLimits;
159
161
  /**
160
- * A content value as a JSON attribute within Hue's 256 KiB value cap. A larger value is cut to
161
- * a UTF-8 prefix and its key listed under `hue.truncated`, where Hue's receiver lists the
162
- * values it cuts itself: the span carries the call and the recorded part of its value instead
163
- * of losing the value whole. Only a value that cannot be encoded is omitted and counted.
162
+ * A content value as a JSON attribute within Hue's value cap (1 MiB unless the receiver
163
+ * advertises another). A larger value is cut to a UTF-8 prefix and its key listed under
164
+ * `hue.truncated`, where Hue's receiver lists the values it cuts itself: the span carries the
165
+ * call and the recorded part of its value instead of losing the value whole. While values over
166
+ * the cap may be uploaded, the value is encoded whole, up to Hue's 1 GB upload limit and as many
167
+ * values as a queued record may hold, and export uploads it or cuts it. Only a value that cannot
168
+ * be encoded is omitted and counted.
164
169
  */
165
170
  private setContent;
166
171
  /**
@@ -173,7 +178,12 @@ export declare class HueClient {
173
178
  checkConnection(): Promise<ProjectConnection>;
174
179
  /** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
175
180
  flushSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
176
- /** Safe for finally blocks; preserves the application's result or original exception. */
181
+ /**
182
+ * Safe for finally blocks; preserves the application's result or original exception. When its
183
+ * budget runs out while the client's own transport holds records for a receiver that limited
184
+ * the rate, the hold ends: those records are reported lost to the rate limit, and the hold no
185
+ * longer keeps the process running.
186
+ */
177
187
  shutdownSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
178
188
  private safeLifecycle;
179
189
  /**
package/dist/client.js CHANGED
@@ -10,12 +10,13 @@ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resource
10
10
  import { HUE_SCOPE } from "./config.js";
11
11
  import { hostedServerAddresses, hostedToolActivity, hostedToolProvider, providerErrorDescription, } from "./provider-tools.js";
12
12
  import { toolCatalogSummary } from "./tool-definitions.js";
13
- import { encodeBoundedContent, encodeContent, noopSpan, safeSpan, } from "./safety.js";
13
+ import { contentLimitsOf, encodeBoundedContent, encodeContent, noopSpan, safeSpan, } from "./safety.js";
14
14
  import { TRUNCATED_KEY, withTruncatedKeys } from "./privacy.js";
15
- import { createHueTransport, HueExportError } from "./transport.js";
15
+ import { MAX_BLOB_BYTES } from "./blobs.js";
16
+ import { createHueTransport, exportBudgetMillis, HueExportError, } from "./transport.js";
16
17
  import { verifyTrace } from "./receipt.js";
17
18
  import { sdkVersion } from "./version.js";
18
- import { MAX_BODY_BYTES, MAX_FILE_DATA_BYTES } from "./config.js";
19
+ import { MAX_BODY_BYTES, MAX_FILE_DATA_BYTES, MAX_RECORD_NODES } from "./config.js";
19
20
  /**
20
21
  * Thrown by {@link HueClient.checkConnection} and {@link HueClient.verifyTrace} when Hue cannot be
21
22
  * reached, rejects the project key or answers unexpectedly. The message is fixed and safe to log;
@@ -151,6 +152,20 @@ class ContextualTracer {
151
152
  }
152
153
  }
153
154
  const propagator = new W3CTraceContextPropagator();
155
+ /**
156
+ * Attributes, events and links one span or log record of a client's own providers keeps, and
157
+ * attributes per event and link. OpenTelemetry's default of 128 silently dropped the rest of a
158
+ * long conversation's flattened messages; Hue's receiver counts none of them. Providers an
159
+ * application attaches Hue to keep their own limits.
160
+ */
161
+ const OWNED_RECORD_LIMIT = 2000;
162
+ const OWNED_SPAN_LIMITS = {
163
+ attributeCountLimit: OWNED_RECORD_LIMIT,
164
+ eventCountLimit: OWNED_RECORD_LIMIT,
165
+ linkCountLimit: OWNED_RECORD_LIMIT,
166
+ attributePerEventCountLimit: OWNED_RECORD_LIMIT,
167
+ attributePerLinkCountLimit: OWNED_RECORD_LIMIT,
168
+ };
154
169
  /** A provider tool listing's bounds before its catalog summary: one export request, 64 levels and
155
170
  * 65,536 values, as the Python SDK's content snapshot. */
156
171
  const catalogLimits = { bytes: MAX_BODY_BYTES, nodes: 65_536, depth: 64 };
@@ -214,10 +229,12 @@ export class HueClient {
214
229
  const tracer = new TracerProvider({
215
230
  resource,
216
231
  spanProcessors: this.transport.options.enabled === false ? [] : [this.transport.spanProcessor],
232
+ spanLimits: OWNED_SPAN_LIMITS,
217
233
  });
218
234
  const logger = new LoggerProvider({
219
235
  resource,
220
236
  processors: this.transport.options.enabled === false ? [] : [this.transport.logRecordProcessor],
237
+ logRecordLimits: { attributeCountLimit: OWNED_RECORD_LIMIT },
221
238
  });
222
239
  this.ownedProviders = { tracer, logger };
223
240
  this.tracerProvider = tracer;
@@ -522,7 +539,7 @@ export class HueClient {
522
539
  severityNumber: SeverityNumber.INFO,
523
540
  eventName: "gen_ai.client.inference.operation.details",
524
541
  attributes,
525
- body: JSON.parse(encodeContent(body)),
542
+ body: JSON.parse(encodeContent(body, this.contentLimits())),
526
543
  });
527
544
  }
528
545
  catch {
@@ -715,17 +732,36 @@ export class HueClient {
715
732
  }
716
733
  /** The keys of each span's content values that were cut to Hue's value cap, for `hue.truncated`. */
717
734
  truncatedKeys = new WeakMap();
735
+ /** A content value's bounds: Hue's value cap as the receiver last advertised it. */
736
+ contentLimits() {
737
+ return contentLimitsOf(this.transport.receiverLimits().valueBytes);
738
+ }
718
739
  /**
719
- * A content value as a JSON attribute within Hue's 256 KiB value cap. A larger value is cut to
720
- * a UTF-8 prefix and its key listed under `hue.truncated`, where Hue's receiver lists the
721
- * values it cuts itself: the span carries the call and the recorded part of its value instead
722
- * of losing the value whole. Only a value that cannot be encoded is omitted and counted.
740
+ * A content value as a JSON attribute within Hue's value cap (1 MiB unless the receiver
741
+ * advertises another). A larger value is cut to a UTF-8 prefix and its key listed under
742
+ * `hue.truncated`, where Hue's receiver lists the values it cuts itself: the span carries the
743
+ * call and the recorded part of its value instead of losing the value whole. While values over
744
+ * the cap may be uploaded, the value is encoded whole, up to Hue's 1 GB upload limit and as many
745
+ * values as a queued record may hold, and export uploads it or cuts it. Only a value that cannot
746
+ * be encoded is omitted and counted.
723
747
  */
724
748
  setContent(span, key, value) {
725
749
  if (!this.enabled || this.closed || !this.captureContent)
726
750
  return;
727
751
  try {
728
- const { text, truncated } = encodeBoundedContent(value);
752
+ let encoded;
753
+ if (this.transport.holdsValues())
754
+ try {
755
+ encoded = encodeBoundedContent(value, {
756
+ ...this.contentLimits(),
757
+ bytes: MAX_BLOB_BYTES,
758
+ nodes: MAX_RECORD_NODES,
759
+ });
760
+ }
761
+ catch {
762
+ // Past the runtime's longest string, or its memory: cut to the cap instead.
763
+ }
764
+ const { text, truncated } = encoded ?? encodeBoundedContent(value, this.contentLimits());
729
765
  span.setAttribute(key, text);
730
766
  // The span's list as it stands: the application's own entries (read from the SDK span,
731
767
  // which carries its attributes; the API alone shows none) and this helper's earlier ones.
@@ -826,12 +862,22 @@ export class HueClient {
826
862
  void work.then(clear, clear);
827
863
  return result;
828
864
  }
829
- /** Safe for finally blocks; preserves the application's result or original exception. */
865
+ /**
866
+ * Safe for finally blocks; preserves the application's result or original exception. When its
867
+ * budget runs out while the client's own transport holds records for a receiver that limited
868
+ * the rate, the hold ends: those records are reported lost to the rate limit, and the hold no
869
+ * longer keeps the process running.
870
+ */
830
871
  shutdownSafe(options = {}) {
831
872
  if (this.safeShutdownPromise)
832
873
  return this.safeShutdownPromise;
833
874
  const work = this.shutdown();
834
- const result = this.safeLifecycle(() => work, options);
875
+ const result = this.safeLifecycle(() => work, options).then((outcome) => {
876
+ // A transport the application attached keeps running past this client's shutdown.
877
+ if (outcome.timedOut && this.ownedProviders)
878
+ this.transport.releaseRateLimitHolds();
879
+ return outcome;
880
+ });
835
881
  this.safeShutdownPromise = result;
836
882
  const clear = () => {
837
883
  this.safeShutdownPromise = undefined;
@@ -896,9 +942,13 @@ export class HueClient {
896
942
  return next;
897
943
  }
898
944
  async flushOnce() {
945
+ // Providers this client owns wait as long as an export may take, a rate-limit hold included,
946
+ // instead of OpenTelemetry's 30 s; borrowed providers keep their own budget.
947
+ const owned = this.ownedProviders;
948
+ const budget = { timeoutMillis: exportBudgetMillis(this.transport.options.timeoutMillis) };
899
949
  const results = await Promise.allSettled([
900
- Promise.resolve().then(() => this.tracerProvider.forceFlush()),
901
- Promise.resolve().then(() => this.loggerProvider.forceFlush()),
950
+ Promise.resolve().then(() => owned ? owned.tracer.forceFlush(budget) : this.tracerProvider.forceFlush()),
951
+ Promise.resolve().then(() => owned ? owned.logger.forceFlush(budget) : this.loggerProvider.forceFlush()),
902
952
  ]);
903
953
  for (const [index, result] of results.entries())
904
954
  if (result.status === "rejected")
package/dist/config.d.ts CHANGED
@@ -1,6 +1,26 @@
1
1
  import type { HueOptions, SharedHueOptions } from "./types.js";
2
+ /** One request on the wire (after gzip), as Hue's receiver accepts before it advertises its own
3
+ * limit with `Hue-Max-Request-Bytes`. */
2
4
  export declare const MAX_BODY_BYTES: number;
5
+ /** One request after decompression, before the receiver advertises `Hue-Max-Decoded-Bytes`. */
6
+ export declare const MAX_DECODED_BYTES: number;
7
+ /** One attribute value or log body kept whole, before the receiver advertises
8
+ * `Hue-Max-Value-Bytes`; a longer value is cut to it and listed under `hue.truncated`. */
3
9
  export declare const MAX_CONTENT_BYTES: number;
10
+ /**
11
+ * Text past the value cap that a record keeps when it is queued, in UTF-16 code units. A string
12
+ * longer than the cap and this context is cut when it is queued, so a huge value cannot exhaust
13
+ * the queue's byte budget and drop its record; the context lets `redact` still see a secret that
14
+ * crosses the cap, and what the redactor saw without its continuation is never exported.
15
+ */
16
+ export declare const REDACTION_CONTEXT_UNITS: number;
17
+ /**
18
+ * The values one record may hold when it is queued, redacted and measured for export. A client's
19
+ * own providers keep 2,000 events and links of 2,000 attributes each, so a record's byte budget
20
+ * (every value is charged at least 16 bytes against `maxQueueBytes`) bounds it before this does;
21
+ * this bounds the work on the application's thread when the queue budget is raised.
22
+ */
23
+ export declare const MAX_RECORD_NODES = 1048576;
4
24
  /** Instrumentation scope of the client's own tracer and logger. */
5
25
  export declare const HUE_SCOPE = "@hue-run/sdk";
6
26
  /** Maximum bytes accepted when recordFile hashes caller-provided data locally. */
package/dist/config.js CHANGED
@@ -1,5 +1,25 @@
1
+ /** One request on the wire (after gzip), as Hue's receiver accepts before it advertises its own
2
+ * limit with `Hue-Max-Request-Bytes`. */
1
3
  export const MAX_BODY_BYTES = 1024 * 1024;
2
- export const MAX_CONTENT_BYTES = 256 * 1024;
4
+ /** One request after decompression, before the receiver advertises `Hue-Max-Decoded-Bytes`. */
5
+ export const MAX_DECODED_BYTES = 4 * 1024 * 1024;
6
+ /** One attribute value or log body kept whole, before the receiver advertises
7
+ * `Hue-Max-Value-Bytes`; a longer value is cut to it and listed under `hue.truncated`. */
8
+ export const MAX_CONTENT_BYTES = 1024 * 1024;
9
+ /**
10
+ * Text past the value cap that a record keeps when it is queued, in UTF-16 code units. A string
11
+ * longer than the cap and this context is cut when it is queued, so a huge value cannot exhaust
12
+ * the queue's byte budget and drop its record; the context lets `redact` still see a secret that
13
+ * crosses the cap, and what the redactor saw without its continuation is never exported.
14
+ */
15
+ export const REDACTION_CONTEXT_UNITS = 64 * 1024;
16
+ /**
17
+ * The values one record may hold when it is queued, redacted and measured for export. A client's
18
+ * own providers keep 2,000 events and links of 2,000 attributes each, so a record's byte budget
19
+ * (every value is charged at least 16 bytes against `maxQueueBytes`) bounds it before this does;
20
+ * this bounds the work on the application's thread when the queue budget is raised.
21
+ */
22
+ export const MAX_RECORD_NODES = 1_048_576;
3
23
  /** Instrumentation scope of the client's own tracer and logger. */
4
24
  export const HUE_SCOPE = "@hue-run/sdk";
5
25
  /** Maximum bytes accepted when recordFile hashes caller-provided data locally. */
@@ -30,9 +30,9 @@ export declare class HueEnvironmentError extends Error {
30
30
  export declare function isTransientEnvironmentError(error: unknown): boolean;
31
31
  /** Typed client for authored environments, isolated runs and immutable journals. */
32
32
  export declare class EnvironmentClient {
33
+ #private;
33
34
  /** Validated Hue origin. */
34
35
  readonly baseUrl: string;
35
- private readonly apiKey;
36
36
  private readonly timeoutMillis;
37
37
  private readonly maxAttempts;
38
38
  constructor(options: EnvironmentClientOptions);
@@ -62,7 +62,7 @@ const ENVIRONMENT_PUBLICATION_BOUNDS = aggregateBounds(240_000 + 32);
62
62
  export class EnvironmentClient {
63
63
  /** Validated Hue origin. */
64
64
  baseUrl;
65
- apiKey;
65
+ #apiKey;
66
66
  timeoutMillis;
67
67
  maxAttempts;
68
68
  constructor(options) {
@@ -72,7 +72,7 @@ export class EnvironmentClient {
72
72
  captureContent: false,
73
73
  });
74
74
  this.baseUrl = validated.baseUrl;
75
- this.apiKey = validated.apiKey;
75
+ this.#apiKey = validated.apiKey;
76
76
  this.timeoutMillis = validated.timeoutMillis;
77
77
  const attempts = options.maxAttempts ?? 4;
78
78
  if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10)
@@ -86,7 +86,7 @@ export class EnvironmentClient {
86
86
  response = await fetch(`${this.baseUrl}/api/v1${path}`, {
87
87
  method,
88
88
  headers: {
89
- Authorization: `Bearer ${this.apiKey}`,
89
+ Authorization: `Bearer ${this.#apiKey}`,
90
90
  ...(payload ? { "Content-Type": "application/json" } : {}),
91
91
  },
92
92
  body: payload,
@@ -56,9 +56,9 @@ export declare class ArtifactSizeError extends Error {
56
56
  * bounded to 4 MiB.
57
57
  */
58
58
  export declare class EvaluationClient {
59
+ #private;
59
60
  /** Validated Hue origin. */
60
61
  readonly baseUrl: string;
61
- private readonly apiKey;
62
62
  private readonly timeoutMillis;
63
63
  private readonly maxAttempts;
64
64
  constructor(options: EvaluationClientOptions);
@@ -183,7 +183,7 @@ function refusalRetryAfter(response) {
183
183
  export class EvaluationClient {
184
184
  /** Validated Hue origin. */
185
185
  baseUrl;
186
- apiKey;
186
+ #apiKey;
187
187
  timeoutMillis;
188
188
  maxAttempts;
189
189
  constructor(options) {
@@ -193,7 +193,7 @@ export class EvaluationClient {
193
193
  captureContent: false,
194
194
  });
195
195
  this.baseUrl = validated.baseUrl;
196
- this.apiKey = validated.apiKey;
196
+ this.#apiKey = validated.apiKey;
197
197
  this.timeoutMillis = validated.timeoutMillis;
198
198
  const attempts = options.maxAttempts ?? 4;
199
199
  if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10)
@@ -276,7 +276,7 @@ export class EvaluationClient {
276
276
  const response = await this.send(`${this.baseUrl}/api/v1${path}`, () => ({
277
277
  method,
278
278
  headers: {
279
- Authorization: `Bearer ${this.apiKey}`,
279
+ Authorization: `Bearer ${this.#apiKey}`,
280
280
  ...(payload ? { "Content-Type": "application/json" } : {}),
281
281
  },
282
282
  body: payload,
@@ -321,7 +321,7 @@ export class EvaluationClient {
321
321
  throw new RangeError("maxBytes must be a non-negative integer");
322
322
  const response = await this.send(`${this.baseUrl}/api/v1/artifacts/${uuid(id)}/download`, () => ({
323
323
  method: "GET",
324
- headers: { Authorization: `Bearer ${this.apiKey}` },
324
+ headers: { Authorization: `Bearer ${this.#apiKey}` },
325
325
  redirect: "error",
326
326
  signal: AbortSignal.timeout(Math.max(this.timeoutMillis, 120_000)),
327
327
  }));
@@ -100,7 +100,7 @@ export interface RunLocalAgentOptions {
100
100
  agent: LocalAgentRegistration;
101
101
  /** Local scorer bindings matching the published source digests. */
102
102
  scorers?: LocalScorer[];
103
- /** Cases in flight, between 1 and 16; defaults to 1. */
103
+ /** Cases in flight, between 1 and 100; defaults to 1. */
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"`. */
@@ -74,7 +74,7 @@ interface RunnerOptions {
74
74
  * (for example a Hue-operated grading worker that owns the evaluator source) instead of
75
75
  * refusing the run. Their IDs are reported in `deferredScorerVersionIds`. */
76
76
  deferUnboundLocalScorers?: boolean;
77
- /** Cases in flight at once, 1–64. Default 1. */
77
+ /** Cases in flight at once, 1–100. Default 1. */
78
78
  concurrency?: number;
79
79
  /** Deadline for JSON Schema scoring in its worker, 100–60000 ms. Default 2000. */
80
80
  schemaTimeoutMillis?: number;
@@ -183,8 +183,8 @@ function settings(options) {
183
183
  if (typeof options.persistResultContent !== "boolean")
184
184
  throw new TypeError("Choose persistResultContent explicitly: true or false");
185
185
  const concurrency = options.concurrency ?? 1;
186
- if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 64)
187
- throw new RangeError("concurrency must be 1–64");
186
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100)
187
+ throw new RangeError("concurrency must be 1–100");
188
188
  const timeout = options.schemaTimeoutMillis ?? 2000;
189
189
  if (!Number.isInteger(timeout) || timeout < 100 || timeout > 60_000)
190
190
  throw new RangeError("schemaTimeoutMillis must be 100–60000");
@@ -166,7 +166,7 @@ export interface RunSimulationOptions {
166
166
  onTelemetryNotAccepted?(entry: TelemetryNotAccepted): void | Promise<void>;
167
167
  /** Local scorer callbacks bound by their declared source digests. */
168
168
  localScorers?: LocalScorer[];
169
- /** Cases in flight, 1–64; defaults to 1. */
169
+ /** Cases in flight, 1–100; defaults to 1. */
170
170
  concurrency?: number;
171
171
  /** JSON Schema worker deadline in milliseconds. */
172
172
  schemaTimeoutMillis?: number;
@@ -1,5 +1,5 @@
1
1
  import type { EvaluationClient } from "./client.js";
2
- import type { ExperimentItem, Metric, Subject, TypedError } from "./types.js";
2
+ import type { ExperimentItem, Metric, ScorerVersion, Subject, TypedError } from "./types.js";
3
3
  /** Subset of {@link EvaluationClient} used to wait for results. */
4
4
  export type VerdictClient = Pick<EvaluationClient, "listEvaluationItems" | "listResults" | "getResult">;
5
5
  /** Options for {@link waitForResults}. */
@@ -44,7 +44,7 @@ export interface VerdictResult {
44
44
  notApplicable?: boolean;
45
45
  /** What a not-applicable evaluator needs that the case lacks, such as `outcome_criteria`. */
46
46
  requires?: string;
47
- /** Whether Hue recorded the result as advisory, as it does for every judge today: a result of a
47
+ /** Whether Hue recorded the result as advisory: a result of a
48
48
  * pinned Hue judge whose evidence says `advisory: true`, not an error, and whose metrics carry
49
49
  * no `passed`. It is shown with its verdict but never decides a case. Absent means false. */
50
50
  advisory?: boolean;
@@ -81,8 +81,8 @@ export interface CaseVerdict {
81
81
  /** `passed` and `failed` are scored verdicts; `error`, `skipped` and `pending` carry no verdict.
82
82
  * Only the evaluators that apply to the case decide it; one where none applies is an error. */
83
83
  state: "passed" | "failed" | "error" | "skipped" | "pending";
84
- /** True only for `state: "passed"`: every scored metric of an applicable pin passed and no
85
- * applicable pin errored or is missing. */
84
+ /** True only for `state: "passed"`: every deciding metric passed, required evaluators decided,
85
+ * and no applicable pin errored or is missing. */
86
86
  passed: boolean;
87
87
  /** Pinned scorer versions Hue recorded as not applicable to the case; always set by
88
88
  * {@link summarizeVerdicts}. */
@@ -114,7 +114,7 @@ export interface VerdictSummary {
114
114
  failed: number;
115
115
  /** Cases with a scorer error. */
116
116
  error: number;
117
- /** Cases whose pins were all skipped. */
117
+ /** Cases whose deciding pins were skipped or whose required evaluator has no decision. */
118
118
  skipped: number;
119
119
  /** Cases still missing a result. */
120
120
  pending: number;
@@ -127,14 +127,17 @@ export interface VerdictSummary {
127
127
  export declare function metricPassed(metric: Metric): boolean;
128
128
  /**
129
129
  * Groups results by experiment case. Cases link to results through their execution's subject;
130
- * pass `subjects` when the items do not carry `execution.subjectId`. A case passes when every
131
- * pinned scorer scored it without a failing metric.
130
+ * pass `subjects` when the items do not carry `execution.subjectId`. Applicable required pins
131
+ * need a decision before a case can pass; genuine not-applicable and advisory pins are exempt.
132
132
  */
133
133
  export declare function summarizeVerdicts(results: VerdictResults, options: {
134
134
  /** Experiment items, in the order rows should appear. */
135
135
  experimentItems: Pick<ExperimentItem, "id" | "externalKey" | "execution">[];
136
136
  /** Scorer versions every case needs a result for; defaults to the versions seen in `results`. */
137
137
  scorerVersionIds?: string[];
138
+ /** Immutable pinned definitions, as returned on the experiment. Outcome entries and required
139
+ * judge versions need a decision; result evidence cannot change their required policy. */
140
+ scorerVersions?: Pick<ScorerVersion, "id" | "definition">[];
138
141
  /** Subject to case links for servers that omit `execution.subjectId`. */
139
142
  subjects?: Pick<Subject, "id" | "caseId">[];
140
143
  }): VerdictSummary;
@@ -104,13 +104,25 @@ export function metricPassed(metric) {
104
104
  return metric.passed;
105
105
  return typeof metric.value === "boolean" ? metric.value : true;
106
106
  }
107
+ /** Outcome entries decide the case; a separate judge's immutable version opts into that policy.
108
+ * A named answer reference remains inside its outcome entry, not an advisory judge promotion. */
109
+ function requiresDecision(definition) {
110
+ if (!definition || typeof definition !== "object" || Array.isArray(definition))
111
+ return false;
112
+ const pinned = definition;
113
+ return (pinned.kind === "world_outcome" ||
114
+ (pinned.kind === "world_judge" && pinned.config?.required === true));
115
+ }
107
116
  /**
108
117
  * Groups results by experiment case. Cases link to results through their execution's subject;
109
- * pass `subjects` when the items do not carry `execution.subjectId`. A case passes when every
110
- * pinned scorer scored it without a failing metric.
118
+ * pass `subjects` when the items do not carry `execution.subjectId`. Applicable required pins
119
+ * need a decision before a case can pass; genuine not-applicable and advisory pins are exempt.
111
120
  */
112
121
  export function summarizeVerdicts(results, options) {
113
122
  const pins = new Set(options.scorerVersionIds ?? results.results.map((result) => result.scorerVersionId));
123
+ const required = new Set((options.scorerVersions ?? [])
124
+ .filter((version) => requiresDecision(version.definition))
125
+ .map((version) => version.id));
114
126
  const subjectByCase = new Map();
115
127
  for (const subject of options.subjects ?? [])
116
128
  subjectByCase.set(subject.caseId, subject.id);
@@ -123,7 +135,8 @@ export function summarizeVerdicts(results, options) {
123
135
  const metrics = own.flatMap((result) => result.metrics.map((metric) => ({ ...metric, scorerVersionId: result.scorerVersionId })));
124
136
  // An evaluator that does not apply to the case neither passes nor fails it.
125
137
  const skippedAsInapplicable = (result) => result.state === "skipped" && result.notApplicable === true;
126
- const isAdvisory = (result) => result.advisory === true &&
138
+ const isAdvisory = (result) => !required.has(result.scorerVersionId) &&
139
+ result.advisory === true &&
127
140
  result.state !== "error" &&
128
141
  result.metrics.every((metric) => metric.passed === undefined);
129
142
  const inapplicable = own.filter(skippedAsInapplicable);
@@ -134,18 +147,29 @@ export function summarizeVerdicts(results, options) {
134
147
  const errors = deciding.filter((result) => result.state === "error");
135
148
  const scored = deciding.filter((result) => result.state === "scored");
136
149
  const failing = scored.filter((result) => !result.metrics.every(metricPassed));
150
+ const undecided = deciding.filter((result) => required.has(result.scorerVersionId) &&
151
+ result.state !== "error" &&
152
+ (result.state !== "scored" ||
153
+ !result.metrics.some((metric) => typeof metric.passed === "boolean" || typeof metric.value === "boolean")));
137
154
  const missing = [...pins].some((pin) => !own.some((result) => result.scorerVersionId === pin));
138
155
  const noneApplies = !missing && own.length > 0 && !applicable.length;
139
156
  const noneDecides = !missing && applicable.length > 0 && !deciding.length;
140
- const state = errors.length || noneApplies || noneDecides
141
- ? "error"
142
- : failing.length
143
- ? "failed"
144
- : missing || !own.length
145
- ? "pending"
146
- : scored.length
147
- ? "passed"
148
- : "skipped";
157
+ // A real failure still decides the case if only required evaluators are unavailable. Other
158
+ // scorer errors keep their existing precedence over quality metrics.
159
+ const failureWins = failing.length > 0 && errors.every((result) => required.has(result.scorerVersionId));
160
+ const state = failureWins
161
+ ? "failed"
162
+ : errors.length || noneApplies || noneDecides
163
+ ? "error"
164
+ : failing.length
165
+ ? "failed"
166
+ : missing || !own.length
167
+ ? "pending"
168
+ : undecided.length
169
+ ? "skipped"
170
+ : scored.length
171
+ ? "passed"
172
+ : "skipped";
149
173
  const requires = [
150
174
  ...new Set(inapplicable.flatMap((result) => (result.requires ? [result.requires] : []))),
151
175
  ];
@@ -159,6 +183,11 @@ export function summarizeVerdicts(results, options) {
159
183
  advisory: advisory.map((result) => result.scorerVersionId),
160
184
  metrics,
161
185
  explanations: [
186
+ ...(state === "skipped" &&
187
+ undecided.length &&
188
+ !undecided.some((result) => /^inconclusive:/i.test(result.explanation ?? ""))
189
+ ? ["Inconclusive: a required evaluator did not decide this case."]
190
+ : []),
162
191
  ...(noneApplies
163
192
  ? [
164
193
  `No pinned evaluator applies to this case${requires.length ? ` (they need ${requires.join(" or ")})` : ""}; pin one that grades it`,
@@ -173,7 +202,7 @@ export function summarizeVerdicts(results, options) {
173
202
  ]
174
203
  : []),
175
204
  ...deciding
176
- .filter((result) => result.state !== "scored" || failing.includes(result))
205
+ .filter((result) => result.state !== "scored" || failing.includes(result) || undecided.includes(result))
177
206
  .map((result) => result.explanation)
178
207
  .filter((explanation) => !!explanation),
179
208
  ],
@@ -227,9 +256,11 @@ export async function collectExperimentVerdicts(client, options) {
227
256
  const experiment = await client.getExperiment(options.experimentId);
228
257
  const scorerVersionIds = experiment.evaluation.scorerVersions.map((version) => version.id);
229
258
  const { experimentId: _experimentId, ...wait } = options;
230
- // Hue's own judges, the only evaluators whose results can be advisory.
259
+ // Only non-required Hue judges can be advisory. Their immutable policy is already in this
260
+ // response; a required version's result evidence cannot override it.
231
261
  const judgeScorerVersionIds = experiment.evaluation.scorerVersions
232
- .filter((version) => version.definition?.kind === "world_judge")
262
+ .filter((version) => version.definition?.kind === "world_judge" &&
263
+ !requiresDecision(version.definition))
233
264
  .map((version) => version.id);
234
265
  const results = await waitForResults(client, {
235
266
  ...wait,
@@ -250,6 +281,11 @@ export async function collectExperimentVerdicts(client, options) {
250
281
  runId: experiment.evaluation.id,
251
282
  scorerVersionIds,
252
283
  results,
253
- summary: summarizeVerdicts(results, { experimentItems, scorerVersionIds, subjects }),
284
+ summary: summarizeVerdicts(results, {
285
+ experimentItems,
286
+ scorerVersionIds,
287
+ scorerVersions: experiment.evaluation.scorerVersions,
288
+ subjects,
289
+ }),
254
290
  };
255
291
  }
@@ -1,9 +1,27 @@
1
1
  /** Inline file content larger than this many UTF-8 bytes is exported as its digest instead. */
2
2
  export declare const INLINE_FILE_LIMIT: number;
3
+ /** The longest message text, in UTF-8 bytes, inspected for inline files. */
4
+ export declare const MAX_INLINE_FILE_TEXT: number;
3
5
  /** Message text one record inspects for inline files in all, in UTF-16 code units. */
4
6
  export declare const INLINE_FILE_TEXT_PER_RECORD: number;
5
7
  /** Whether an attribute is one of the recorded message attributes that can inline files. */
6
8
  export declare function isMessageKey(key: string): boolean;
9
+ /** An inline file's bytes, whether they were decoded from the part's text (base64 or
10
+ * percent-escapes) rather than taken as its UTF-8, and the media type its `data:` URL names. */
11
+ export interface InlineFileBytes {
12
+ bytes: Buffer;
13
+ decoded: boolean;
14
+ urlMediaType?: string;
15
+ }
16
+ /**
17
+ * The bytes an inline file part carries, the file's own bytes whatever its media type: a `data:`
18
+ * URL decoded by its own encoding (base64 with `;base64`, percent-escapes otherwise), content in
19
+ * the base64 alphabet decoded, and anything else, such as a text file's own text, as UTF-8. A
20
+ * `data:` URL whose data does not decode is taken as UTF-8 too.
21
+ */
22
+ export declare function inlineFileBytes(content: string): InlineFileBytes;
23
+ /** The key holding a part's inline content: GenAI `blob` parts and AI SDK 6 `file` parts. */
24
+ export declare function contentKey(part: Record<string, unknown>): "content" | "data" | undefined;
7
25
  /**
8
26
  * Replaces inline files longer than {@link INLINE_FILE_LIMIT} in a recorded message attribute
9
27
  * with their SHA-256 and byte size, so a span that inlines a large file exports the file's
@@ -11,4 +29,29 @@ export declare function isMessageKey(key: string): boolean;
11
29
  * `mime_type`, `modality`, `mediaType`, …). Other attributes, shorter messages and values that
12
30
  * are not JSON are returned unchanged.
13
31
  */
14
- export declare function hashInlineFiles(key: string, value: unknown): unknown;
32
+ export declare function hashInlineFiles(key: string, value: unknown,
33
+ /** Counts the files replaced by their digest, when given. */
34
+ count?: {
35
+ files: number;
36
+ }): unknown;
37
+ /** One inline file part found in a parsed message: the part object, the key holding its inline
38
+ * content, that content, and the part's content as a JSON Pointer (RFC 6901) into the message. */
39
+ export interface InlineFilePart {
40
+ part: Record<string, unknown>;
41
+ key: "content" | "data";
42
+ content: string;
43
+ pointer: string;
44
+ }
45
+ /**
46
+ * The inline file parts of a parsed message whose content text is longer than
47
+ * {@link INLINE_FILE_LIMIT} UTF-8 bytes, in document order: the caller decodes each and keeps the
48
+ * ones whose file bytes are over the limit. The parts are the parsed value's own objects, so the
49
+ * caller may replace their content in place.
50
+ */
51
+ export declare function largeInlineFileParts(value: unknown): InlineFilePart[];
52
+ /** The digest an inline file part is exported as when its bytes are not uploaded: the part's
53
+ * other fields with the file's SHA-256 and byte size, as {@link hashInlineFiles} writes it. */
54
+ export declare function digestPart(part: Record<string, unknown>, key: "content" | "data", digest: {
55
+ sha256: string;
56
+ size: number;
57
+ }): void;