@hue-run/sdk 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CLI.md +240 -49
  2. package/ENVIRONMENTS.md +48 -9
  3. package/EVALUATIONS.md +41 -21
  4. package/README.md +70 -30
  5. package/dist/cli/eval-direct.d.ts +12 -1
  6. package/dist/cli/eval-direct.js +145 -33
  7. package/dist/cli/eval.d.ts +28 -1
  8. package/dist/cli/eval.js +504 -81
  9. package/dist/cli/listen.d.ts +76 -0
  10. package/dist/cli/listen.js +816 -0
  11. package/dist/cli/mcp.d.ts +65 -7
  12. package/dist/cli/mcp.js +271 -73
  13. package/dist/client.d.ts +7 -4
  14. package/dist/client.js +36 -10
  15. package/dist/evals/checkpoint.d.ts +15 -1
  16. package/dist/evals/checkpoint.js +34 -3
  17. package/dist/evals/client.d.ts +24 -4
  18. package/dist/evals/client.js +92 -38
  19. package/dist/evals/environment-target.d.ts +18 -3
  20. package/dist/evals/environment-target.js +22 -10
  21. package/dist/evals/exit-cleanup.d.ts +4 -0
  22. package/dist/evals/exit-cleanup.js +26 -0
  23. package/dist/evals/files.d.ts +69 -3
  24. package/dist/evals/files.js +255 -42
  25. package/dist/evals/json.js +116 -9
  26. package/dist/evals/local-worker.d.ts +21 -3
  27. package/dist/evals/local-worker.js +14 -0
  28. package/dist/evals/runner.d.ts +45 -0
  29. package/dist/evals/runner.js +199 -21
  30. package/dist/evals/scenarios.d.ts +7 -5
  31. package/dist/evals/scenarios.js +71 -14
  32. package/dist/evals/scorer-publication.js +58 -3
  33. package/dist/evals/simulation.d.ts +17 -4
  34. package/dist/evals/simulation.js +8 -0
  35. package/dist/evals/types.d.ts +48 -0
  36. package/dist/evals/verdicts.d.ts +26 -2
  37. package/dist/evals/verdicts.js +72 -9
  38. package/dist/evals.d.ts +4 -3
  39. package/dist/evals.js +2 -2
  40. package/dist/inline-files.d.ts +4 -0
  41. package/dist/inline-files.js +48 -17
  42. package/dist/provider-tools.d.ts +12 -1
  43. package/dist/provider-tools.js +192 -106
  44. package/dist/safety.d.ts +7 -1
  45. package/dist/safety.js +9 -7
  46. package/dist/setup/cli.js +31 -8
  47. package/dist/setup/peers.d.ts +13 -0
  48. package/dist/setup/peers.js +44 -0
  49. package/dist/snapshot.d.ts +2 -2
  50. package/dist/snapshot.js +47 -12
  51. package/dist/tool-definitions.d.ts +18 -0
  52. package/dist/tool-definitions.js +232 -0
  53. package/dist/transport.js +2 -2
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +1 -1
@@ -30,6 +30,47 @@ const worldOutcomeMetrics = [
30
30
  "process_constraints",
31
31
  "task_success",
32
32
  ].map((name) => ({ name, type: "boolean" }));
33
+ /** `hue.conversion_outcome.v2`: v1's rubric with the destination reported as `recipient`,
34
+ * `thread` and `subject`. */
35
+ const conversionOutcomeV2Metrics = [
36
+ "completed_run",
37
+ "saved_draft",
38
+ "correct_destination",
39
+ "recipient",
40
+ "thread",
41
+ "subject",
42
+ "content",
43
+ "unrelated_preserved",
44
+ "process_constraints",
45
+ "task_success",
46
+ ].map((name) => ({ name, type: "boolean" }));
47
+ /** `hue.outcome_assertions.v2`'s counts; `.v3` adds the judge counts. */
48
+ const outcomeAssertionMetrics = [
49
+ { name: "task_success", type: "boolean" },
50
+ ...["assertions_passed", "assertions_failed", "advisory_failed", "agent_mistakes"].map((name) => ({ name, type: "number", min: 0 })),
51
+ ];
52
+ const outcomeAssertionV3Metrics = [
53
+ ...outcomeAssertionMetrics,
54
+ ...["judges_passed", "judges_failed", "judges_advisory"].map((name) => ({
55
+ name,
56
+ type: "number",
57
+ min: 0,
58
+ })),
59
+ ];
60
+ /** The judge pin a `hue.outcome_assertions.v3` version carries; every field is required. */
61
+ const outcomeJudgeConfig = z.strictObject({
62
+ model: z.string().min(1).max(200),
63
+ provider: z.string().min(1).max(64),
64
+ template: z.string().regex(/^[a-f0-9]{64}$/),
65
+ samples: z.number().int().min(1).max(9),
66
+ temperature: z.number().min(0).max(2),
67
+ maxOutputTokens: z.number().int().min(1).max(32_000),
68
+ timeoutMs: z.number().int().min(1_000).max(300_000),
69
+ });
70
+ /** A Hue-executed entry whose metrics are fixed by the entry itself. */
71
+ const fixedMetrics = (fixed) => metrics
72
+ .default(fixed)
73
+ .refine((value) => digest(value) === digest(fixed), "World outcome metrics are fixed by the pinned entry");
33
74
  const jsonValue = z.custom((value) => {
34
75
  try {
35
76
  json(value);
@@ -85,9 +126,23 @@ const sdkScorerPublication = z.union([
85
126
  z.strictObject({
86
127
  kind: z.literal("world_outcome"),
87
128
  entry: z.literal("hue.conversion_outcome.v1"),
88
- metrics: metrics
89
- .default(worldOutcomeMetrics)
90
- .refine((value) => digest(value) === digest(worldOutcomeMetrics), "World outcome metrics are fixed by the pinned entry"),
129
+ metrics: fixedMetrics(worldOutcomeMetrics),
130
+ }),
131
+ z.strictObject({
132
+ kind: z.literal("world_outcome"),
133
+ entry: z.literal("hue.conversion_outcome.v2"),
134
+ metrics: fixedMetrics(conversionOutcomeV2Metrics),
135
+ }),
136
+ z.strictObject({
137
+ kind: z.literal("world_outcome"),
138
+ entry: z.literal("hue.outcome_assertions.v2"),
139
+ metrics: fixedMetrics(outcomeAssertionMetrics),
140
+ }),
141
+ z.strictObject({
142
+ kind: z.literal("world_outcome"),
143
+ entry: z.literal("hue.outcome_assertions.v3"),
144
+ metrics: fixedMetrics(outcomeAssertionV3Metrics),
145
+ config: z.strictObject({ judge: outcomeJudgeConfig }),
91
146
  }),
92
147
  z.strictObject({ kind: z.literal("manual"), metrics }),
93
148
  z.strictObject({ kind: z.literal("llm_judge"), config: judgeConfig, metrics }),
@@ -4,8 +4,8 @@ import type { EnvironmentTool } from "../environment/tools.js";
4
4
  import type { EnvironmentIdentity, PublishableEnvironmentDefinition, WorldHandoff } from "../environment/types.js";
5
5
  import { type EvaluationClient } from "./client.js";
6
6
  import type { ActualAgentManifestInputV2, AttemptConnectionBundleV2, RequestedAttemptProviderV2 } from "./attempt.js";
7
- import { type RunnerReport } from "./runner.js";
8
- import type { ExperimentCase, Identity, JsonValue, LocalScorer, ScorerDefinition, SimulationMcpCapability } from "./types.js";
7
+ import { type RunnerReport, type TelemetryNotAccepted } from "./runner.js";
8
+ import type { ExperimentCase, Identity, JsonValue, LocalFile, LocalScorer, ScorerDefinition, SimulationMcpCapability, TargetResult } from "./types.js";
9
9
  /** Repository-authored scorer identity and its public definition or local binding. */
10
10
  export interface RepositorySimulationScorer extends Identity {
11
11
  /** Scorer definition to publish, or a local callback bound to its source digest. */
@@ -121,6 +121,12 @@ export interface SimulationTargetContext {
121
121
  /** Credential-bearing provider connections for this callback only. Hue never
122
122
  * checkpoints, logs or adds this response to parity digests. */
123
123
  connectionBundle?: AttemptConnectionBundleV2;
124
+ /** Verified copies of the case's agent-visible input files (`source`, templates,
125
+ * references, originals); evaluator-only files are withheld. Empty when the case has none. */
126
+ files: LocalFile[];
127
+ /** Private directory for this case, removed after it; return generated files with
128
+ * `withFiles`. */
129
+ outputDirectory: string;
124
130
  /** Cancellation is cooperative: pass this signal into the real agent/provider call. */
125
131
  signal?: AbortSignal;
126
132
  }
@@ -152,6 +158,12 @@ export interface RunSimulationOptions {
152
158
  /** Bounded explanation for the omission. */
153
159
  reason: string;
154
160
  };
161
+ /** What a case does when required telemetry is not accepted; see
162
+ * `traceNotAccepted` of {@link runExperiment}. Defaults to `"stop"`. */
163
+ traceNotAccepted?: "stop" | "fail_case";
164
+ /** Called as each case is failed for its telemetry; see `onTelemetryNotAccepted` of
165
+ * {@link runExperiment}. */
166
+ onTelemetryNotAccepted?(entry: TelemetryNotAccepted): void | Promise<void>;
155
167
  /** Local scorer callbacks bound by their declared source digests. */
156
168
  localScorers?: LocalScorer[];
157
169
  /** Cases in flight, 1–64; defaults to 1. */
@@ -190,8 +202,9 @@ export interface RunSimulationOptions {
190
202
  /** Selected MCP surface. */
191
203
  surfaceKey: "google.gmail/mcp" | "slack/mcp";
192
204
  };
193
- /** Invokes the existing local agent exactly once for this attempt. */
194
- target(inputs: JsonValue, context: SimulationTargetContext): JsonValue | undefined | Promise<JsonValue | undefined>;
205
+ /** Invokes the existing local agent exactly once for this attempt; return the output, or
206
+ * `withFiles(output, files)` when it generated files. */
207
+ target(inputs: JsonValue, context: SimulationTargetContext): JsonValue | TargetResult | undefined | Promise<JsonValue | TargetResult | undefined>;
195
208
  /** Receives bounded nonsecret lifecycle progress. */
196
209
  onProgress?(event: SimulationProgress): void | Promise<void>;
197
210
  }
@@ -430,6 +430,12 @@ export async function runSimulation(options) {
430
430
  checkpointDirectory: join(store.directory, `experiment-${experimentId}`),
431
431
  persistResultContent: options.persistResultContent,
432
432
  traceEvidence: options.traceEvidence,
433
+ ...(options.traceNotAccepted ? { traceNotAccepted: options.traceNotAccepted } : {}),
434
+ ...(options.onTelemetryNotAccepted
435
+ ? {
436
+ onTelemetryNotAccepted: (entry) => options.onTelemetryNotAccepted(entry),
437
+ }
438
+ : {}),
433
439
  environmentEvidence: "required",
434
440
  scorers: bindings,
435
441
  concurrency: options.concurrency,
@@ -474,6 +480,8 @@ export async function runSimulation(options) {
474
480
  ...(targetContext.connectionBundle
475
481
  ? { connectionBundle: structuredClone(targetContext.connectionBundle) }
476
482
  : {}),
483
+ files: structuredClone(targetContext.files),
484
+ outputDirectory: targetContext.outputDirectory,
477
485
  signal: targetContext.signal,
478
486
  }),
479
487
  }),
@@ -202,6 +202,33 @@ export type ScorerDefinition = {
202
202
  entry: "hue.conversion_outcome.v1";
203
203
  /** The seven fixed boolean metrics defined by the entry. */
204
204
  metrics: MetricDefinition[];
205
+ } | {
206
+ /** Scored inside Hue using immutable world evidence; the local runner defers it. */
207
+ kind: "world_outcome";
208
+ /** v1's rubric with the destination reported as `recipient`, `thread` and `subject`. */
209
+ entry: "hue.conversion_outcome.v2";
210
+ /** The ten fixed boolean metrics defined by the entry. */
211
+ metrics: MetricDefinition[];
212
+ } | {
213
+ /** A case's reviewed outcome assertions, scored inside Hue; the local runner defers it. */
214
+ kind: "world_outcome";
215
+ /** Pinned Hue-executed assertions evaluator. */
216
+ entry: "hue.outcome_assertions.v2";
217
+ /** `task_success` and the fixed assertion counts defined by the entry. */
218
+ metrics: MetricDefinition[];
219
+ } | {
220
+ /** The v2 assertions plus the case's calibrated judges, scored inside Hue; the local runner
221
+ * defers it. */
222
+ kind: "world_outcome";
223
+ /** Pinned Hue-executed assertions-and-judges evaluator. */
224
+ entry: "hue.outcome_assertions.v3";
225
+ /** `task_success`, the assertion counts and the judge counts defined by the entry. */
226
+ metrics: MetricDefinition[];
227
+ /** The judge pin: model, template and sampling policy belong to the version. */
228
+ config: {
229
+ /** The judge every case scored by this version uses. */
230
+ judge: OutcomeJudgeConfig;
231
+ };
205
232
  } | {
206
233
  /** Scored by a person in Hue; the local runner defers it. */
207
234
  kind: "manual";
@@ -215,6 +242,23 @@ export type ScorerDefinition = {
215
242
  /** Metrics the judge reports. */
216
243
  metrics: MetricDefinition[];
217
244
  };
245
+ /** The judge a `hue.outcome_assertions.v3` version pins. */
246
+ export interface OutcomeJudgeConfig {
247
+ /** A provider-prefixed model id, such as `anthropic/claude-fable-5.1`. */
248
+ model: string;
249
+ /** The model's provider. */
250
+ provider: string;
251
+ /** SHA-256 hex digest of the judge template: its fixed instructions and evidence rendering. */
252
+ template: string;
253
+ /** Answers sampled per rubric item, 1–9; a majority decides. */
254
+ samples: number;
255
+ /** Sampling temperature, 0–2. */
256
+ temperature: number;
257
+ /** Output token limit per answer. */
258
+ maxOutputTokens: number;
259
+ /** Per-answer timeout in milliseconds. */
260
+ timeoutMs: number;
261
+ }
218
262
  /** Configuration of a hosted judge scorer. */
219
263
  export interface JudgeConfig {
220
264
  /** Judge model identifier. */
@@ -264,6 +308,10 @@ export interface StoredResult extends ResultSummary {
264
308
  error: TypedError | null;
265
309
  /** Source digest of the local scorer that produced it, when applicable. */
266
310
  sourceDigest: string | null;
311
+ /** True only for a `skipped` result Hue's own outcome scoring recorded because the evaluator
312
+ * does not apply to the case (it has no outcome criteria or conversion rubric to grade); a
313
+ * submitted result never reads true. Absent from servers that predate it. */
314
+ notApplicable?: boolean;
267
315
  }
268
316
  /** A hosted judge job and its charge accounting. */
269
317
  export interface JudgeJob {
@@ -16,6 +16,10 @@ export interface WaitForResultsOptions {
16
16
  pollIntervalMillis?: number;
17
17
  /** Stops waiting early; the partial state is returned with `complete: false`. */
18
18
  signal?: AbortSignal;
19
+ /** Pinned versions of Hue's own judges (definition kind `world_judge`), whose results Hue can
20
+ * mark advisory. Any evaluator can write the same evidence, so no other result is advisory.
21
+ * {@link collectExperimentVerdicts} reads them from the experiment. */
22
+ judgeScorerVersionIds?: string[];
19
23
  }
20
24
  /** One stored result joined to its evaluation item. */
21
25
  export interface VerdictResult {
@@ -35,6 +39,15 @@ export interface VerdictResult {
35
39
  explanation: string | null;
36
40
  /** Scorer failure for `state: "error"`, otherwise `null`. */
37
41
  error: TypedError | null;
42
+ /** Whether Hue recorded that the evaluator does not apply to the case; such a result counts as
43
+ * neither a pass nor a failure. Absent means false. */
44
+ notApplicable?: boolean;
45
+ /** What a not-applicable evaluator needs that the case lacks, such as `outcome_criteria`. */
46
+ requires?: string;
47
+ /** Whether Hue recorded the result as advisory, as it does for every judge today: a result of a
48
+ * pinned Hue judge whose evidence says `advisory: true`, not an error, and whose metrics carry
49
+ * no `passed`. It is shown with its verdict but never decides a case. Absent means false. */
50
+ advisory?: boolean;
38
51
  }
39
52
  /** Outcome of {@link waitForResults}. */
40
53
  export interface VerdictResults {
@@ -65,10 +78,18 @@ export interface CaseVerdict {
65
78
  externalKey: string;
66
79
  /** Subject scored for the case, or `null` before completion. */
67
80
  subjectId: string | null;
68
- /** `passed` and `failed` are scored verdicts; `error`, `skipped` and `pending` carry no verdict. */
81
+ /** `passed` and `failed` are scored verdicts; `error`, `skipped` and `pending` carry no verdict.
82
+ * Only the evaluators that apply to the case decide it; one where none applies is an error. */
69
83
  state: "passed" | "failed" | "error" | "skipped" | "pending";
70
- /** True only for `state: "passed"`: every scored metric passed and no pin errored or is missing. */
84
+ /** True only for `state: "passed"`: every scored metric of an applicable pin passed and no
85
+ * applicable pin errored or is missing. */
71
86
  passed: boolean;
87
+ /** Pinned scorer versions Hue recorded as not applicable to the case; always set by
88
+ * {@link summarizeVerdicts}. */
89
+ notApplicable?: string[];
90
+ /** Pinned scorer versions whose results for the case are advisory: their metrics are listed
91
+ * but never decide the case; always set by {@link summarizeVerdicts}. */
92
+ advisory?: string[];
72
93
  /** Reported metrics across pinned scorers, in result order. */
73
94
  metrics: (Metric & {
74
95
  /** Scorer version that reported the metric. */
@@ -97,6 +118,9 @@ export interface VerdictSummary {
97
118
  skipped: number;
98
119
  /** Cases still missing a result. */
99
120
  pending: number;
121
+ /** Results of evaluators that do not apply to their case, a case can have several; always
122
+ * set by {@link summarizeVerdicts}. */
123
+ notApplicable?: number;
100
124
  };
101
125
  }
102
126
  /** Whether one reported metric counts as passing: an explicit `passed`, else a true boolean. */
@@ -42,6 +42,7 @@ export async function waitForResults(client, options) {
42
42
  throw new RangeError("timeoutMillis must be ≥ 0");
43
43
  if (!Number.isInteger(interval) || interval < 250 || interval > 60_000)
44
44
  throw new RangeError("pollIntervalMillis must be 250–60000");
45
+ const judges = new Set(options.judgeScorerVersionIds ?? []);
45
46
  const wanted = options.subjectIds ? new Set(options.subjectIds) : undefined;
46
47
  const pins = [...new Set(options.scorerVersionIds)];
47
48
  const deadline = Date.now() + timeout;
@@ -66,6 +67,16 @@ export async function waitForResults(client, options) {
66
67
  if (!id)
67
68
  continue;
68
69
  const stored = await client.getResult(id);
70
+ // Only Hue's own flag on a skipped result marks it not applicable; the evidence alone
71
+ // never does, and a scored or errored result keeps its verdict whatever it carries.
72
+ const notApplicable = stored.state === "skipped" && stored.notApplicable === true;
73
+ const evidence = stored.evidence;
74
+ // Any evaluator can write `advisory: true` in its evidence; only Hue's own judges'
75
+ // results are advisory, never as an error or with a metric that says it passed.
76
+ const advisory = judges.has(stored.scorerVersionId) &&
77
+ stored.state !== "error" &&
78
+ evidence?.advisory === true &&
79
+ !(stored.state === "scored" && stored.metrics.some((metric) => "passed" in metric));
69
80
  results.push({
70
81
  id: stored.id,
71
82
  itemId: stored.itemId,
@@ -75,6 +86,11 @@ export async function waitForResults(client, options) {
75
86
  metrics: stored.state === "scored" ? stored.metrics : [],
76
87
  explanation: stored.explanation ?? null,
77
88
  error: stored.error ?? null,
89
+ notApplicable,
90
+ advisory,
91
+ ...(notApplicable && typeof evidence?.requires === "string"
92
+ ? { requires: evidence.requires }
93
+ : {}),
78
94
  });
79
95
  }
80
96
  return { complete, items, results };
@@ -105,11 +121,23 @@ export function summarizeVerdicts(results, options) {
105
121
  const subjectId = subjectByCase.get(item.id) ?? null;
106
122
  const own = subjectId ? results.results.filter((result) => result.subjectId === subjectId) : [];
107
123
  const metrics = own.flatMap((result) => result.metrics.map((metric) => ({ ...metric, scorerVersionId: result.scorerVersionId })));
108
- const errors = own.filter((result) => result.state === "error");
109
- const scored = own.filter((result) => result.state === "scored");
124
+ // An evaluator that does not apply to the case neither passes nor fails it.
125
+ const skippedAsInapplicable = (result) => result.state === "skipped" && result.notApplicable === true;
126
+ const isAdvisory = (result) => result.advisory === true &&
127
+ result.state !== "error" &&
128
+ result.metrics.every((metric) => metric.passed === undefined);
129
+ const inapplicable = own.filter(skippedAsInapplicable);
130
+ const applicable = own.filter((result) => !skippedAsInapplicable(result));
131
+ // An advisory result, such as a judge's, is shown but never decides the case, as in Hue.
132
+ const advisory = applicable.filter(isAdvisory);
133
+ const deciding = applicable.filter((result) => !isAdvisory(result));
134
+ const errors = deciding.filter((result) => result.state === "error");
135
+ const scored = deciding.filter((result) => result.state === "scored");
110
136
  const failing = scored.filter((result) => !result.metrics.every(metricPassed));
111
137
  const missing = [...pins].some((pin) => !own.some((result) => result.scorerVersionId === pin));
112
- const state = errors.length
138
+ const noneApplies = !missing && own.length > 0 && !applicable.length;
139
+ const noneDecides = !missing && applicable.length > 0 && !deciding.length;
140
+ const state = errors.length || noneApplies || noneDecides
113
141
  ? "error"
114
142
  : failing.length
115
143
  ? "failed"
@@ -118,23 +146,53 @@ export function summarizeVerdicts(results, options) {
118
146
  : scored.length
119
147
  ? "passed"
120
148
  : "skipped";
149
+ const requires = [
150
+ ...new Set(inapplicable.flatMap((result) => (result.requires ? [result.requires] : []))),
151
+ ];
121
152
  return {
122
153
  caseId: item.id,
123
154
  externalKey: item.externalKey,
124
155
  subjectId,
125
156
  state,
126
157
  passed: state === "passed",
158
+ notApplicable: inapplicable.map((result) => result.scorerVersionId),
159
+ advisory: advisory.map((result) => result.scorerVersionId),
127
160
  metrics,
128
- explanations: own
129
- .filter((result) => result.state !== "scored" || failing.includes(result))
130
- .map((result) => result.explanation)
131
- .filter((explanation) => !!explanation),
161
+ explanations: [
162
+ ...(noneApplies
163
+ ? [
164
+ `No pinned evaluator applies to this case${requires.length ? ` (they need ${requires.join(" or ")})` : ""}; pin one that grades it`,
165
+ ]
166
+ : []),
167
+ ...(noneDecides
168
+ ? [
169
+ "Only advisory evaluators ran for this case, and they never decide it; pin one that grades it",
170
+ ...advisory
171
+ .filter((result) => result.state === "skipped")
172
+ .map((result) => `An advisory evaluator skipped the case: ${result.explanation ?? "no reason given"}`),
173
+ ]
174
+ : []),
175
+ ...deciding
176
+ .filter((result) => result.state !== "scored" || failing.includes(result))
177
+ .map((result) => result.explanation)
178
+ .filter((explanation) => !!explanation),
179
+ ],
132
180
  errors: errors.map((result) => result.error?.type ?? "ScorerError"),
133
181
  };
134
182
  });
135
- const totals = { cases: cases.length, passed: 0, failed: 0, error: 0, skipped: 0, pending: 0 };
136
- for (const item of cases)
183
+ const totals = {
184
+ cases: cases.length,
185
+ passed: 0,
186
+ failed: 0,
187
+ error: 0,
188
+ skipped: 0,
189
+ pending: 0,
190
+ notApplicable: 0,
191
+ };
192
+ for (const item of cases) {
137
193
  totals[item.state]++;
194
+ totals.notApplicable += item.notApplicable?.length ?? 0;
195
+ }
138
196
  return { cases, totals };
139
197
  }
140
198
  /** Diffs the current summary against a baseline by case key. */
@@ -169,10 +227,15 @@ export async function collectExperimentVerdicts(client, options) {
169
227
  const experiment = await client.getExperiment(options.experimentId);
170
228
  const scorerVersionIds = experiment.evaluation.scorerVersions.map((version) => version.id);
171
229
  const { experimentId: _experimentId, ...wait } = options;
230
+ // Hue's own judges, the only evaluators whose results can be advisory.
231
+ const judgeScorerVersionIds = experiment.evaluation.scorerVersions
232
+ .filter((version) => version.definition?.kind === "world_judge")
233
+ .map((version) => version.id);
172
234
  const results = await waitForResults(client, {
173
235
  ...wait,
174
236
  runId: experiment.evaluation.id,
175
237
  scorerVersionIds,
238
+ judgeScorerVersionIds,
176
239
  });
177
240
  const experimentItems = await allPages((after) => client.listExperimentItems(experiment.id, { after }));
178
241
  const linked = new Set(experimentItems.map((item) => item.execution?.subjectId).filter((id) => !!id));
package/dist/evals.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- export { createEvaluationClient, EvaluationClient, HueApiError } from "./evals/client.js";
1
+ export { ArtifactSizeError, createEvaluationClient, EvaluationClient, HueApiError, } from "./evals/client.js";
2
2
  export type { EvaluationClientOptions } from "./evals/client.js";
3
3
  export { runExperiment, rescore, UncertainExecutionError, OutcomeSerializationError, TargetCancelledError, TargetOutcomeUncertainError, } from "./evals/runner.js";
4
- export type { RunExperimentOptions, RunExperimentTargetContext, RescoreOptions, RunnerReport, } from "./evals/runner.js";
4
+ export type { RunExperimentOptions, RunExperimentTargetContext, RescoreOptions, RunnerReport, TelemetryIssueCount, TelemetryNotAccepted, } from "./evals/runner.js";
5
5
  export { TargetResult, withFiles } from "./evals/types.js";
6
- export { outputContentTypes, outputFileLimits, OutputFileError, safeFilename, targetFileRoles, } from "./evals/files.js";
6
+ export { CaseFileError, outputContentTypes, outputFileLimits, OutputFileError, safeFilename, targetFileRoles, } from "./evals/files.js";
7
+ export type { CaseFileErrorCode } from "./evals/files.js";
7
8
  export { runSimulation } from "./evals/simulation.js";
8
9
  export type { RepositorySimulationCase, RepositorySimulationScorer, RunSimulationOptions, SimulationDefinition, SimulationProgress, SimulationReport, SimulationScenario, SimulationTargetContext, } from "./evals/simulation.js";
9
10
  export { actualAgentManifestV2, agentManifestDigestV2, attemptBaselineV2, attemptBindingRead, attemptConnectionBundleV2, attemptIdentityV2, dependencyManifestV2, dependencyProviderV2, expectedAgentManifestV2, executionManifestDigestV2, parityEvidenceV2, preflightFindingV2, preflightReportV2, prepareAttemptInputV2, projectMcpConnectionV2, secretFreeBindingV2, surfaceBindingV2, } from "./evals/attempt.js";
package/dist/evals.js CHANGED
@@ -1,7 +1,7 @@
1
- export { createEvaluationClient, EvaluationClient, HueApiError } from "./evals/client.js";
1
+ export { ArtifactSizeError, createEvaluationClient, EvaluationClient, HueApiError, } from "./evals/client.js";
2
2
  export { runExperiment, rescore, UncertainExecutionError, OutcomeSerializationError, TargetCancelledError, TargetOutcomeUncertainError, } from "./evals/runner.js";
3
3
  export { TargetResult, withFiles } from "./evals/types.js";
4
- export { outputContentTypes, outputFileLimits, OutputFileError, safeFilename, targetFileRoles, } from "./evals/files.js";
4
+ export { CaseFileError, outputContentTypes, outputFileLimits, OutputFileError, safeFilename, targetFileRoles, } from "./evals/files.js";
5
5
  export { runSimulation } from "./evals/simulation.js";
6
6
  export { actualAgentManifestV2, agentManifestDigestV2, attemptBaselineV2, attemptBindingRead, attemptConnectionBundleV2, attemptIdentityV2, dependencyManifestV2, dependencyProviderV2, expectedAgentManifestV2, executionManifestDigestV2, parityEvidenceV2, preflightFindingV2, preflightReportV2, prepareAttemptInputV2, projectMcpConnectionV2, secretFreeBindingV2, surfaceBindingV2, } from "./evals/attempt.js";
7
7
  export { builtins, defineLocalScorer, scoreLocally } from "./evals/scorers.js";
@@ -1,5 +1,9 @@
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
+ /** Message text one record inspects for inline files in all, in UTF-16 code units. */
4
+ export declare const INLINE_FILE_TEXT_PER_RECORD: number;
5
+ /** Whether an attribute is one of the recorded message attributes that can inline files. */
6
+ export declare function isMessageKey(key: string): boolean;
3
7
  /**
4
8
  * Replaces inline files longer than {@link INLINE_FILE_LIMIT} in a recorded message attribute
5
9
  * with their SHA-256 and byte size, so a span that inlines a large file exports the file's
@@ -3,32 +3,64 @@ import { MAX_BODY_BYTES } from "./config.js";
3
3
  /** Inline file content larger than this many UTF-8 bytes is exported as its digest instead. */
4
4
  export const INLINE_FILE_LIMIT = 64 * 1024;
5
5
  const MAX_INLINE_FILE_TEXT = 8 * MAX_BODY_BYTES;
6
+ /** Message text one record inspects for inline files in all, in UTF-16 code units. */
7
+ export const INLINE_FILE_TEXT_PER_RECORD = 2 * MAX_INLINE_FILE_TEXT;
6
8
  /** Message attributes whose JSON can inline files: GenAI blob parts and AI SDK 6 file parts. */
7
9
  const messageKeys = new Set([
8
10
  "gen_ai.input.messages",
9
11
  "gen_ai.output.messages",
10
12
  "ai.prompt.messages",
11
13
  ]);
14
+ /** Whether an attribute is one of the recorded message attributes that can inline files. */
15
+ export function isMessageKey(key) {
16
+ return messageKeys.has(key);
17
+ }
12
18
  /** Strict base64: alphabet characters only, padded to a multiple of four. */
13
19
  const base64 = /^[A-Za-z0-9+/]*={0,2}$/;
14
- const base64DataUrl = /^data:[^,]*;base64,/;
15
- function binaryMimeType(value) {
16
- return (typeof value === "string" &&
17
- !/^text\//i.test(value) &&
18
- value.toLowerCase() !== "application/json");
20
+ /** An RFC 2397 `data:` URL's header, up to the comma before its data. Its `;` parameters are read
21
+ * in code: a pattern repeated per parameter fails on a pathological count (Node's stack overflows,
22
+ * Bun stops matching). */
23
+ const dataUrl = /^data:([^,]*),/;
24
+ /** Whether a `data:` URL header's parameters, after its media type, include `base64`. */
25
+ function base64Header(header) {
26
+ const semicolon = header.indexOf(";");
27
+ // One `;`-separated parameter is exactly `base64`, without splitting them all.
28
+ return semicolon !== -1 && `${header.slice(semicolon)};`.includes(";base64;");
29
+ }
30
+ const hexPair = /^[0-9A-Fa-f]{2}$/;
31
+ /** RFC 2397 data without `;base64`: percent-escaped octets, other characters as UTF-8. Undefined
32
+ * when a `%` does not start an escape. */
33
+ function percentDecoded(payload) {
34
+ // Escapes only shrink the text, so its UTF-8 length bounds the bytes.
35
+ const bytes = Buffer.alloc(Buffer.byteLength(payload, "utf8"));
36
+ let length = 0;
37
+ let start = 0;
38
+ for (let index = payload.indexOf("%"); index !== -1; index = payload.indexOf("%", start)) {
39
+ const hex = payload.slice(index + 1, index + 3);
40
+ if (!hexPair.test(hex))
41
+ return undefined;
42
+ length += bytes.write(payload.slice(start, index), length, "utf8");
43
+ bytes[length++] = Number.parseInt(hex, 16);
44
+ start = index + 3;
45
+ }
46
+ length += bytes.write(payload.slice(start), length, "utf8");
47
+ return bytes.subarray(0, length);
19
48
  }
20
49
  /**
21
- * The bytes an inline file part carries: base64 (plain or as a `data:` URL) is decoded, and
22
- * anything else, such as a text file's content, is taken as UTF-8.
50
+ * The bytes an inline file part carries, the file's own bytes whatever its media type: a `data:`
51
+ * URL decoded by its own encoding (base64 with `;base64`, percent-escapes otherwise), content in
52
+ * the base64 alphabet decoded, and anything else, such as a text file's own text, as UTF-8. A
53
+ * `data:` URL whose data does not decode is taken as UTF-8 too.
23
54
  */
24
- function fileBytes(content, mimeType) {
25
- const prefix = base64DataUrl.exec(content)?.[0].length ?? 0;
26
- const payload = content.slice(prefix);
27
- return (prefix > 0 || binaryMimeType(mimeType)) &&
28
- payload.length % 4 === 0 &&
29
- base64.test(payload)
30
- ? Buffer.from(payload, "base64")
31
- : Buffer.from(content, "utf8");
55
+ function fileBytes(content) {
56
+ const url = dataUrl.exec(content);
57
+ const payload = url ? content.slice(url[0].length) : content;
58
+ const decoded = url && !base64Header(url[1])
59
+ ? percentDecoded(payload)
60
+ : payload.length % 4 === 0 && base64.test(payload)
61
+ ? Buffer.from(payload, "base64")
62
+ : undefined;
63
+ return decoded ?? Buffer.from(content, "utf8");
32
64
  }
33
65
  /** The key holding a part's inline content: GenAI `blob` parts and AI SDK 6 `file` parts. */
34
66
  function contentKey(part) {
@@ -44,9 +76,8 @@ function hashNode(value, state, depth) {
44
76
  const part = value;
45
77
  const key = contentKey(part);
46
78
  const inline = key === undefined ? undefined : part[key];
47
- const mimeType = part.mime_type ?? part.mediaType;
48
79
  if (key !== undefined && typeof inline === "string") {
49
- const bytes = fileBytes(inline, mimeType);
80
+ const bytes = fileBytes(inline);
50
81
  if (bytes.byteLength > INLINE_FILE_LIMIT) {
51
82
  const { [key]: _omitted, ...rest } = part;
52
83
  state.changed = true;
@@ -1,14 +1,19 @@
1
1
  import type { HostedToolProvider } from "./types.js";
2
- /** One provider-executed tool call. `arguments` and `result` are content. */
2
+ /** One provider-executed tool call. `arguments`, `result` and `errorText` are content. */
3
3
  export interface HostedToolCall {
4
4
  name: string;
5
5
  callId?: string;
6
+ /** 0-based position of the call's item in the response: the OpenAI `output` index or the
7
+ * Anthropic `content` block index. */
8
+ position: number;
6
9
  /** The provider's label (OpenAI `server_label`) or name (Anthropic `server_name`) for the MCP server. */
7
10
  server?: string;
8
11
  arguments?: unknown;
9
12
  result?: unknown;
10
13
  /** Low-cardinality failure marker for `error.type`; absent when the call succeeded. */
11
14
  errorType?: string;
15
+ /** The provider's own error text for a failed OpenAI MCP call, unscrubbed and unbounded. */
16
+ errorText?: string;
12
17
  }
13
18
  /** One `mcp_list_tools` result: the tools a hosted MCP server offered. */
14
19
  export interface HostedToolListing {
@@ -23,6 +28,12 @@ export interface HostedToolActivity {
23
28
  /** Items that looked like hosted calls but could not be read. */
24
29
  skipped: number;
25
30
  }
31
+ /**
32
+ * A provider's error text as a failed call exports it under content capture: credentials
33
+ * scrubbed first, then at most 1,024 code points with `…` marking a cut, lone surrogates
34
+ * replaced and NUL removed. The Python SDK produces the same text.
35
+ */
36
+ export declare function providerErrorDescription(value: string): string;
26
37
  /**
27
38
  * Reads the hosted tool calls out of a provider response: the `output` items of an OpenAI
28
39
  * Responses API response, or the `content` blocks of an Anthropic Messages API response. An array